cloudflare / cloudflare/vinext
Anchor async local storage instances to global symbols to survive duplicate module evaluation
- Dominant language
- TypeScript
- Stars
- 8.8k
- Forks
- 406
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 120
Description
## Upstream change
Next.js commit [`5817bd1`](https://github.com/vercel/next.js/commit/5817bd1def537b43d5cd20de9cd08537a33d8f35) ("Anchor the async local storage instances to global symbols", [#97255](https://github.com/vercel/next.js/pull/97255)) changes how Next.js constructs its core AsyncLocalStorage instances.
Instead of each `*-async-storage-instance.ts` calling `createAsyncLocalStorage()` at module scope, they now call a new `getOrCreateGlobalAsyncLocalStorage(name)` helper that stores the instance on `globalThis` under a version-keyed global symbol:
```ts
export function getOrCreateGlobalAsyncLocalStorage(
name: string
): AsyncLocalStorage {
const key = Symbol.for(`@next/${name}@${process.env.__NEXT_VERSION}`)
const globalStore = globalThis as typeof globalThis & {
[key: symbol]: AsyncLocalStorage | undefined
}
return (globalStore[key] ??= createAsyncLocalStorage())
}
```
Applied to: `work-async-storage`, `work-unit-async-storage`, `action-async-storage`, `after-task-async-storage`, `console-async-storage`, `dynamic-access-async-storage`, and `request-insights-identity-storage`.
## Why it matters
These storages must be **singletons within a realm**. A store entered through one reference to a storage must be readable through every other reference to it; otherwise code running inside the scope sees no store at all. Module identity does not guarantee this — a realm can evaluate the same file more than once when it is reachable through more than one path, and each evaluation creates its own storage. A global symbol keeps the singleton intact across any number of duplicate module copies. Worker threads / edge sandboxes still get separate storages because each has its own `globalThis`.
Upstream notes this concretely broke in `next dev` due to a Node `fs.realpathSync` bug ([nodejs/node#65113](https://github.com/nodejs/node/pull/65113)) that returns symlink-unresolved paths on pnpm installs, causing `next/dist/...` files to be evaluated twice.
## Relevance to vinext
This is directly relevant to vinext's architecture. Per `AGENTS.md` ("RSC and SSR Are Separate Vite Environments"), the RSC and SSR environments are **separate Vite module graphs with separate module instances**. Any per-request state built on AsyncLocalStorage is at risk of the exact failure mode this upstream change guards against: duplicate module evaluation yielding multiple ALS instances, so a store set through one reference is invisible through another, and request-scoped context (headers, cookies, params, work store) silently reads as empty.
vinext should adopt the same robustness pattern — anchoring its async storage singletons to version-keyed global symbols — so its request-scoped context survives:
- separate RSC/SSR Vite environments,
- pnpm symlink / `realpathSync` duplicate-evaluation cases,
- multiple copies of the shim modules reachable via different resolution paths.
## Suggested work
- Audit vinext's AsyncLocalStorage usage (shims for `next/navigation`, work/work-unit storage, request context setters passed across the RSC→SSR boundary).
- Introduce a `getOrCreateGlobalAsyncLocalStorage(name)` equivalent that keys on `globalThis` with a version-scoped `Symbol.for(...)`, and route instance creation through it.
- Add a test that simulates duplicate module evaluation (two module instances) and asserts the store is shared.
## Reference
- Upstream commit: https://github.com/vercel/next.js/commit/5817bd1def537b43d5cd20de9cd08537a33d8f35
- Upstream PR: https://github.com/vercel/next.js/pull/97255
- Related Node bug: https://github.com/nodejs/node/pull/65113
Contributor guide
Research direction
Start by auditing vinext's AsyncLocalStorage shims for next/navigation, work/work-unit storage, and the request context setters passed across the RSC→SSR boundary. Compare their instance creation with the referenced Next.js change, then add a test simulating duplicate module evaluation and verify that a store entered through one module instance is readable through the other.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, web-dev
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100