feat(core): carry the request context in an AsyncLocalStorage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,11 @@ export type {
|
||||
TFunction,
|
||||
} from "./context.ts";
|
||||
export { createContext, withContextHeaders } from "./context.ts";
|
||||
export {
|
||||
runWithRequestContext,
|
||||
getRequestContext,
|
||||
requireRequestContext,
|
||||
} from "./request-context.ts";
|
||||
export { createExecutionContext, executionContextFromHttp } from "./execution-context.ts";
|
||||
export type {
|
||||
ExecutionContext,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import type { Context } from "./context.ts";
|
||||
|
||||
/**
|
||||
* The request context for the currently executing server work.
|
||||
*
|
||||
* A server-side API call needs the request's cookies, session, and URL, but
|
||||
* `ctx` is not in scope everywhere server code runs: load blocks have it,
|
||||
* schema actions take it as a parameter, and plain actions and server
|
||||
* functions have neither. Threading it through every signature would make the
|
||||
* call site differ between server and browser, which defeats the point.
|
||||
*/
|
||||
const storage = new AsyncLocalStorage<Context>();
|
||||
|
||||
export function runWithRequestContext<T>(ctx: Context, fn: () => T): T {
|
||||
return storage.run(ctx, fn);
|
||||
}
|
||||
|
||||
export function getRequestContext(): Context | undefined {
|
||||
return storage.getStore();
|
||||
}
|
||||
|
||||
export function requireRequestContext(what: string): Context {
|
||||
const ctx = storage.getStore();
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
`${what} needs a request context. It ran outside a request — server-side API calls are only available while handling one.`,
|
||||
);
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import {
|
||||
getRequestContext,
|
||||
requireRequestContext,
|
||||
runWithRequestContext,
|
||||
} from "../src/request-context.ts";
|
||||
|
||||
const ctx = { marker: "the-request" } as never;
|
||||
|
||||
test("the context is visible inside the run", () => {
|
||||
runWithRequestContext(ctx, () => {
|
||||
expect(getRequestContext()).toBe(ctx);
|
||||
});
|
||||
});
|
||||
|
||||
test("the context is visible across an await", async () => {
|
||||
// The whole point is that it survives async boundaries a caller cannot see.
|
||||
await runWithRequestContext(ctx, async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
expect(getRequestContext()).toBe(ctx);
|
||||
});
|
||||
});
|
||||
|
||||
test("there is no context outside a run", () => {
|
||||
expect(getRequestContext()).toBeUndefined();
|
||||
});
|
||||
|
||||
test("requireRequestContext throws a message naming the caller", () => {
|
||||
expect(() => requireRequestContext("api.searchUsers")).toThrow(/api\.searchUsers/);
|
||||
});
|
||||
|
||||
test("concurrent runs do not see each other's context", async () => {
|
||||
const first = { id: 1 } as never;
|
||||
const second = { id: 2 } as never;
|
||||
const seen: unknown[] = [];
|
||||
|
||||
await Promise.all([
|
||||
runWithRequestContext(first, async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
seen.push(getRequestContext());
|
||||
}),
|
||||
runWithRequestContext(second, async () => {
|
||||
seen.push(getRequestContext());
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(seen).toContain(first);
|
||||
expect(seen).toContain(second);
|
||||
});
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
isSafeRequestPath,
|
||||
renderError,
|
||||
renderNotFound,
|
||||
runWithRequestContext,
|
||||
withContextHeaders,
|
||||
withSecurityHeaders,
|
||||
resolveRequestUrl,
|
||||
@@ -1266,8 +1267,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
return secure(new Response("Expected a WebSocket upgrade request", { status: 426 }));
|
||||
}
|
||||
|
||||
try {
|
||||
const ctx = createContext(req, url);
|
||||
return runWithRequestContext(ctx, async () => {
|
||||
try {
|
||||
initializeRequestCache(ctx);
|
||||
ctx.ip = server.requestIP?.(req)?.address ?? undefined;
|
||||
ctx.locals.cspNonce = nonce; // available to pages for their own inline scripts
|
||||
@@ -1331,6 +1333,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
response.headers.set("x-wrnexus-internal-error", encodeURIComponent(message.slice(0, 500)));
|
||||
return compressResponse(req, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function dispatch(ctx: Context): Promise<Response> {
|
||||
|
||||
Reference in New Issue
Block a user