mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
When a `'use cache'` fill stalls in dev today, the user has to wait the full `useCacheTimeout` — 54 seconds by default — before any error surfaces, and the resulting `UseCacheTimeoutError` is too generic to point at the cause. A common cause is module-scoped state that ends up joining a promise from the outer render scope — for example a top-level `Map<string, Promise>` used to dedupe fetches, where the cache body and the outer scope both await the same promise. That promise hangs because Next.js intentionally converts uncached fetches into hanging promises during prerendering (and in dev while filling caches in the static or runtime stage), so the cache function ends up waiting forever on an outer-scope fetch that will never resolve. Most users reload long before the timeout appears, so they never see any signal that something specific is wrong. This change adds a dev-only probe that surfaces this class of deadlock earlier. Once a cache fill has been idle for ten seconds, the dev server re-runs the same cache function in a worker thread with a fresh module scope. If it completes there, the hang in the main process is attributable to outer-scope state, and we abort the fill with `UseCacheDeadlockError` whose message points the user at the dedupe pattern and how to fix it. If the probe also hangs or fails for any other reason — decode failure, missing module, the body throwing — we fall back to the regular cache-fill timeout, so the probe is a positive signal only and never false-positives a deadlock. The probe is gated on `__NEXT_DEV_SERVER` and tree-shakes out of the production runtime entirely. The worker pool is lazy, with no process forked until a probe actually fires, reused across probes for the same dev session, and torn down on HMR or worker crash. A snapshot of the outer request store is forwarded to the worker so that cache functions — including private caches that read `cookies()`, `headers()`, or `draftMode()` — see the same values they would in a real invocation.
59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
import { cookies } from 'next/headers'
|
|
import { Suspense } from 'react'
|
|
import { getData, preload } from '../shared'
|
|
|
|
const sharedUrl =
|
|
'https://next-data-api-endpoint.vercel.app/api/random?page=private-cookies'
|
|
|
|
// A `'use cache: private'` body that legitimately reads `cookies()` AND
|
|
// joins the module-scoped hanging promise from `shared.ts`. The probe must:
|
|
//
|
|
// 1. Forward the real cookie values into the worker's request store so
|
|
// `cookies()` returns the same values as it would in the real fill
|
|
// (rather than throwing).
|
|
// 2. Run the body in a fresh module scope so `getData(sharedUrl)` returns
|
|
// a fresh fetch instead of joining the outer scope's hanging promise.
|
|
//
|
|
// Both pieces have to work for this test to pass; if request-snapshot
|
|
// forwarding regresses, this fixture catches it before any "no-snapshot"
|
|
// fallback path.
|
|
async function getCachedData(): Promise<string> {
|
|
'use cache: private'
|
|
|
|
// Read a cookie — exercises the worker's reconstructed `cookies` object.
|
|
const sessionCookie = (await cookies()).get('next-session')
|
|
|
|
// Joins the outer fetch via the module-scoped dedupe map (see note in
|
|
// ../shared.ts).
|
|
const text = await getData(sharedUrl).then((res) => res.text())
|
|
|
|
return `${sessionCookie?.value ?? 'no-session'}:${text}`
|
|
}
|
|
|
|
async function Cached() {
|
|
try {
|
|
const data = await getCachedData()
|
|
return <p id="result">{data}</p>
|
|
} catch (error: any) {
|
|
return <p id="result">Error: {error.message}</p>
|
|
}
|
|
}
|
|
|
|
async function Runtime() {
|
|
// Touch cookies in the outer scope to advance to the runtime stage and
|
|
// start the deadlock-prone outer fetch.
|
|
await cookies()
|
|
|
|
preload(sharedUrl)
|
|
|
|
return <Cached />
|
|
}
|
|
|
|
export default function Page() {
|
|
return (
|
|
<Suspense fallback="Loading...">
|
|
<Runtime />
|
|
</Suspense>
|
|
)
|
|
}
|