@beignet/core
Version:
Core framework primitives for Beignet
139 lines (127 loc) • 4.42 kB
text/typescript
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,
};
}