From 806091c8d25bb3dce011a06207cdfbc081fa8c87 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:14:02 +0000 Subject: [PATCH 1/2] perf: Warm docs manifests and deduplicate builds --- src/routes/api/github/webhook.ts | 33 ++++++++++++++ src/utils/docs-warm.server.ts | 59 ++++++++++++++++++++++++ src/utils/docs.functions.ts | 78 ++++++++++++++++++++------------ 3 files changed, 142 insertions(+), 28 deletions(-) create mode 100644 src/utils/docs-warm.server.ts diff --git a/src/routes/api/github/webhook.ts b/src/routes/api/github/webhook.ts index cfe009743..5c69a1c01 100644 --- a/src/routes/api/github/webhook.ts +++ b/src/routes/api/github/webhook.ts @@ -166,6 +166,11 @@ export const Route = createFileRoute("/api/github/webhook")({ .map((library) => `docs:${library.id}:branch:${gitRef}`), ]; + const affectedLibraries = libraries.filter( + (library) => + library.repo === repo && library.latestBranch === gitRef, + ); + const invalidate = async () => { const [staleContentCount, staleArtifactCount] = await Promise.all([ markGitHubContentStale({ repo, gitRef }), @@ -176,6 +181,27 @@ export const Route = createFileRoute("/api/github/webhook")({ return { purge, staleArtifactCount, staleContentCount }; }; + const warmCaches = async () => { + const { warmDocsArtifacts } = await import( + '~/utils/docs-warm.server' + ); + + await Promise.all( + affectedLibraries.map((library) => + warmDocsArtifacts({ + repo: library.repo, + branch: gitRef, + docsRoot: library.docsRoot ?? 'docs', + }).catch((error) => { + console.warn( + `[GitHub webhook] docs cache warm-up failed for ${library.repo}@${gitRef}`, + error, + ); + }), + ), + ); + }; + if ( scheduleHostRuntimeTask(async () => { try { @@ -187,6 +213,10 @@ export const Route = createFileRoute("/api/github/webhook")({ repo, }); } + + // Proactively rebuild manifests so the next user request is + // served from cache rather than triggering an N+1 build. + await warmCaches(); }) ) { return jsonResponse({ @@ -200,6 +230,9 @@ export const Route = createFileRoute("/api/github/webhook")({ const { purge, staleArtifactCount, staleContentCount } = await invalidate(); + // Warm caches inline when background scheduling is unavailable. + await warmCaches(); + return jsonResponse({ ok: true, gitRef, diff --git a/src/utils/docs-warm.server.ts b/src/utils/docs-warm.server.ts new file mode 100644 index 000000000..616875a3c --- /dev/null +++ b/src/utils/docs-warm.server.ts @@ -0,0 +1,59 @@ +/** + * Server-only utility for pre-warming the docs artifact cache. + * + * Call warmDocsArtifacts after marking artifacts stale (e.g. from a GitHub + * webhook) so the next user request is served from cache rather than + * triggering an on-request N+1 GitHub API call. + */ +import { getCachedDocsArtifact } from './github-content-cache.server' +import { + buildDocsManifest, + buildDocsPathManifest, +} from './docs.functions' + +type DocsManifest = { + paths: Array + redirects: Record +} + +function isDocsManifest(value: unknown): value is DocsManifest { + return ( + typeof value === 'object' && + value !== null && + 'paths' in value && + 'redirects' in value && + Array.isArray((value as DocsManifest).paths) && + typeof (value as DocsManifest).redirects === 'object' + ) +} + +export async function warmDocsArtifacts({ + repo, + branch, + docsRoot, +}: { + repo: string + branch: string + docsRoot: string +}) { + await Promise.all([ + getCachedDocsArtifact({ + repo, + gitRef: branch, + docsRoot, + artifactType: 'docs-manifest', + artifactKey: 'default', + isValue: isDocsManifest, + build: () => buildDocsManifest({ repo, branch, docsRoot }), + }), + getCachedDocsArtifact({ + repo, + gitRef: branch, + docsRoot, + artifactType: 'docs-path-manifest', + artifactKey: 'default', + isValue: isDocsManifest, + build: () => buildDocsPathManifest({ repo, branch, docsRoot }), + }), + ]) +} diff --git a/src/utils/docs.functions.ts b/src/utils/docs.functions.ts index 71969bf16..320b8d59f 100644 --- a/src/utils/docs.functions.ts +++ b/src/utils/docs.functions.ts @@ -104,6 +104,10 @@ const docsRedirectInput = v.object({ // Matches RAW_FETCH_CONCURRENCY in github-example.server.ts. const DOCS_MANIFEST_FETCH_CONCURRENCY = 6 +// In-flight deduplication for concurrent cold-start manifest builds. +// Keyed by `${repo}@${branch}:${docsRoot}` for each manifest type. +const pendingManifestBuilds = new Map>() + export async function mapWithConcurrency( values: Array, concurrency: number, @@ -261,7 +265,7 @@ export async function collectRedirectEntriesForFile( return entries } -async function buildDocsManifest({ +export async function buildDocsManifest({ repo, branch, docsRoot, @@ -270,40 +274,58 @@ async function buildDocsManifest({ branch: string docsRoot: string }): Promise { - const { fetchApiContents, fetchRepoFile } = await loadDocumentsServerModule() - const nodes = await fetchApiContents(repo, branch, docsRoot) + const key = `manifest:${repo}@${branch}:${docsRoot}` - if (!nodes) { - return { paths: [], redirects: {} } + const inFlight = pendingManifestBuilds.get(key) + if (inFlight) { + return inFlight } - const markdownFiles = flattenDocsNodes(nodes).filter((node) => - node.path.endsWith('.md'), - ) - const paths = new Set() - - // A recoverable error on one file must not fail the whole manifest build - // (see collectRedirectEntriesForFile). - const redirectsByFile = await mapWithConcurrency( - markdownFiles, - DOCS_MANIFEST_FETCH_CONCURRENCY, - (node) => - collectRedirectEntriesForFile(node, { - docsRoot, - fetchFile: (filePath) => fetchRepoFile(repo, branch, filePath), - onCanonicalPath: (canonicalPath) => paths.add(canonicalPath), - }), - ) + const build = async (): Promise => { + try { + const { fetchApiContents, fetchRepoFile } = + await loadDocumentsServerModule() + const nodes = await fetchApiContents(repo, branch, docsRoot) - return { - paths: Array.from(paths), - redirects: buildRedirectManifest(redirectsByFile.flat(), { - label: `docs redirects for ${repo}@${branch}:${docsRoot}`, - }), + if (!nodes) { + return { paths: [], redirects: {} } + } + + const markdownFiles = flattenDocsNodes(nodes).filter((node) => + node.path.endsWith('.md'), + ) + const paths = new Set() + + // A recoverable error on one file must not fail the whole manifest build + // (see collectRedirectEntriesForFile). + const redirectsByFile = await mapWithConcurrency( + markdownFiles, + DOCS_MANIFEST_FETCH_CONCURRENCY, + (node) => + collectRedirectEntriesForFile(node, { + docsRoot, + fetchFile: (filePath) => fetchRepoFile(repo, branch, filePath), + onCanonicalPath: (canonicalPath) => paths.add(canonicalPath), + }), + ) + + return { + paths: Array.from(paths), + redirects: buildRedirectManifest(redirectsByFile.flat(), { + label: `docs redirects for ${repo}@${branch}:${docsRoot}`, + }), + } + } finally { + pendingManifestBuilds.delete(key) + } } + + const promise = build() + pendingManifestBuilds.set(key, promise) + return promise } -async function buildDocsPathManifest({ +export async function buildDocsPathManifest({ repo, branch, docsRoot, From d0a64ed60d44bc08781529051991d69e3ab0e8a4 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 11:14:59 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- src/utils/docs-warm.server.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/utils/docs-warm.server.ts b/src/utils/docs-warm.server.ts index 616875a3c..afd762998 100644 --- a/src/utils/docs-warm.server.ts +++ b/src/utils/docs-warm.server.ts @@ -6,10 +6,7 @@ * triggering an on-request N+1 GitHub API call. */ import { getCachedDocsArtifact } from './github-content-cache.server' -import { - buildDocsManifest, - buildDocsPathManifest, -} from './docs.functions' +import { buildDocsManifest, buildDocsPathManifest } from './docs.functions' type DocsManifest = { paths: Array