140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
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<string, unknown>;
|
|
authorize(permission: string): void | Promise<void>;
|
|
}
|
|
export interface ExecutionContextInput extends Partial<
|
|
Omit<
|
|
ExecutionContext,
|
|
"kind" | "id" | "request" | "response" | "signal" | "deadline" | "metadata" | "authorize"
|
|
>
|
|
> {
|
|
kind: ExecutionKind;
|
|
id?: string;
|
|
request?: Request;
|
|
response?: Partial<Pick<ResponseContext, "status">> & { headers?: HeadersInit };
|
|
signal?: AbortSignal;
|
|
deadline?: Date | number | null;
|
|
timeoutMs?: number;
|
|
metadata?: Record<string, unknown>;
|
|
authorize?: (permission: string) => void | Promise<void>;
|
|
}
|
|
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<typeof setTimeout> | 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 ?? {}) },
|
|
});
|
|
}
|