- Added support for new delivery modes in cron jobs: `announce`, `deliver`, and `none`. - Updated documentation to reflect changes in delivery options and usage examples. - Enhanced the cron job schema to include delivery configuration. - Refactored related CLI commands and UI components to accommodate the new delivery settings. - Improved handling of legacy delivery fields for backward compatibility. This update allows users to choose how output from isolated jobs is delivered, enhancing flexibility in job management.
186 lines
4.5 KiB
TypeScript
186 lines
4.5 KiB
TypeScript
import type { CronJobCreate, CronJobPatch } from "./types.js";
|
|
import { sanitizeAgentId } from "../routing/session-key.js";
|
|
import { parseAbsoluteTimeMs } from "./parse.js";
|
|
import { migrateLegacyCronPayload } from "./payload-migration.js";
|
|
|
|
type UnknownRecord = Record<string, unknown>;
|
|
|
|
type NormalizeOptions = {
|
|
applyDefaults?: boolean;
|
|
};
|
|
|
|
const DEFAULT_OPTIONS: NormalizeOptions = {
|
|
applyDefaults: false,
|
|
};
|
|
|
|
function isRecord(value: unknown): value is UnknownRecord {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function coerceSchedule(schedule: UnknownRecord) {
|
|
const next: UnknownRecord = { ...schedule };
|
|
const kind = typeof schedule.kind === "string" ? schedule.kind : undefined;
|
|
const atMsRaw = schedule.atMs;
|
|
const atRaw = schedule.at;
|
|
const parsedAtMs =
|
|
typeof atMsRaw === "string"
|
|
? parseAbsoluteTimeMs(atMsRaw)
|
|
: typeof atRaw === "string"
|
|
? parseAbsoluteTimeMs(atRaw)
|
|
: null;
|
|
|
|
if (!kind) {
|
|
if (
|
|
typeof schedule.atMs === "number" ||
|
|
typeof schedule.at === "string" ||
|
|
typeof schedule.atMs === "string"
|
|
) {
|
|
next.kind = "at";
|
|
} else if (typeof schedule.everyMs === "number") {
|
|
next.kind = "every";
|
|
} else if (typeof schedule.expr === "string") {
|
|
next.kind = "cron";
|
|
}
|
|
}
|
|
|
|
if (typeof schedule.atMs !== "number" && parsedAtMs !== null) {
|
|
next.atMs = parsedAtMs;
|
|
}
|
|
|
|
if ("at" in next) {
|
|
delete next.at;
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
function coercePayload(payload: UnknownRecord) {
|
|
const next: UnknownRecord = { ...payload };
|
|
// Back-compat: older configs used `provider` for delivery channel.
|
|
migrateLegacyCronPayload(next);
|
|
return next;
|
|
}
|
|
|
|
function coerceDelivery(delivery: UnknownRecord) {
|
|
const next: UnknownRecord = { ...delivery };
|
|
if (typeof delivery.mode === "string") {
|
|
next.mode = delivery.mode.trim().toLowerCase();
|
|
}
|
|
if (typeof delivery.channel === "string") {
|
|
const trimmed = delivery.channel.trim().toLowerCase();
|
|
if (trimmed) {
|
|
next.channel = trimmed;
|
|
} else {
|
|
delete next.channel;
|
|
}
|
|
}
|
|
if (typeof delivery.to === "string") {
|
|
const trimmed = delivery.to.trim();
|
|
if (trimmed) {
|
|
next.to = trimmed;
|
|
} else {
|
|
delete next.to;
|
|
}
|
|
}
|
|
return next;
|
|
}
|
|
|
|
function unwrapJob(raw: UnknownRecord) {
|
|
if (isRecord(raw.data)) {
|
|
return raw.data;
|
|
}
|
|
if (isRecord(raw.job)) {
|
|
return raw.job;
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
export function normalizeCronJobInput(
|
|
raw: unknown,
|
|
options: NormalizeOptions = DEFAULT_OPTIONS,
|
|
): UnknownRecord | null {
|
|
if (!isRecord(raw)) {
|
|
return null;
|
|
}
|
|
const base = unwrapJob(raw);
|
|
const next: UnknownRecord = { ...base };
|
|
|
|
if ("agentId" in base) {
|
|
const agentId = base.agentId;
|
|
if (agentId === null) {
|
|
next.agentId = null;
|
|
} else if (typeof agentId === "string") {
|
|
const trimmed = agentId.trim();
|
|
if (trimmed) {
|
|
next.agentId = sanitizeAgentId(trimmed);
|
|
} else {
|
|
delete next.agentId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ("enabled" in base) {
|
|
const enabled = base.enabled;
|
|
if (typeof enabled === "boolean") {
|
|
next.enabled = enabled;
|
|
} else if (typeof enabled === "string") {
|
|
const trimmed = enabled.trim().toLowerCase();
|
|
if (trimmed === "true") {
|
|
next.enabled = true;
|
|
}
|
|
if (trimmed === "false") {
|
|
next.enabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isRecord(base.schedule)) {
|
|
next.schedule = coerceSchedule(base.schedule);
|
|
}
|
|
|
|
if (isRecord(base.payload)) {
|
|
next.payload = coercePayload(base.payload);
|
|
}
|
|
|
|
if (isRecord(base.delivery)) {
|
|
next.delivery = coerceDelivery(base.delivery);
|
|
}
|
|
|
|
if (options.applyDefaults) {
|
|
if (!next.wakeMode) {
|
|
next.wakeMode = "next-heartbeat";
|
|
}
|
|
if (!next.sessionTarget && isRecord(next.payload)) {
|
|
const kind = typeof next.payload.kind === "string" ? next.payload.kind : "";
|
|
if (kind === "systemEvent") {
|
|
next.sessionTarget = "main";
|
|
}
|
|
if (kind === "agentTurn") {
|
|
next.sessionTarget = "isolated";
|
|
}
|
|
}
|
|
}
|
|
|
|
return next;
|
|
}
|
|
|
|
export function normalizeCronJobCreate(
|
|
raw: unknown,
|
|
options?: NormalizeOptions,
|
|
): CronJobCreate | null {
|
|
return normalizeCronJobInput(raw, {
|
|
applyDefaults: true,
|
|
...options,
|
|
}) as CronJobCreate | null;
|
|
}
|
|
|
|
export function normalizeCronJobPatch(
|
|
raw: unknown,
|
|
options?: NormalizeOptions,
|
|
): CronJobPatch | null {
|
|
return normalizeCronJobInput(raw, {
|
|
applyDefaults: false,
|
|
...options,
|
|
}) as CronJobPatch | null;
|
|
}
|