UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

316 lines (290 loc) 7.43 kB
import type { StandardSchemaV1 } from "@standard-schema/spec"; import { runWithResolvedTracingContext } from "../tracing/execution.js"; import type { TracingPort } from "../tracing/index.js"; /** * Any Standard Schema compatible validator. */ export type StandardSchema = StandardSchemaV1<unknown, unknown>; /** * Value or promise of that value. */ export type MaybePromise<T> = T | Promise<T>; /** * Infer the parsed output type from a Standard Schema. */ export type InferSchemaOutput<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>; /** * Operational task definition created by `defineTask(...)`. */ export interface TaskDef< Name extends string = string, Input extends StandardSchema = StandardSchema, Ctx = unknown, Output = unknown, > { /** * Discriminator for task definitions. */ readonly kind: "task"; /** * Stable task name used by CLIs and operational runners. */ readonly name: Name; /** * Standard Schema input validator. */ readonly input: Input; /** * Optional human-readable description for docs and tooling. */ readonly description?: string; /** * Handle a parsed task input. */ handle( args: TaskHandleArgs<TaskDef<Name, Input, Ctx, Output>, Ctx>, ): MaybePromise<Output>; } /** * Infer the parsed input type for an operational task. */ export type InferTaskInput<T extends TaskDef> = T["input"] extends StandardSchemaV1<unknown, infer Output> ? Output : never; /** * Infer the result type for an operational task. */ export type InferTaskOutput<T extends TaskDef> = T extends TaskDef<string, StandardSchema, unknown, infer Output> ? Awaited<Output> : never; /** * Arguments passed to a task handler. */ export interface TaskHandleArgs<T extends TaskDef, Ctx> { /** * Task definition being handled. */ task: T; /** * Parsed task input. */ input: InferTaskInput<T>; /** Handler context. */ ctx: Ctx; } /** * Options for `defineTask(...)`. */ export interface DefineTaskOptions< Name extends string, Input extends StandardSchema, Ctx, Output, > { /** * Standard Schema input validator. */ input: Input; /** * Optional human-readable description for docs and tooling. */ description?: string; /** * Handle a parsed task input. */ handle( args: TaskHandleArgs<TaskDef<Name, Input, Ctx, Output>, Ctx>, ): MaybePromise<Output>; } /** * Options for one task run. */ export interface RunTaskOptions<Ctx> { /** * Raw task input. It is parsed with the task's Standard Schema before the * handler runs. */ input: unknown; /** Handler context or factory resolved inside the task span. */ ctx: Ctx | (() => MaybePromise<Ctx>); /** Runtime tracing port used before a lazy context factory runs. */ tracing?: TracingPort; } /** * Arguments `beignet task run` passes to the app's `createTaskContext` and * `stopTaskContext` exports in `server/tasks.ts`. */ export interface TaskRunContextArgs<T extends TaskDef = TaskDef> { /** * Task definition being run. */ task: T; /** * Stable task name being run. */ taskName: string; /** * Schema-parsed task input. */ input: unknown; /** * Tenant id or slug from `--tenant`, resolved by the app. */ tenant?: string; } /** * Context-bound operational task helper factory. */ export interface Tasks<Ctx> { /** * Define a task with the bound context type. */ defineTask< Name extends string, Input extends StandardSchema, Output = unknown, >( name: Name, options: DefineTaskOptions<Name, Input, Ctx, Output>, ): TaskDef<Name, Input, Ctx, Output>; } /** * Error thrown when task input validation fails. */ export class TaskValidationError extends Error { /** * Raw Standard Schema validation issues. */ readonly issues: readonly StandardSchemaV1.Issue[]; constructor(args: { name: string; issues: readonly StandardSchemaV1.Issue[]; }) { super( `Task "${args.name}" input validation failed: ${formatIssues(args.issues)}`, ); this.name = "TaskValidationError"; this.issues = args.issues; } } function formatPath(path: StandardSchemaV1.Issue["path"]): string { if (!path || path.length === 0) return ""; return path .map((segment) => { if (typeof segment === "number") return `[${segment}]`; const key = String(segment); return /^[A-Za-z_$][\w$]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`; }) .join("") .replace(/^\./, ""); } function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string { return issues .map((issue) => { const path = formatPath(issue.path); return path ? `${path}: ${issue.message}` : issue.message; }) .join("; "); } async function parseInput<Schema extends StandardSchemaV1>( schema: Schema, input: unknown, options: { name: string }, ): Promise<InferSchemaOutput<Schema>> { const result = await schema["~standard"].validate(input); if (result.issues?.length) { throw new TaskValidationError({ name: options.name, issues: result.issues, }); } return (result as { value: InferSchemaOutput<Schema> }).value; } function defineTaskImpl< Name extends string, Input extends StandardSchema, Ctx = unknown, Output = unknown, >( name: Name, options: DefineTaskOptions<Name, Input, Ctx, Output>, ): TaskDef<Name, Input, Ctx, Output> { return { kind: "task", name, input: options.input, description: options.description, handle: options.handle as TaskDef<Name, Input, Ctx, Output>["handle"], }; } /** * Validate and parse a task input with the task's Standard Schema. */ export async function parseTaskInput<T extends TaskDef>( task: T, input: unknown, ): Promise<InferTaskInput<T>> { return (await parseInput(task.input, input, { name: task.name, })) as InferTaskInput<T>; } /** * Parse input and run an operational task. */ export async function runTask< T extends TaskDef<string, StandardSchema, Ctx>, Ctx, >(task: T, options: RunTaskOptions<Ctx>): Promise<InferTaskOutput<T>> { const traceAttributes = { "beignet.task.name": task.name, } as const; return await runWithResolvedTracingContext({ tracing: options.tracing, ctx: options.ctx, operation: { name: `beignet.task ${task.name}`, type: "task", kind: "internal", attributes: traceAttributes, metricAttributes: traceAttributes, }, run: async (ctx) => { const parsed = await parseTaskInput(task, options.input); return (await task.handle({ task, input: parsed, ctx, })) as InferTaskOutput<T>; }, }); } /** * Define a task registry while preserving tuple inference. */ export function defineTasks<const Defs extends readonly TaskDef[]>( tasks: Defs, ): Defs { return tasks; } /** * Create task helper methods bound to an application context type. * * Call it once in `lib/tasks.ts`: * * ```ts * export const { defineTask } = createTasks<AppContext>(); * ``` */ export function createTasks<Ctx>(): Tasks<Ctx> { return { defineTask<Name extends string, Input extends StandardSchema, Output>( name: Name, options: DefineTaskOptions<Name, Input, Ctx, Output>, ) { return defineTaskImpl<Name, Input, Ctx, Output>(name, options); }, }; }