Skip to content

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

Description

@github-actions

Upstream change

Next.js commit 5817bd1 ("Anchor the async local storage instances to global symbols", #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:

export function getOrCreateGlobalAsyncLocalStorage<Store extends {}>(
  name: string
): AsyncLocalStorage<Store> {
  const key = Symbol.for(`@next/${name}@${process.env.__NEXT_VERSION}`)
  const globalStore = globalThis as typeof globalThis & {
    [key: symbol]: AsyncLocalStorage<Store> | undefined
  }
  return (globalStore[key] ??= createAsyncLocalStorage<Store>())
}

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) 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    nextjs-trackingTracking issue for a Next.js canary change relevant to vinext

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions