mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
When `'use cache'` fills during `next dev`, there was no timeout, so a stalled fill could hang the request indefinitely. This change restores a dev-mode cache-fill timeout that fires 50 seconds after the fill starts, matching the 50s timeout we already have during prerender. The timer sets `workStore.invalidDynamicUsageError` to a `UseCacheTimeoutError` and aborts the signal passed to React's `renderToReadableStream`, which errors the cache stream. From there the existing handling of `invalidDynamicUsageError` in app-render surfaces the error in the Redbox and the CLI output without any additional plumbing. Prior to PR #85747, `spawnDynamicValidationInDev` reused the prerender store and inherited the prerender cache timeout as a side effect. After that change the validation became staged-rendering based and the timeout stopped applying to dev renders. This PR closes that gap. The timeout is skipped when the render is already in the Dynamic stage, which mirrors prerender, where caches past `await connection()` aren't executed at all. The check is written as exact equality on `RenderStage.Dynamic` rather than a numeric `< Dynamic` comparison because `RenderStage.Abandoned` is numerically higher than Dynamic but semantically represents an aborted initial prospective render whose pending caches still need to be timed out while the outer flow awaits `cacheSignal.cacheReady()`. The 50s duration is shared with the prerender path via a new `USE_CACHE_FILL_TIMEOUT_MS` constant and carries a TODO to derive it from the user-configurable `staticPageGenerationTimeout`. The tests model a realistic migration hazard: a module-scoped in-flight fetch dedupe loader (the hand-rolled variant of the documented `React.cache`-based preload pattern) joined by a `'use cache'` function. That configuration deadlocks because the outer fetch is parked on the Dynamic stage in dev, or returns an intentionally hanging promise during prerender, and the cache never fills.
39 lines
1.6 KiB
TypeScript
39 lines
1.6 KiB
TypeScript
// This module simulates a realistic migration hazard. The documented "preload"
|
|
// pattern (see the "Preloading data" section of the
|
|
// caching-without-cache-components guide in the Next.js docs) wraps the loader
|
|
// in `React.cache`, so its store is tied to the ALS snapshot — `'use cache'`
|
|
// bodies run in a clean snapshot and miss, re-executing the loader in cache
|
|
// scope. That path is safe during migration.
|
|
//
|
|
// What's modeled here is the common-in-the-wild variant that uses a
|
|
// module-scoped `Map` instead of `React.cache`. The map lives above ALS, so a
|
|
// `'use cache'` body joining it reuses the outer-scope promise (request scope
|
|
// or prerender scope) — a promise that was never meant to resolve for the
|
|
// cache:
|
|
// - During prerendering, an uncached fetch in the outer scope returns a
|
|
// hanging promise that intentionally never resolves.
|
|
// - In dev, the outer fetch is parked on the Dynamic stage, which can't
|
|
// advance until the cache fills. The cache awaits the outer fetch via the
|
|
// shared loader, so neither can make progress.
|
|
// In both cases, the cache never fills and times out.
|
|
const pendingFetches = new Map<string, Promise<Response>>()
|
|
|
|
export function getData(url: string): Promise<Response> {
|
|
let promise = pendingFetches.get(url)
|
|
if (!promise) {
|
|
promise = fetch(url)
|
|
pendingFetches.set(url, promise)
|
|
promise.then(
|
|
() => pendingFetches.delete(url),
|
|
() => pendingFetches.delete(url)
|
|
)
|
|
}
|
|
return promise
|
|
}
|
|
|
|
// "Preload" primes the shared loader without awaiting so later callers reuse
|
|
// the stored promise.
|
|
export function preload(url: string): void {
|
|
void getData(url)
|
|
}
|