Files
cloudflare__vinext/tests/request-context.test.ts
T
Divanshu Chauhan (divkix) e6439ffce8 refactor: phase 1/2 unify per-request ALS into a shared request context (#450)
* refactor: consolidate 5 nested ALS scopes into unified request context

Every App Router request previously ran through 5-6 nested
AsyncLocalStorage.run() calls (headers, navigation, cache-state,
private-cache, fetch-cache, plus optional execution-context). Each
ALS scope push/pop has measurable cost on Workers isolates.

Introduce a single UnifiedRequestContext that holds all per-request
state in one flat object, backed by one ALS instance. Each shim module
checks isInsideUnifiedScope() first and reads its sub-fields from the
unified store, falling back to its own standalone ALS when outside
(SSR environment, Pages Router, tests).

- Create unified-request-context.ts with createRequestContext(),
  runWithRequestContext(), getRequestContext(), isInsideUnifiedScope()
- Rewire _getState() in headers, navigation-state, cache, cache-runtime,
  fetch-cache, and request-context to dual-path (unified first, own ALS
  fallback)
- Make each runWith*() a no-op inside unified scope
- Replace 5-deep nesting in app-rsc-entry.ts (main handler + ISR regen)
  with single _runWithUnifiedCtx() call
- Export ensureFetchPatch() from fetch-cache for standalone patch install
- Add 17 unit tests for unified context (isolation, nesting, concurrency)

* test: update entry-templates snapshots for unified ALS context

Snapshots were stale after the unified request context refactor changed
the generated entry code (new imports, removed 5-deep nesting).

* test: update app router waitUntil assertion

* refactor: unify request ALS across router flows

* fix: address unified request context review feedback

* fix: address unified ALS review feedback

* fix: address remaining unified ALS review feedback

* fix: wrap Pages Router ISR regeneration in unified context

Ensures patched fetch and ALS-based context are available during
background regeneration, matching App Router behavior.

- Creates fresh unified context with execution context for regen
- Calls ensureFetchPatch() to enable cache tagging
- Adds test verifying _runWithUnifiedCtx and ensureFetchPatch presence
- Updates snapshot

Part of Phase 1.5: minimal Pages Router fix for ISR parity.

* Fix dev ISR unified context parity

* fix: address unified ALS review followups

* Fix pages router unified execution context seeding

* Fix Pages Router ISR rerender state isolation

* fix: address unified ALS review feedback
2026-03-13 07:19:13 +00:00

133 lines
3.9 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
runWithExecutionContext,
getRequestExecutionContext,
type ExecutionContextLike,
} from "../packages/vinext/src/shims/request-context.js";
import {
createRequestContext,
runWithRequestContext,
} from "../packages/vinext/src/shims/unified-request-context.js";
function makeCtx(): ExecutionContextLike & { calls: Promise<unknown>[] } {
const calls: Promise<unknown>[] = [];
return {
calls,
waitUntil(p: Promise<unknown>) {
calls.push(p);
},
};
}
describe("getRequestExecutionContext", () => {
it("returns null outside a runWithExecutionContext scope", () => {
// Ensure we're not accidentally inside a scope from a previous test
expect(getRequestExecutionContext()).toBeNull();
});
});
describe("runWithExecutionContext", () => {
it("makes the context available inside the scope", () => {
const ctx = makeCtx();
runWithExecutionContext(ctx, () => {
expect(getRequestExecutionContext()).toBe(ctx);
});
});
it("returns null outside the scope after it exits", async () => {
const ctx = makeCtx();
await runWithExecutionContext(ctx, async () => {
expect(getRequestExecutionContext()).toBe(ctx);
});
expect(getRequestExecutionContext()).toBeNull();
});
it("propagates through async continuations (Promise chains)", async () => {
const ctx = makeCtx();
let captured: ExecutionContextLike | null = null;
await runWithExecutionContext(ctx, async () => {
await new Promise<void>((resolve) => setTimeout(resolve, 0));
captured = getRequestExecutionContext();
});
expect(captured).toBe(ctx);
});
it("isolates concurrent requests — each sees its own ctx", async () => {
const ctxA = makeCtx();
const ctxB = makeCtx();
const results: Array<{ id: string; ctx: ExecutionContextLike | null }> = [];
const runA = runWithExecutionContext(ctxA, async () => {
await new Promise<void>((resolve) => setTimeout(resolve, 5));
results.push({ id: "A", ctx: getRequestExecutionContext() });
});
const runB = runWithExecutionContext(ctxB, async () => {
await new Promise<void>((resolve) => setTimeout(resolve, 1));
results.push({ id: "B", ctx: getRequestExecutionContext() });
});
await Promise.all([runA, runB]);
const a = results.find((r) => r.id === "A");
const b = results.find((r) => r.id === "B");
expect(a?.ctx).toBe(ctxA);
expect(b?.ctx).toBe(ctxB);
});
it("returns the value from fn", async () => {
const ctx = makeCtx();
const result = await runWithExecutionContext(ctx, async () => 42);
expect(result).toBe(42);
});
it("ctx.waitUntil can be called from inside the scope", async () => {
const ctx = makeCtx();
const p = Promise.resolve("done");
runWithExecutionContext(ctx, () => {
const c = getRequestExecutionContext();
c?.waitUntil(p);
});
expect(ctx.calls).toContain(p);
});
it("nested runWithExecutionContext overrides the outer ctx", () => {
const outerCtx = makeCtx();
const innerCtx = makeCtx();
runWithExecutionContext(outerCtx, () => {
expect(getRequestExecutionContext()).toBe(outerCtx);
runWithExecutionContext(innerCtx, () => {
expect(getRequestExecutionContext()).toBe(innerCtx);
});
// Outer scope is restored after inner exits
expect(getRequestExecutionContext()).toBe(outerCtx);
});
});
it("restores the outer ctx when nested inside a unified request scope", () => {
const outerCtx = makeCtx();
const innerCtx = makeCtx();
runWithExecutionContext(outerCtx, () => {
runWithRequestContext(createRequestContext(), () => {
expect(getRequestExecutionContext()).toBe(outerCtx);
runWithExecutionContext(innerCtx, () => {
expect(getRequestExecutionContext()).toBe(innerCtx);
});
expect(getRequestExecutionContext()).toBe(outerCtx);
});
});
});
});