cloudflare / cloudflare/vinext
Detect 'use cache' module-scope deadlocks early in dev
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## Upstream
- vercel/next.js#93500 (88368254e6d0cc66779b044d0bbedefd0d378ae5) — Detect `'use cache'` module-scope deadlocks early in dev
- vercel/next.js#93538 (608b569cb2f7cfa8251bb7abac2732ab32730f87) — Bundle the `'use cache'` deadlock probe worker (follow-up)
## What changed in Next.js
When a `'use cache'` fill stalls in dev, users currently have to wait the full `useCacheTimeout` (default 54 seconds) before any error surfaces, and the resulting `UseCacheTimeoutError` is too generic. A common cause is module-scoped state (e.g. a top-level `Map` used to dedupe fetches) where the cache body and outer scope both await the same promise — Next.js intentionally converts uncached fetches into hanging promises during prerendering, so the cache function ends up waiting forever.
Next.js now adds a dev-only probe: 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 is attributable to outer-scope state, and the fill is aborted with a new `UseCacheDeadlockError` that points the user at the dedupe pattern and how to fix it. If the probe also hangs or errors, it falls back silently to the regular timeout — the probe is a positive signal only.
Key implementation pieces:
- New files in `packages/next/src/server/dev/`:
- `use-cache-probe-pool.ts` — lazy worker pool, reused across probes, torn down on HMR/crash
- `use-cache-probe-worker.ts` — worker that re-executes the cache function with a fresh module scope
- `use-cache-probe-require-hook.ts` (later removed in #93538)
- New files in `packages/next/src/server/use-cache/`:
- `use-cache-probe-globals.ts`
- `use-cache-probe-scheduler.ts`
- New `UseCacheDeadlockError` in `use-cache-errors.ts`
- A snapshot of the outer request store (cookies, headers, draft mode) is forwarded to the worker so private caches see the same values they would in a real invocation.
- Gated on `__NEXT_DEV_SERVER` so it tree-shakes out of production.
- Follow-up #93538 routes the probe worker through the standard webpack bundling pipeline so React/`react-server-dom-webpack` resolve correctly under the `react-server` condition (previously the worker shipped as plain tsc output and crashed on React 18 apps because top-level `require('react')` fell through to `react.shared-subset`).
## Why this matters for vinext
vinext supports `'use cache'`. Today, when a user hits the same module-scope deadlock pattern in vinext's dev server, they get the same poor DX as upstream Next.js had before this change: a long wait for a generic timeout, no signal pointing at the actual cause.
Porting this would mean:
1. Adding a dev-only probe scheduler around the `'use cache'` wrapper that fires after ~10s of idle fill time.
2. Spawning a worker (Node `worker_threads` in Node dev; not applicable to the Workers production target since this is dev-only) that re-imports the route's RSC entry with a fresh module graph and re-executes the cache function.
3. Forwarding a snapshot of the request store (cookies, headers, draft mode) so private caches resolve the same values.
4. Tearing the pool down on HMR / file invalidation.
5. Surfacing a `UseCacheDeadlockError` with the actionable message instead of the generic timeout.
Worth considering whether Vite's module graph (RSC environment specifically) makes the "fresh module scope" part easier or harder than webpack's loader-tree-driven entry. The fact that Vite has separate environments for RSC/SSR/client (per AGENTS.md) means we already have the plumbing to spin up a fresh RSC module graph; the question is whether we do it via a worker thread or in-process with a separate Vite environment instance.
## Acceptance
- [ ] Probe fires after a configurable idle threshold (default 10s) on hung `'use cache'` fills in dev only
- [ ] Probe reuses a long-lived worker (or environment) across calls in the same dev session
- [ ] Probe forwards the outer request store so private caches resolve correctly
- [ ] If the probe completes successfully while the main fill is still hung, the fill is aborted with `UseCacheDeadlockError` pointing at module-scope state as the likely cause
- [ ] If the probe also hangs or errors, falls back to the existing cache-fill timeout — never false-positives
- [ ] Tree-shaken out of production builds (Workers and Node), gated on a dev-only flag
- [ ] Ports the relevant test cases from `test/e2e/app-dir/use-cache-deadlock-probe/`
Contributor guide
Research direction
Start by locating vinext’s existing 'use cache' wrapper and its Vite RSC/SSR environment plumbing, then compare the upstream use-cache-probe-pool.ts, use-cache-probe-worker.ts, use-cache-probe-scheduler.ts, and use-cache-errors.ts designs. Review test/e2e/app-dir/use-cache-deadlock-probe/ and verify the acceptance cases, including safe fallback, request-store forwarding, HMR teardown, and production tree-shaking.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, react, typescript, vite
- Domain
- backend, devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100