cloudflare / cloudflare/vinext

Detect 'use cache' module-scope deadlocks early in dev

Open
#1,126 0 comments 0 reactions 0 assignees View on GitHub
nextjs-tracking
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.