From 275beb6e6dad190d8ec87e133a42adf6e496c1e4 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 19 May 2026 19:34:55 -0600 Subject: [PATCH] fix(content-cache): force sync refresh on operator invalidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit markGitHubContentStale and markDocsArtifactsStale set staleAt to the epoch as a "force refresh on next read" sentinel — used by the admin invalidate button and the GitHub push webhook. The SWR readers in getCachedGitHubContent and getCachedDocsArtifact ignored that intent: when a row had positive cached content but was stale, they returned the cached value and fire-and-forgot a background refresh. On Netlify Functions the background promise often never lands, the rendered page goes back into the CDN with stale content, and the next CDN revalidation pulls the same stale row — invalidation effectively never converges. Add isForciblyStale(staleAt) that recognizes the epoch sentinel, and route forcibly-stale rows past the SWR branch into withPendingRefresh so the very next request awaits a fresh origin fetch. Natural TTL expiry still SWRs as before. The stale-on-origin-error fallback at the bottom of withPendingRefresh is preserved, so a failed GitHub call after invalidation still returns the previously cached value instead of erroring. --- src/utils/github-content-cache.server.ts | 40 ++++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/utils/github-content-cache.server.ts b/src/utils/github-content-cache.server.ts index 782d8568..ab080607 100644 --- a/src/utils/github-content-cache.server.ts +++ b/src/utils/github-content-cache.server.ts @@ -128,6 +128,18 @@ function isFresh(staleAt: Date) { return staleAt.getTime() > Date.now() } +// markGitHubContentStale / markDocsArtifactsStale set staleAt to the epoch +// (new Date(0)) as a sentinel for "forcibly invalidated" — an admin clicked +// the purge button or a push webhook fired. In that case we must NOT serve +// SWR-stale: the operator's intent is "get fresh content on the very next +// request." Natural TTL expiry (staleAt drifts past now within the normal +// window) still SWRs as before. The row stays around so the bottom of +// getCachedGitHubContent / getCachedDocsArtifact can still fall back to it +// if GitHub is unreachable. +function isForciblyStale(staleAt: Date) { + return staleAt.getTime() <= 0 +} + function queueRefresh(key: string, fn: () => Promise) { void withPendingRefresh(key, fn).catch((error) => { console.error(`[GitHub Cache] Failed to refresh ${key}:`, error) @@ -253,8 +265,9 @@ async function getCachedGitHubContent(opts: { const cachedRow = await readRow() const storedValue = opts.readStoredValue(cachedRow) + const forciblyStale = !!cachedRow && isForciblyStale(cachedRow.staleAt) - if (storedValue !== undefined) { + if (storedValue !== undefined && !forciblyStale) { if (cachedRow && isFresh(cachedRow.staleAt)) { return storedValue } @@ -272,8 +285,15 @@ async function getCachedGitHubContent(opts: { return withPendingRefresh(opts.cacheKey, async () => { const latestRow = await readRow() const latestValue = opts.readStoredValue(latestRow) - - if (latestValue !== undefined && latestRow && isFresh(latestRow.staleAt)) { + const latestForciblyStale = + !!latestRow && isForciblyStale(latestRow.staleAt) + + if ( + latestValue !== undefined && + latestRow && + !latestForciblyStale && + isFresh(latestRow.staleAt) + ) { return latestValue } @@ -388,8 +408,9 @@ export async function getCachedDocsArtifact(opts: { const cachedRow = await readRow() const storedValue = cachedRow && opts.isValue(cachedRow.payload) ? cachedRow.payload : undefined + const forciblyStale = !!cachedRow && isForciblyStale(cachedRow.staleAt) - if (storedValue !== undefined) { + if (storedValue !== undefined && !forciblyStale) { if (cachedRow && isFresh(cachedRow.staleAt)) { return storedValue } @@ -408,8 +429,15 @@ export async function getCachedDocsArtifact(opts: { latestRow && opts.isValue(latestRow.payload) ? latestRow.payload : undefined - - if (latestValue !== undefined && latestRow && isFresh(latestRow.staleAt)) { + const latestForciblyStale = + !!latestRow && isForciblyStale(latestRow.staleAt) + + if ( + latestValue !== undefined && + latestRow && + !latestForciblyStale && + isFresh(latestRow.staleAt) + ) { return latestValue }