Skip to content

refactor(als): dedupe AsyncLocalStorage globalThis registration#1081

Merged
james-elicx merged 1 commit intomainfrom
refactor/dedupe-als-registration
May 5, 2026
Merged

refactor(als): dedupe AsyncLocalStorage globalThis registration#1081
james-elicx merged 1 commit intomainfrom
refactor/dedupe-als-registration

Conversation

@james-elicx
Copy link
Copy Markdown
Collaborator

Summary

Extracts the repeated Symbol.for(...) + globalThis ??= new AsyncLocalStorage<T>()
pattern into a single helper, getOrCreateAls<T>(key: string), in
packages/vinext/src/shims/internal/als-registry.ts.

The pattern was inlined in 12 sites across 10 modules:

  • shims/headers.tsvinext.nextHeadersShim.als
  • shims/cache.tsvinext.cache.als, vinext.unstableCache.als
  • shims/cache-runtime.tsvinext.cacheRuntime.contextAls (exported), vinext.cacheRuntime.privateAls
  • shims/fetch-cache.tsvinext.fetchCache.als
  • shims/request-context.tsvinext.requestContext.als
  • shims/unified-request-context.tsvinext.unifiedRequestContext.als
  • shims/router-state.tsvinext.router.als
  • shims/i18n-state.tsvinext.i18n.als
  • shims/head-state.tsvinext.head.als
  • shims/navigation-state.tsvinext.navigation.als

The 12 sites used identical structure: same Symbol.for(key) registry-side
lookup, same ??= semantics, same fallback-type AsyncLocalStorage<T>().
No site initialised the ALS with a sentinel store value or wrapped it in a
container object — they were all clean candidates.

Net: 11 files changed, +77/-46 (the new helper module is the bulk of
+77; call sites collapse from 4 lines to 1).

Cross-bundle singleton property — preserved

This is the load-bearing invariant for ALS bugs and is the reason the
inline pattern existed in the first place. The helper preserves it:

  • Symbol.for(key) consults the global symbol registry and returns the
    same symbol no matter which module instance calls it.
  • globalThis[sym] is one slot shared by every module instance.
  • ??= only assigns when the slot is empty, so the first caller wins and
    every later caller (in any module instance, in any Vite environment)
    reads the same AsyncLocalStorage instance.

The helper module itself never closes over the ALS — it always
round-trips through globalThis. So even if als-registry.ts is loaded
under multiple specifiers (Vite RSC/SSR/client environments, HMR), every
copy hands back the one true ALS for a given key. There is no new
"ownership-by-reference" layer introduced by the helper.

The unrelated _FALLBACK_KEY / _fallbackState pattern in some shims
(plain-object cross-bundle singletons with shape varying per call site)
is intentionally left inline.

Files changed

  • packages/vinext/src/shims/internal/als-registry.ts (new)
  • packages/vinext/src/shims/headers.ts
  • packages/vinext/src/shims/cache.ts
  • packages/vinext/src/shims/cache-runtime.ts
  • packages/vinext/src/shims/fetch-cache.ts
  • packages/vinext/src/shims/request-context.ts
  • packages/vinext/src/shims/unified-request-context.ts
  • packages/vinext/src/shims/router-state.ts
  • packages/vinext/src/shims/i18n-state.ts
  • packages/vinext/src/shims/head-state.ts
  • packages/vinext/src/shims/navigation-state.ts

Follow-up to #1058.

Test plan

  • pnpm fmt --write on touched files
  • npx tsc --noEmit in packages/vinext — clean
  • pnpm knip — clean
  • pnpm vp test run for app-router, pages-router, cache-control, cache-for-request, fetch-cache, head, request-context, unified-request-context, pages-router-concurrency — 683 tests passing. The single suite-level failure (pages-router.test.ts > allowedDevOrigins config afterAll timeout) reproduces on clean main with the changes stashed; it is a pre-existing flaky teardown unrelated to this refactor.
  • CI runs full suite

🤖 Generated with Claude Code

