import type { Context } from "./context.ts"; import type { Tenant } from "./tenant.ts"; import type { Tracer } from "./observability.ts"; export type ExecutionKind = "http" | "api" | "action" | "loader" | "middleware" | "realtime" | "queue" | "cron" | "webhook"; export interface ResponseContext { status: number; headers: Headers; setStatus(status: number): void; } export interface ExecutionContext { kind: ExecutionKind; id: string; request: Request; response: ResponseContext; user: unknown | null; session: unknown | null; tenant: Tenant | null; locale: string; timezone: string; db?: unknown; cache?: unknown; logger?: unknown; trace?: Tracer; signal: AbortSignal; deadline: Date | null; metadata: Record; authorize(permission: string): void | Promise; } export interface ExecutionContextInput extends Partial< Omit< ExecutionContext, "kind" | "id" | "request" | "response" | "signal" | "deadline" | "metadata" | "authorize" > > { kind: ExecutionKind; id?: string; request?: Request; response?: Partial> & { headers?: HeadersInit }; signal?: AbortSignal; deadline?: Date | number | null; timeoutMs?: number; metadata?: Record; authorize?: (permission: string) => void | Promise; } export function createExecutionContext(input: ExecutionContextInput): ExecutionContext { const controller = new AbortController(); const source = input.signal; if (source?.aborted) controller.abort(source.reason); else source?.addEventListener("abort", () => controller.abort(source.reason), { once: true }); const deadline = input.deadline instanceof Date ? input.deadline : typeof input.deadline === "number" ? new Date(input.deadline) : input.timeoutMs !== undefined ? new Date(Date.now() + input.timeoutMs) : null; let timer: ReturnType | undefined; if (deadline) { const delay = deadline.getTime() - Date.now(); if (delay <= 0) controller.abort(new DOMException("Execution deadline exceeded", "TimeoutError")); else { timer = setTimeout( () => controller.abort(new DOMException("Execution deadline exceeded", "TimeoutError")), delay, ); timer.unref?.(); } } controller.signal.addEventListener( "abort", () => { if (timer) clearTimeout(timer); }, { once: true }, ); const response: ResponseContext = { status: input.response?.status ?? 200, headers: new Headers(input.response?.headers), setStatus(status) { if (!Number.isInteger(status) || status < 100 || status > 599) throw new RangeError("response status must be an HTTP status code"); this.status = status; }, }; return { kind: input.kind, id: input.id ?? crypto.randomUUID(), request: input.request ?? new Request(`https://execution.wrnexus.invalid/${input.kind}`), response, user: input.user ?? null, session: input.session ?? null, tenant: input.tenant ?? null, locale: input.locale ?? "en", timezone: input.timezone ?? "UTC", db: input.db, cache: input.cache, logger: input.logger, trace: input.trace, signal: controller.signal, deadline, metadata: { ...(input.metadata ?? {}) }, authorize: input.authorize ?? (() => { throw new Error("WRN-AUTHORIZATION-NOT-CONFIGURED"); }), }; } export function executionContextFromHttp( context: Context, kind: Extract< ExecutionKind, "http" | "api" | "action" | "loader" | "middleware" | "webhook" > = "http", input: Omit< ExecutionContextInput, "kind" | "request" | "user" | "tenant" | "locale" | "trace" > = {}, ): ExecutionContext { return createExecutionContext({ ...input, kind, request: context.req, user: context.user ?? null, session: context.session, tenant: context.tenant ?? null, locale: context.lang || "en", trace: context.tracer, signal: input.signal ?? context.req.signal, db: input.db ?? context.locals.db, cache: input.cache ?? context.locals.cache, logger: input.logger ?? context.locals.logger, metadata: { ...context.locals, ...(input.metadata ?? {}) }, }); }