cloudflare / cloudflare/vinext

Anchor async local storage instances to global symbols to survive duplicate module evaluation

Open
#2,951 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 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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.