cloudflare / cloudflare/vinext
Cache: derive foreground stale-entry revalidation from whether the consumer persists to a server cache (not just prerender state)
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## Next.js Change
**Commit:** [`89ca693`](https://github.com/vercel/next.js/commit/89ca693c2082770f49a2c1964ba78e8971f3d146)
**PR:** [#96731](https://github.com/vercel/next.js/pull/96731)
## What changed
Fixes stale-cache lifetime extension across nested caches. Foreground (synchronous) revalidation of a stale cache entry is necessary whenever a stale result **will be persisted by another server cache**, not only when the request is prerendering. Otherwise a cache created during a dynamic request can consume a stale inner value and **extend its lifetime** by persisting it into the outer cache.
Previously this decision came from `shouldRevalidateStaleCacheEntryInForeground(workUnitStore)`, which was tied to whether the request was prerendering. That missed the dynamic-request case where an outer `'use cache'` (or `unstable_cache`) scope reads a stale inner entry and stores it.
### Mechanism (from the diff)
- `shouldRevalidateStaleCacheEntryInForeground` is replaced by `willConsumerServerCache(workUnitStore)`, modeling the capability as **whether the immediate work-unit consumer will persist the result in a server cache** — rather than inheriting it through the full outer scope chain.
- Cache-store creation now sets `consumerWillServerCache: true` on `'cache'` and `'private-cache'` work units (replacing the per-store `shouldRevalidateStaleCacheEntryInForeground` value).
- **Server cache** and **static prerender** work units are treated as server-caching consumers; **runtime prerenders** are not.
- In `cache()`, the stale-entry regeneration guard uses `willConsumerServerCache(workUnitStore)` instead of the old prerender-derived check. When the entry is stale and its consumer will persist the result in a server cache, don't serve the stale entry (which would unnecessarily shorten the generated result's lifetime) — regenerate it now since we're not time-constrained.
Touched files: `work-unit-async-storage.external.ts`, `patch-fetch.ts`, `use-cache-wrapper.ts`, `unstable-cache.ts`.
## Impact on vinext
vinext reimplements `'use cache'`, `unstable_cache`, and ISR stale-while-revalidate. If vinext decides foreground vs background revalidation purely from "is this a prerender", it has the same bug: during a dynamic request, an outer cache scope that reads a stale inner entry will persist that stale value, extending its lifetime instead of regenerating it synchronously.
What to check/do:
1. **Base foreground revalidation on the consumer's persistence capability**, not on prerender state alone. Model a `willConsumerServerCache` / `consumerWillServerCache` flag: is the immediate work-unit consumer going to persist the result in a server cache?
2. **Classify consumers correctly:** server-cache scopes (`'use cache'`, `unstable_cache`) and static prerenders are server-caching consumers; a plain dynamic request and runtime prerenders are not — **unless** they are nested inside a server-caching scope.
3. **Regenerate stale entries in the foreground when the consumer will persist them**, so a stale inner value doesn't get its lifetime extended by an outer cache.
4. **Propagate the capability to the immediate consumer** rather than inheriting through the whole outer scope chain (an inner cache read from a dynamic request whose immediate consumer is an outer `'use cache'` must revalidate in foreground).
5. **Port the regression test.** Next.js added a `cache-consumer-foreground-revalidate` fixture: an outer `'use cache'` scope consuming a stale `unstable_cache` entry during a dynamic request.
## Related
- #2820 — Cache: reuse completed entries for the rest of a request
- #2819 — Cache: discard only entries that predate a tag revalidation
- #2002 — Cache Components: exclude short-stale `'use cache'` entries from prerenders
- #1936 — Dev cache handler: serve stale `'use cache'` entries until `expire`
Contributor guide
Research direction
Inspect vinext's implementations of 'use cache', unstable_cache, and ISR, using work-unit-async-storage.external.ts, patch-fetch.ts, use-cache-wrapper.ts, and unstable-cache.ts from the referenced Next.js change as context. Find or port the cache-consumer-foreground-revalidate fixture, then verify that an outer server cache regenerates a stale inner entry during a dynamic request while plain dynamic and runtime-prerender consumers do not.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- next.js, typescript
- Domain
- backend, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100