import { Cron } from "croner"; import type { CronSchedule } from "./types.js"; import { parseAbsoluteTimeMs } from "./parse.js"; export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): number | undefined { if (schedule.kind === "at") { // Handle both canonical `at` (string) and legacy `atMs` (number) fields. // The store migration should convert atMs→at, but be defensive in case // the migration hasn't run yet or was bypassed. const sched = schedule as { at?: string; atMs?: number | string }; const atMs = typeof sched.atMs === "number" && Number.isFinite(sched.atMs) && sched.atMs > 0 ? sched.atMs : typeof sched.atMs === "string" ? parseAbsoluteTimeMs(sched.atMs) : typeof sched.at === "string" ? parseAbsoluteTimeMs(sched.at) : null; if (atMs === null) { return undefined; } return atMs > nowMs ? atMs : undefined; } if (schedule.kind === "every") { const everyMs = Math.max(1, Math.floor(schedule.everyMs)); const anchor = Math.max(0, Math.floor(schedule.anchorMs ?? nowMs)); if (nowMs < anchor) { return anchor; } const elapsed = nowMs - anchor; const steps = Math.max(1, Math.floor((elapsed + everyMs - 1) / everyMs)); return anchor + steps * everyMs; } const expr = schedule.expr.trim(); if (!expr) { return undefined; } const cron = new Cron(expr, { timezone: schedule.tz?.trim() || undefined, catch: false, }); const next = cron.nextRun(new Date(nowMs)); return next ? next.getTime() : undefined; }