Skip to content

Commit ca88f2c

Browse files
fix(content-cache): force sync refresh on operator invalidation (#933)
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.
1 parent 3a136f2 commit ca88f2c

1 file changed

Lines changed: 34 additions & 6 deletions

File tree

src/utils/github-content-cache.server.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ function isFresh(staleAt: Date) {
128128
return staleAt.getTime() > Date.now()
129129
}
130130

131+
// markGitHubContentStale / markDocsArtifactsStale set staleAt to the epoch
132+
// (new Date(0)) as a sentinel for "forcibly invalidated" — an admin clicked
133+
// the purge button or a push webhook fired. In that case we must NOT serve
134+
// SWR-stale: the operator's intent is "get fresh content on the very next
135+
// request." Natural TTL expiry (staleAt drifts past now within the normal
136+
// window) still SWRs as before. The row stays around so the bottom of
137+
// getCachedGitHubContent / getCachedDocsArtifact can still fall back to it
138+
// if GitHub is unreachable.
139+
function isForciblyStale(staleAt: Date) {
140+
return staleAt.getTime() <= 0
141+
}
142+
131143
function queueRefresh(key: string, fn: () => Promise<unknown>) {
132144
void withPendingRefresh(key, fn).catch((error) => {
133145
console.error(`[GitHub Cache] Failed to refresh ${key}:`, error)
@@ -253,8 +265,9 @@ async function getCachedGitHubContent<T>(opts: {
253265

254266
const cachedRow = await readRow()
255267
const storedValue = opts.readStoredValue(cachedRow)
268+
const forciblyStale = !!cachedRow && isForciblyStale(cachedRow.staleAt)
256269

257-
if (storedValue !== undefined) {
270+
if (storedValue !== undefined && !forciblyStale) {
258271
if (cachedRow && isFresh(cachedRow.staleAt)) {
259272
return storedValue
260273
}
@@ -272,8 +285,15 @@ async function getCachedGitHubContent<T>(opts: {
272285
return withPendingRefresh(opts.cacheKey, async () => {
273286
const latestRow = await readRow()
274287
const latestValue = opts.readStoredValue(latestRow)
275-
276-
if (latestValue !== undefined && latestRow && isFresh(latestRow.staleAt)) {
288+
const latestForciblyStale =
289+
!!latestRow && isForciblyStale(latestRow.staleAt)
290+
291+
if (
292+
latestValue !== undefined &&
293+
latestRow &&
294+
!latestForciblyStale &&
295+
isFresh(latestRow.staleAt)
296+
) {
277297
return latestValue
278298
}
279299

@@ -388,8 +408,9 @@ export async function getCachedDocsArtifact<T>(opts: {
388408
const cachedRow = await readRow()
389409
const storedValue =
390410
cachedRow && opts.isValue(cachedRow.payload) ? cachedRow.payload : undefined
411+
const forciblyStale = !!cachedRow && isForciblyStale(cachedRow.staleAt)
391412

392-
if (storedValue !== undefined) {
413+
if (storedValue !== undefined && !forciblyStale) {
393414
if (cachedRow && isFresh(cachedRow.staleAt)) {
394415
return storedValue
395416
}
@@ -408,8 +429,15 @@ export async function getCachedDocsArtifact<T>(opts: {
408429
latestRow && opts.isValue(latestRow.payload)
409430
? latestRow.payload
410431
: undefined
411-
412-
if (latestValue !== undefined && latestRow && isFresh(latestRow.staleAt)) {
432+
const latestForciblyStale =
433+
!!latestRow && isForciblyStale(latestRow.staleAt)
434+
435+
if (
436+
latestValue !== undefined &&
437+
latestRow &&
438+
!latestForciblyStale &&
439+
isFresh(latestRow.staleAt)
440+
) {
413441
return latestValue
414442
}
415443

0 commit comments

Comments
 (0)