Skip to content

fix(pages): land the stale-while-revalidate prefetch refresh instead of discarding it - #227

Merged
Brooooooklyn merged 1 commit into
mainfrom
fix/prefetch-swr-stale-refresh
Jun 20, 2026
Merged

Brooooooklyn merged 1 commit into
mainfrom
fix/prefetch-swr-stale-refresh

Conversation

@hyfdev

@hyfdev hyfdev commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Prefetched pages configured with a [stale, expires] cache window now actually serve the revalidated data after they go stale — instead of serving stale data for the entire expires window.

The bug

With cacheFor: [stale, expires] (stale < expires), navigating to a stale-but-usable prefetch entry correctly serves the stale page and fires a background revalidate. But it then stored the refreshed promise via _prefetchCache.set(...), and set() refuses to overwrite a still-usable entry — so the revalidated data was silently discarded. The cache kept serving the stale page (and re-firing a discarded refresh) until the entry fully expired, defeating stale-while-revalidate.

Expected-correct behavior

SWR = serve stale, then replace with the revalidated data so the next read is fresh.

The fix

In the revalidate branch (navigation-engine.ts loadNavigationShell), flush() the stale entry before re-set()ing it with the background promise, so the refreshed data replaces the stale one. This is a one-line call-site change that leaves set()'s "don't overwrite a usable entry" guard intact for all other callers. The default single-duration cacheFor (stale == expires) is unaffected — that path never reaches this branch as a usable entry.

Tests

Regression test added in pages-client.test.ts: prefetch with cacheFor: ['5ms','10s'], let it go stale, navigate (serves v1 + revalidates), then assert the next read returns the revalidated v2. Fails on origin/main (returns the discarded-stale v1) and passes after. Local gate: pages-client.test.ts 17/17; full --project unit zero-collateral vs a clean origin/main baseline; packages/void typecheck + lint + fmt clean. Verified the only other set() call in this function is a fresh insert (no usable entry present) and correctly needs no flush.

Prepared with AI assistance.

…of discarding it

When a prefetch cacheFor uses a [stale, expires] tuple with stale < expires,
navigating to a stale-but-usable entry correctly served the stale page and
fired a background revalidate — but then called _prefetchCache.set() to store
the refreshed promise. set() refuses to overwrite a still-usable entry, so the
revalidated data was discarded and the cache kept serving stale data (and
re-firing a discarded refresh) for the entire expires window.

Flush the stale entry before set() in the revalidate branch so the refreshed
data replaces it, matching stale-while-revalidate semantics. The default
cacheFor (stale == expires) is unaffected.
@netlify

netlify Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

❌ Deploy Preview for staging-void-cloud failed.

Name Link
🔨 Latest commit 602d4d8
🔍 Latest deploy log https://app.netlify.com/projects/staging-void-cloud/deploys/6a341de05d801e000865e113

@hyfdev
hyfdev marked this pull request as ready for review June 18, 2026 16:52
@Brooooooklyn
Brooooooklyn merged commit bcc51f8 into main Jun 20, 2026
6 of 10 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/prefetch-swr-stale-refresh branch June 20, 2026 07:59
cpojer pushed a commit that referenced this pull request Sep 11, 2026
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants