Cap concurrent per-author listing fetches during indexing (AI)

Wave 2 of the crawl fetched every author's directory listing via a
plain Promise.all, with no limit — a site with hundreds of authors
fired one simultaneous request per author on every cold-cache load.
Routed through mapWithConcurrency/MAX_CONCURRENT_REQUESTS, the same
cap hydrateArticles() already respects for content fetches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
biosiasandClaude Sonnet 5 committed 2026-09-22 16:05:54 +02:00
1 parent cf2733368f
commit 057f6673cb
1 file changed
+21 -16
+21 -16
View File
@@ -4,13 +4,16 @@ import { mapWithConcurrency } from './concurrency.js';
import { getAllCacheEntries, setCacheEntry, pruneCache } from './cache.js';
import { t } from './i18n.js';
// Caps simultaneous markdown content fetches during hydration (see
// hydrateArticle()/hydrateArticles()) — the only per-article requests left,
// since indexing no longer needs a per-article listing fetch at all (every
// stub field is encoded in its file's own name — see buildStub()). Raise it
// for a fast/local nginx, lower it for a slow or shared/production instance
// — see .claude/nginx-sitemap-crawler-plan.md §2a for the original rationale
// (written when this also governed indexing requests).
// Caps simultaneous requests in the two places this module still fires one
// per item: markdown content fetches during hydration (see
// hydrateArticle()/hydrateArticles()) and each author's own directory
// listing during wave 2 of indexing (see runCrawl()) — indexing no longer
// needs a per-*article* fetch at all (every stub field is encoded in its
// file's own name — see buildStub()), but still fetches one listing per
// author. Raise it for a fast/local nginx, lower it for a slow or shared/
// production instance — see .claude/nginx-sitemap-crawler-plan.md §2a for
// the original rationale (written when this also governed per-article
// indexing requests).
const MAX_CONCURRENT_REQUESTS = 10;
const MONTH_ABBREVIATIONS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
@@ -333,13 +336,15 @@ async function runCrawl() {
// Wave 2 — always fetched live, in full, for every author (not just the
// current page): each author's own directory listing, with per-file
// mtimes. This is what builds the flat, cross-author list that drives
// global sorting and pagination — see the plan doc's "keep the flat
// feed" decision. It's also the *only* per-author network request
// indexing ever needs: every stub field (slug, date, tags) is parsed
// straight out of each article file's own name (see
// buildStub/parseArticleFileName) — there's no per-article fetch at
// index time. The `type === 'file'` filter below also means any
// mtimes. Capped by MAX_CONCURRENT_REQUESTS like hydrateArticles() below
// — a site with many authors would otherwise fire one listing request
// per author simultaneously, with no limit. This is what builds the
// flat, cross-author list that drives global sorting and pagination —
// see the plan doc's "keep the flat feed" decision. It's also the *only*
// per-author network request indexing ever needs: every stub field
// (slug, date, tags) is parsed straight out of each article file's own
// name (see buildStub/parseArticleFileName) — there's no per-article
// fetch at index time. The `type === 'file'` filter below also means any
// subdirectory (the author's shared `media/` folder, a `draft/` folder —
// see "Adding an article" in the README) is skipped without ever being
// descended into. The author's own bio file (`<author>.md`) is filtered
@@ -350,7 +355,7 @@ async function runCrawl() {
// origin, CORS) remains visible via getCrawlStats() instead of just
// silently producing an empty article list.
let authorsFailed = 0;
const authorFileLists = await Promise.all(authors.map(async author => {
const authorFileLists = await mapWithConcurrency(authors, MAX_CONCURRENT_REQUESTS, async author => {
const authorRoot = `${ARTICLES_URL}${encodeURIComponent(author.name)}/`;
const bioFileName = `${author.name}.md`;
try {
@@ -362,7 +367,7 @@ async function runCrawl() {
authorsFailed += 1;
return [];
}
}));
});
const allFiles = authorFileLists.flat();
// Every stub is parsed synchronously from data already in hand — no