@beignet/core
Version:
Core framework primitives for Beignet
270 lines • 8.91 kB
JavaScript
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