Files
Hendrik LiebauandGitHub 1669edee37 Hold cacheSignal across unstable_cache foreground revalidation (#93617)
When `unstable_cache` finds a stale entry during the prospective phase of a prerender, it dispatches a foreground revalidation and returns the in-flight promise. The wrapper's `cacheSignal.beginRead` / `endRead` pair was scoped only to the synchronous lookup.`endRead` in the outer `finally` fires the moment the function returns the promise, before the recompute and `cacheNewResult` have completed. The prospective prerender's `cacheSignal` then resolves `cacheReady` while the recompute is still in flight, ending the prospective phase early. Any `'use cache'` invocation rendered after the `unstable_cache` await in the same component never gets reached, so its RDC entry is never populated, and the final prerender phase throws "Unexpected cache miss after cache warming phase during prerendering."

Returning `await pendingRevalidates[invocationKey]` instead of returning the promise directly keeps the wrapper function suspended until the recompute and `cacheNewResult` actually complete. The outer `finally` then runs `endRead` at the right time, the prospective phase keeps waiting, and downstream `'use cache'` invocations are reached and populate the RDC as expected.

Adds a regression test under the `use-cache` suite that exercises an `unstable_cache` await followed by a downstream `'use cache'` component. Without the fix, the test fails consistently under `__NEXT_CACHE_COMPONENTS=true` with the cache-miss-after-warming error.
2026-05-08 00:42:44 +02:00

51 lines
1.5 KiB
TypeScript

import { revalidateTag, unstable_cache } from 'next/cache'
import { setTimeout } from 'node:timers/promises'
const getUnstableTime = unstable_cache(
async () => {
// Small artificial delay so the foreground-await is actually in flight when
// the prospective prerender decides whether `cacheSignal` is ready.
await setTimeout(100)
return new Date().toISOString()
},
['blocked-by-unstable-cache'],
{ tags: ['blocked-by-unstable-cache-tag'], revalidate: false }
)
async function Cached() {
'use cache'
return (
<p>
Cached: <span id="cached-time">{new Date().toISOString()}</span>
</p>
)
}
// Awaiting an `unstable_cache` BEFORE rendering a `'use cache'` component.
// After the tag is revalidated, the next render runs a background revalidation
// prerender. In its prospective phase, the `unstable_cache` lookup hits a stale
// entry and foreground-awaits the recompute. The downstream `<Cached />` must
// still be reached during prospective so its RDC entry is populated for the
// final phase — otherwise the final phase throws "Unexpected cache miss after
// cache warming phase during prerendering".
export default async function Page() {
const time = await getUnstableTime()
return (
<main>
<p>
Unstable: <span id="unstable-time">{time}</span>
</p>
<Cached />
<form
action={async () => {
'use server'
revalidateTag('blocked-by-unstable-cache-tag', 'max')
}}
>
<button id="revalidate">Revalidate</button>
</form>
</main>
)
}