UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

270 lines 8.91 kB
import { createProviderInstrumentation, } from "../providers/index.js"; import { runWithResolvedTracingContext } from "../tracing/execution.js"; /** * Error thrown when schedule payload validation fails. */ export class ScheduleValidationError extends Error { /** * Raw Standard Schema validation issues. */ issues; constructor(args) { super(`Schedule "${args.name}" payload validation failed: ${formatIssues(args.issues)}`); this.name = "ScheduleValidationError"; this.issues = args.issues; } } /** * Error thrown when schedule run metadata cannot be normalized. */ export class ScheduleRunContextError extends Error { constructor(message) { super(message); this.name = "ScheduleRunContextError"; } } function formatPath(path) { if (!path?.length) return ""; return path .map((segment) => typeof segment === "object" && segment !== null && "key" in segment ? String(segment.key) : String(segment)) .join("."); } function formatIssues(issues) { return issues .map((issue) => { const path = formatPath(issue.path); return path ? `${path}: ${issue.message}` : issue.message; }) .join("; "); } async function parsePayload(schema, input, args) { const result = await schema["~standard"].validate(input); if (result.issues?.length) { throw new ScheduleValidationError({ name: args.name, issues: result.issues, }); } if ("value" in result) { return result.value; } throw new Error("Invalid Standard Schema result: missing value"); } function normalizeDate(value, field, fallback) { if (value === undefined) return fallback(); const date = value instanceof Date ? new Date(value.getTime()) : new Date(value); if (Number.isNaN(date.getTime())) { throw new ScheduleRunContextError(`Schedule run ${field} must be a valid date.`); } return date; } function normalizeOptionalDate(value, field) { if (value === undefined) return undefined; return normalizeDate(value, field, () => new Date()); } function normalizeOptionalAttempt(value) { if (value === undefined) return undefined; if (!Number.isInteger(value) || value < 1) { throw new ScheduleRunContextError("Schedule run attempt must be a positive integer."); } return value; } function createRunContext(options, now) { return { id: options.id, attempt: normalizeOptionalAttempt(options.attempt), scheduledAt: normalizeOptionalDate(options.scheduledAt, "scheduledAt"), triggeredAt: normalizeDate(options.triggeredAt, "triggeredAt", now), source: options.source, }; } function scheduleTraceOperation(schedule, options) { const attributes = { "beignet.schedule.name": schedule.name, ...(options.attempt === undefined ? {} : { "beignet.schedule.attempt": options.attempt }), ...(options.source === undefined ? {} : { "beignet.schedule.source": options.source }), }; const metricAttributes = { "beignet.schedule.name": schedule.name, ...(options.attempt === undefined ? {} : { "beignet.schedule.attempt": options.attempt }), }; return { name: `beignet.schedule ${schedule.name}`, type: "schedule", kind: "consumer", attributes, metricAttributes, }; } async function resolveSchedulePayload(schedule, options, run) { const hasExplicitPayload = Object.hasOwn(options, "payload"); const rawPayload = hasExplicitPayload ? options.payload : await schedule.createPayload?.({ schedule, run }); return parseSchedulePayload(schedule, rawPayload); } async function reportHookError(onHookError, args) { try { await onHookError?.(args); } catch { // Hook failures are isolated from schedule execution. } } async function runLifecycleHook(hook, handler, onHookError, args) { try { await handler?.(args); } catch (error) { await reportHookError(onHookError, { ...args, hook, error }); } } async function runErrorHook(handler, onHookError, args) { try { await handler?.(args); } catch (error) { await reportHookError(onHookError, { schedule: args.schedule, payload: args.payload, run: args.run, hook: "error", error, scheduleError: args.error, }); } } async function recordScheduleEvent(instrumentation, instrumentationContext, schedule, status, run, details) { instrumentation.record({ type: "schedule", watcher: "schedules", requestId: instrumentationContext?.requestId, traceId: instrumentationContext?.traceId, scheduleName: schedule.name, status, cron: schedule.cron, timezone: schedule.timezone, details: { source: run.source, scheduledAt: run.scheduledAt?.toISOString(), ...details, }, }); } function defineScheduleImpl(name, options) { return { kind: "schedule", name, cron: options.cron, timezone: options.timezone, payload: options.payload, description: options.description, createPayload: options.createPayload, handle: options.handle, }; } /** * Validate and parse a schedule payload with the schedule's Standard Schema. */ export async function parseSchedulePayload(schedule, payload) { return (await parsePayload(schedule.payload, payload, { name: schedule.name, })); } /** * Run one schedule directly with an explicit context. */ export async function runSchedule(schedule, args) { await runWithResolvedTracingContext({ tracing: args.tracing, ctx: args.ctx, operation: scheduleTraceOperation(schedule, args), run: async (ctx) => { const run = createRunContext(args, () => new Date()); const payload = await resolveSchedulePayload(schedule, args, run); await schedule.handle({ schedule, payload, ctx, run, }); }, }); } /** * Create a local/test schedule runner that executes handlers inline. */ export function createInlineScheduleRunner(options = {}) { const now = options.now ?? (() => new Date()); const instrumentation = createProviderInstrumentation(options.instrumentation, { providerName: "schedules", watcher: "schedules", }); return { async run(schedule, runOptions = {}) { const run = createRunContext(runOptions, now); let payload; try { payload = await resolveSchedulePayload(schedule, runOptions, run); const lifecycleArgs = { schedule, payload, run }; await recordScheduleEvent(instrumentation, options.instrumentationContext, schedule, "started", run); await runLifecycleHook("start", options.onStart, options.onHookError, lifecycleArgs); await runWithResolvedTracingContext({ tracing: options.tracing, ctx: options.ctx, operation: scheduleTraceOperation(schedule, runOptions), run: (ctx) => schedule.handle({ schedule, payload: payload, ctx, run, }), }); await recordScheduleEvent(instrumentation, options.instrumentationContext, schedule, "completed", run); await runLifecycleHook("success", options.onSuccess, options.onHookError, lifecycleArgs); } catch (error) { await recordScheduleEvent(instrumentation, options.instrumentationContext, schedule, "failed", run, { error }); await runErrorHook(options.onError, options.onHookError, { error, schedule, payload, run, }); throw error; } }, }; } /** * Create schedule helper methods bound to an application context type. * * Call it once in `lib/schedules.ts`: * * ```ts * export const { defineSchedule } = createSchedules<AppContext>(); * ``` * * Cron and timezone are metadata for schedule providers. The inline runner only * runs schedules when its `run(...)` method is called. */ export function createSchedules() { return { defineSchedule(name, options) { return defineScheduleImpl(name, options); }, }; } //# sourceMappingURL=index.js.map