diff --git a/scripts/check-live-surfaces.mjs b/scripts/check-live-surfaces.mjs index e947ba9..822969c 100644 --- a/scripts/check-live-surfaces.mjs +++ b/scripts/check-live-surfaces.mjs @@ -28,18 +28,47 @@ function check(name, condition, detail = '') { } const cache = new Map(); +// A network failure is a failed check, not a crash: status 0 with the error in the body. async function fetchText(url) { if (cache.has(url)) return cache.get(url); - const res = await fetch(url, { redirect: 'manual', headers: { 'user-agent': 'next-docs-live-check/1' } }); - const body = await res.text(); - const out = { status: res.status, headers: res.headers, body, bytes: Buffer.byteLength(body, 'utf8') }; + let out; + try { + const res = await fetch(url, { + redirect: 'manual', + headers: { 'user-agent': 'next-docs-live-check/1' }, + signal: AbortSignal.timeout(20_000), + }); + const body = await res.text(); + out = { status: res.status, headers: res.headers, body, bytes: Buffer.byteLength(body, 'utf8') }; + } catch (error) { + out = { status: 0, headers: new Headers(), body: '', bytes: 0, error: error?.cause?.code ?? error?.name ?? String(error) }; + } cache.set(url, out); return out; } +// Bounded concurrency so ~200 link checks do not open ~200 connections at once. +// Side effects only; callers record results themselves. An exception from one +// item is recorded as a failed check and does not stop the other workers. +async function forEachLimit(items, limit, fn) { + let next = 0; + await Promise.all( + Array.from({ length: Math.min(limit, items.length) }, async () => { + while (next < items.length) { + const item = items[next++]; + try { + await fn(item); + } catch (error) { + failures.push(`check for ${String(item)} threw: ${error?.message ?? error}`); + } + } + }), + ); +} + async function page(url) { const r = await fetchText(url); - check(`200 ${url}`, r.status === 200, `status ${r.status}`); + check(`200 ${url}`, r.status === 200, `status ${r.status}${r.error ? ` (${r.error})` : ''}`); return r; } @@ -90,15 +119,13 @@ if (capabilityMap) { for (const w of c.webhooks) if (w.url) linked.add(w.url); } let broken = 0; - await Promise.all( - [...linked].map(async (u) => { - const r = await fetchText(u); - if (r.status !== 200) { - broken += 1; - failures.push(`map link ${u} returned ${r.status}`); - } - }), - ); + await forEachLimit([...linked], 8, async (u) => { + const r = await fetchText(u); + if (r.status !== 200) { + broken += 1; + failures.push(`map link ${u} returned ${r.status}${r.error ? ` (${r.error})` : ''}`); + } + }); check(`all ${linked.size} capability-map links resolve`, broken === 0); for (const b of capabilityMap.bundles) {