{"version":3,"file":"request-context.mjs","names":[],"sources":["../src/request-context.ts"],"sourcesContent":["/**\n * EmDash Request Context\n *\n * Uses AsyncLocalStorage to provide request-scoped state to query functions\n * without requiring explicit parameter passing. The middleware wraps next()\n * in als.run(), making the context available to all code during rendering.\n *\n * Middleware always wraps each request in a context so per-request\n * metrics (db.*, cache.*) can be surfaced via Server-Timing. The cost is\n * one ALS frame per request — sub-microsecond, negligible compared to\n * any real work.\n *\n * The AsyncLocalStorage instance is stored on globalThis with a Symbol key\n * to guarantee a singleton even when bundlers duplicate this module across\n * code-split chunks. Without this, Rollup/Vite may inline the module into\n * multiple chunks (e.g. middleware and page components), each with its own\n * ALS instance — breaking request-scoped state propagation.\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport type { QueryRecorder } from \"./database/instrumentation.js\";\nimport type { DeferredTaskTracker } from \"./deferred-tasks.js\";\n\n/**\n * Lightweight always-on counters surfaced in Server-Timing.\n *\n * Bumped by the Kysely log hook (db queries) and by `requestCached`\n * (cache hits/misses). Read by middleware after the response is\n * generated to emit `db.*` and `cache.*` Server-Timing fields.\n *\n * Offsets are milliseconds from `start` (the request's entry into\n * middleware), captured via `performance.now()`.\n */\nexport interface RequestMetrics {\n\tstart: number;\n\tdbCount: number;\n\tdbTotalMs: number;\n\tdbFirstOffset: number | null;\n\tdbLastOffset: number | null;\n\tcacheHits: number;\n\tcacheMisses: number;\n\t/**\n\t * Physical database round trips. Differs from `dbCount` (logical queries)\n\t * when a backend batches: the DO SQL driver coalesces same-turn SELECTs into\n\t * one RPC, so `rpcCount` can be far lower than `dbCount`. Bumped by the\n\t * adapter, not the Kysely log hook.\n\t */\n\trpcCount: number;\n}\n\nexport function createRequestMetrics(start: number): RequestMetrics {\n\treturn {\n\t\tstart,\n\t\tdbCount: 0,\n\t\tdbTotalMs: 0,\n\t\tdbFirstOffset: null,\n\t\tdbLastOffset: null,\n\t\tcacheHits: 0,\n\t\tcacheMisses: 0,\n\t\trpcCount: 0,\n\t};\n}\n\nexport interface EmDashRequestContext {\n\t/** Whether the current request is in visual editing mode */\n\teditMode: boolean;\n\t/** Preview token info, if this is a preview request */\n\tpreview?: {\n\t\tcollection: string;\n\t\tid: string;\n\t};\n\t/** Current locale from Astro's i18n routing (when configured) */\n\tlocale?: string;\n\t/**\n\t * Per-request database override.\n\t *\n\t * Set by middleware when D1 read replica sessions are enabled.\n\t * The runtime's `db` getter checks this first, falling back to\n\t * the singleton instance. Also used by the DO preview pattern.\n\t */\n\tdb?: unknown;\n\t/**\n\t * Indicates the per-request `db` points at an isolated database\n\t * instance whose schema may diverge from the configured one\n\t * (playground, DO preview sessions). When true, schema-derived caches\n\t * (manifest, taxonomy defs, etc.) must not be reused across requests.\n\t *\n\t * Plain D1 Sessions API routing does NOT set this — sessions are just\n\t * a routing hint over the same schema, so the module-scoped manifest\n\t * cache remains valid.\n\t */\n\tdbIsIsolated?: boolean;\n\t/**\n\t * Query recorder attached by middleware when EMDASH_QUERY_LOG_FILE is set.\n\t * The Kysely `log` hook appends an event per query; middleware flushes\n\t * to NDJSON after the response.\n\t */\n\tqueryRecorder?: QueryRecorder;\n\t/**\n\t * Per-request metrics for Server-Timing. Always attached by middleware\n\t * for requests that emit timing headers; bumped by the Kysely log hook\n\t * and `requestCached`.\n\t */\n\tmetrics?: RequestMetrics;\n\t/** Deferred work that owns resources scoped to this request. */\n\tdeferredTasks?: DeferredTaskTracker;\n}\n\nconst ALS_KEY = Symbol.for(\"emdash:request-context\");\n\nconst storage: AsyncLocalStorage<EmDashRequestContext> =\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern\n\t((globalThis as Record<symbol, unknown>)[ALS_KEY] as\n\t\t| AsyncLocalStorage<EmDashRequestContext>\n\t\t| undefined) ??\n\t(() => {\n\t\tconst als = new AsyncLocalStorage<EmDashRequestContext>();\n\t\t(globalThis as Record<symbol, unknown>)[ALS_KEY] = als;\n\t\treturn als;\n\t})();\n\n/**\n * Run a function within an EmDash request context.\n * Called by middleware to wrap next().\n */\nexport function runWithContext<T>(ctx: EmDashRequestContext, fn: () => T): T {\n\treturn storage.run(ctx, fn);\n}\n\n/**\n * Get the current request context.\n * Returns undefined if no context is set (logged-out fast path).\n */\nexport function getRequestContext(): EmDashRequestContext | undefined {\n\treturn storage.getStore();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,qBAAqB,OAA+B;AACnE,QAAO;EACN;EACA,SAAS;EACT,WAAW;EACX,eAAe;EACf,cAAc;EACd,WAAW;EACX,aAAa;EACb,UAAU;EACV;;AAgDF,MAAM,UAAU,OAAO,IAAI,yBAAyB;AAEpD,MAAM,UAEH,WAAuC,mBAGlC;CACN,MAAM,MAAM,IAAI,mBAAyC;AACzD,CAAC,WAAuC,WAAW;AACnD,QAAO;IACJ;;;;;AAML,SAAgB,eAAkB,KAA2B,IAAgB;AAC5E,QAAO,QAAQ,IAAI,KAAK,GAAG;;;;;;AAO5B,SAAgB,oBAAsD;AACrE,QAAO,QAAQ,UAAU"}