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; /** * Value or promise of that value. */ export type MaybePromise = T | Promise; /** * Infer the parsed output type from a Standard Schema. */ export type InferSchemaOutput = StandardSchemaV1.InferOutput; /** * 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, Ctx>, ): MaybePromise; } /** * Infer the parsed input type for an operational task. */ export type InferTaskInput = T["input"] extends StandardSchemaV1 ? Output : never; /** * Infer the result type for an operational task. */ export type InferTaskOutput = T extends TaskDef ? Awaited : never; /** * Arguments passed to a task handler. */ export interface TaskHandleArgs { /** * Task definition being handled. */ task: T; /** * Parsed task input. */ input: InferTaskInput; /** 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, Ctx>, ): MaybePromise; } /** * Options for one task run. */ export interface RunTaskOptions { /** * 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); /** 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 { /** * 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 { /** * Define a task with the bound context type. */ defineTask< Name extends string, Input extends StandardSchema, Output = unknown, >( name: Name, options: DefineTaskOptions, ): TaskDef; } /** * 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: Schema, input: unknown, options: { name: string }, ): Promise> { 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 }).value; } function defineTaskImpl< Name extends string, Input extends StandardSchema, Ctx = unknown, Output = unknown, >( name: Name, options: DefineTaskOptions, ): TaskDef { return { kind: "task", name, input: options.input, description: options.description, handle: options.handle as TaskDef["handle"], }; } /** * Validate and parse a task input with the task's Standard Schema. */ export async function parseTaskInput( task: T, input: unknown, ): Promise> { return (await parseInput(task.input, input, { name: task.name, })) as InferTaskInput; } /** * Parse input and run an operational task. */ export async function runTask< T extends TaskDef, Ctx, >(task: T, options: RunTaskOptions): Promise> { 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; }, }); } /** * Define a task registry while preserving tuple inference. */ export function defineTasks( 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(); * ``` */ export function createTasks(): Tasks { return { defineTask( name: Name, options: DefineTaskOptions, ) { return defineTaskImpl(name, options); }, }; }