Extract the `Symbol.for(...) + globalThis ??= new AsyncLocalStorage()`
pattern from headers.ts, cache.ts, cache-runtime.ts, fetch-cache.ts,
request-context.ts, unified-request-context.ts, and the four *-state.ts
shims into a single helper at shims/internal/als-registry.ts.

Cross-bundle singleton property is preserved: the helper still routes
through `Symbol.for(key)` (global symbol registry) and `globalThis[sym]
??= ...`, so multiple instances of the helper module — and multiple
instances of each call site — all converge on the same ALS for a given
key. The helper holds no module-local reference of its own.

Follow-up to #1058.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 5, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1081

commit: 4d0d56e

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original

@james-elicx
Copy link
Copy Markdown
Collaborator Author

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean dedup. Reviewed all 11 files — the helper faithfully preserves the cross-bundle singleton invariant:

  1. Correctness: getOrCreateAls<T>(key) reproduces the exact Symbol.for(key) + globalThis[sym] ??= new AsyncLocalStorage<T>() semantics. The function never closes over the returned ALS — it always round-trips through globalThis on every call. Multiple module instances of als-registry.ts itself still hand back the same ALS for a given key. No behavioral change.

  2. All 12 sites converted: Verified each one in the diff:

    • headers.tsvinext.nextHeadersShim.als
    • cache.tsvinext.cache.als, vinext.unstableCache.als
    • cache-runtime.tsvinext.cacheRuntime.contextAls, vinext.cacheRuntime.privateAls
    • fetch-cache.tsvinext.fetchCache.als
    • request-context.tsvinext.requestContext.als
    • unified-request-context.tsvinext.unifiedRequestContext.als
    • router-state.tsvinext.router.als
    • i18n-state.tsvinext.i18n.als
    • head-state.tsvinext.head.als
    • navigation-state.tsvinext.navigation.als
  3. Type-only import preserved where needed: headers.ts and unified-request-context.ts correctly switched their AsyncLocalStorage import to import type since they only need the type for the cache-scope detection casts (as AsyncLocalStorage<unknown> | undefined), not a runtime value. The runtime construction is now fully delegated to the helper.

  4. Exported ALS preserved: cache-runtime.ts's export const cacheContextStorage still works — getOrCreateAls returns the ALS directly and the export just binds it. The cross-module Symbol.for("vinext.cacheRuntime.contextAls") lookup in headers.ts (lines 188-196) still works because the symbol key is unchanged.

  5. Fallback patterns left inline: Correct call — the _FALLBACK_KEY / _fallbackState patterns have per-module shapes (HeadState, RouterState, CacheState, etc.) that don't share structure, so they aren't candidates for the same dedup treatment.

  6. Module location: shims/internal/als-registry.ts is the right spot — internal/ signals this isn't part of the public shim surface, and it sits alongside the other internal helpers (cookie-serialize.ts, parse-cookie-header.ts, etc.).

CI is fully green across all checks including all Vitest shards, all 11 Playwright E2E projects, Check, CodeQL, smoke tests, and all 7 deploy previews.

Ship it.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 5, 2026

Review posted. Here's the summary:

Approved PR #1081 — clean dedup refactor that extracts the repeated Symbol.for(...) + globalThis ??= new AsyncLocalStorage<T>() pattern into getOrCreateAls<T>(key) in shims/internal/als-registry.ts.

Key findings:

  • All 12 inline sites across 10 modules correctly converted
  • The cross-bundle singleton invariant is preserved — the helper never closes over the ALS, always round-trips through globalThis
  • Type-only imports correctly used where the runtime AsyncLocalStorage constructor is no longer needed (headers.ts, unified-request-context.ts)
  • The exported cacheContextStorage in cache-runtime.ts still works correctly
  • Fallback singleton patterns (_FALLBACK_KEY / _fallbackState) correctly left inline since they have per-module shapes
  • All CI checks green (Vitest, Playwright E2E, Check, deploys, smoke tests)

github run

@james-elicx james-elicx marked this pull request as ready for review May 5, 2026 18:53
@james-elicx james-elicx merged commit 604d548 into main May 5, 2026
33 checks passed
@james-elicx james-elicx deleted the refactor/dedupe-als-registration branch May 5, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant