Files
cloudflare__vinext/tests/als-registry.test.ts
T
Xplod13 201f4a64dc fix(shims): guard AsyncLocalStorage construction for browser bundles (#2020)
## Summary
- Guard `getOrCreateAls` so it no longer calls `new AsyncLocalStorage()`
  unconditionally; when the runtime does not expose a usable constructor it
  falls back to a no-op store instead of throwing.
- Add a `NoopAsyncLocalStorage` whose `getStore()` returns `undefined`, mirroring
  Next.js' `FakeAsyncLocalStorage` and this repo's `async-hooks-stub.ts`.
- Preserve the cross-bundle singleton behavior (`Symbol.for` + `??=`).

## Root Cause
In browser/client bundles `node:async_hooks` can resolve to a stub without a
usable `AsyncLocalStorage` constructor (e.g. Vite's `__vite-browser-external`).
`getOrCreateAls` ran `new AsyncLocalStorage()` at module-eval time, throwing
`TypeError: AsyncLocalStorage is not a constructor`. Because many client-reachable
shims (request-context, headers, cache, …) call this helper on import, the whole
client bundle died and hydration crashed. Checking
`typeof AsyncLocalStorage === "function"` before constructing, with a no-op
fallback, matches Next.js' availability guard.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:31:38 +01:00

37 lines
1.6 KiB
TypeScript

import { describe, expect, it, vi } from "vite-plus/test";
// Simulate a browser/client bundle where `node:async_hooks` resolves to a stub
// that does NOT expose a usable `AsyncLocalStorage` constructor (e.g. Vite's
// `__vite-browser-external`). On such a runtime `new AsyncLocalStorage()` would
// throw `TypeError: AsyncLocalStorage is not a constructor` at module-eval time,
// crashing every client-reachable shim that calls `getOrCreateAls` on import.
vi.mock("node:async_hooks", () => ({
AsyncLocalStorage: undefined,
}));
describe("getOrCreateAls — browser-safe fallback", () => {
it("does not throw when AsyncLocalStorage is unavailable and returns a no-op store", async () => {
const { getOrCreateAls } =
await import("../packages/vinext/src/shims/internal/als-registry.js");
let als: ReturnType<typeof getOrCreateAls<{ value: number }>> | undefined;
expect(() => {
als = getOrCreateAls<{ value: number }>("vinext.test.als.unavailable");
}).not.toThrow();
// The fallback mirrors Next.js' FakeAsyncLocalStorage: getStore() === undefined
// so shims fall back to their non-ALS code path instead of crashing.
expect(als).toBeDefined();
expect(als!.getStore()).toBeUndefined();
});
it("keeps the global-dedupe behavior: same key returns the same instance", async () => {
const { getOrCreateAls } =
await import("../packages/vinext/src/shims/internal/als-registry.js");
const a = getOrCreateAls("vinext.test.als.dedupe");
const b = getOrCreateAls("vinext.test.als.dedupe");
expect(a).toBe(b);
});
});