UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

139 lines (127 loc) 4.42 kB
import { AsyncLocalStorage } from "node:async_hooks"; import type { ActivityActor, ActivityTenant } from "../ports/audit.js"; /** * Ambient correlation values for the request currently being handled. * * The server enters this context before user hooks run so instrumentation * sinks can correlate events recorded anywhere in the request lifecycle. * Identity values (`actor`, `tenant`) are refreshed by the server whenever * hooks finalize a new context, so wrappers such as `createAmbientAuditLog` * observe the elevated identity at record time. */ export interface ActiveRequestContext { requestId?: string; traceId?: string; spanId?: string; parentSpanId?: string; traceparent?: string; tracestate?: string; /** * Actor for the current request or service execution, when known. */ actor?: ActivityActor; /** * Tenant scope for the current request or service execution, when known. */ tenant?: ActivityTenant; } const activeRequestContext = new AsyncLocalStorage< ActiveRequestContext | undefined >(); /** * Enter the ambient request context for the current async execution. */ export function enterActiveRequestContext(context: ActiveRequestContext): void { activeRequestContext.enterWith(context); } /** * Clear the ambient request context for the current async execution. */ export function clearActiveRequestContext(): void { activeRequestContext.enterWith(undefined); } /** * Run a function inside a scoped ambient request context frame. * * Internal to the server runtime — not part of the public package surface. * `server.runServiceContext(...)` uses this `AsyncLocalStorage.run` form * instead of `enterWith` because resuming an `enterWith` frame across * top-level await crashes Bun 1.3.x in plain scripts. */ export function runWithActiveRequestContext<T>( context: ActiveRequestContext, fn: () => T, ): T { return activeRequestContext.run(context, fn); } /** * Read the ambient request context, when one is active. */ export function getActiveRequestContext(): ActiveRequestContext | undefined { return activeRequestContext.getStore(); } /** * Read a normalized actor from an app context object, when present. */ export function readContextActor(ctx: unknown): ActivityActor | undefined { if (!ctx || typeof ctx !== "object") return undefined; const actor = (ctx as { actor?: unknown }).actor; if (!actor || typeof actor !== "object") return undefined; return typeof (actor as { type?: unknown }).type === "string" ? (actor as ActivityActor) : undefined; } /** * Read a normalized tenant from an app context object, when present. */ export function readContextTenant(ctx: unknown): ActivityTenant | undefined { if (!ctx || typeof ctx !== "object") return undefined; const tenant = (ctx as { tenant?: unknown }).tenant; if (!tenant || typeof tenant !== "object") return undefined; return typeof (tenant as { id?: unknown }).id === "string" ? (tenant as ActivityTenant) : undefined; } /** * Update identity fields on the active ambient request context in place. * * The server calls this after hooks finalize a new request context so the * elevated actor/tenant become visible to ambient consumers, including async * frames that captured the context object before the update. No-op when no * ambient context is active. */ export function setActiveRequestIdentity(identity: { actor?: ActivityActor; tenant?: ActivityTenant; }): void { const context = activeRequestContext.getStore(); if (!context) return; if (identity.actor) context.actor = identity.actor; if (identity.tenant) context.tenant = identity.tenant; } /** * Fill missing correlation fields on an event from the ambient request * context. */ export function inheritActiveRequestContext< Event extends { requestId?: string; traceId?: string; spanId?: string; parentSpanId?: string; traceparent?: string; tracestate?: string; }, >(event: Event): Event { const context = getActiveRequestContext(); if (!context) return event; return { ...event, requestId: event.requestId ?? context.requestId, traceId: event.traceId ?? context.traceId, spanId: event.spanId ?? context.spanId, parentSpanId: event.parentSpanId ?? context.parentSpanId, traceparent: event.traceparent ?? context.traceparent, tracestate: event.tracestate ?? context.tracestate, }; }