# cron-rescheduler ## Purpose Houses the two cron-related decisions the scheduler makes after a job settles or resumes: (1) compute the next fire from the cron expression, OR (2) recognise that the expression has no future fire and gracefully cancel the job. The module also implements optional catchup-missed-fires logic for resume-after-pause scenarios. Extracting this here keeps the scheduler core small and isolates the only place where `CRON.NEXT_RUN_NOT_FOUND` is consumed. ## Features - `reenqueueCron(job, env, tz)` — pushes the next fire onto the queue, or emits `job:scheduling_exhausted` once and marks the job cancelled when the expression is exhausted. - `applyCatchupAndReenqueue(...)` — enqueues up to `maxMissedRuns` past fires (those at `now`-or-later are skipped) before re-enqueuing the next future fire. - Closed `ReenqueueOutcome = "scheduled" | "exhausted"` return for caller branching. - OTel `cron_exhausted_total` counter is incremented exactly once per exhausted job. ## Usage: Basic Example ```ts import { reenqueueCron } from "@suaveplan/job-scheduler"; const outcome = reenqueueCron(job, env, "UTC"); if (outcome === "exhausted") { // job has been marked cancelled; bus has emitted the event } ``` ## Usage: Advanced Example ```ts import { applyCatchupAndReenqueue, reenqueueCron, } from "@suaveplan/job-scheduler"; function onResume(job: ScheduledJob): void { if (job.kind !== "cron") return; const outcome = applyCatchupAndReenqueue( job, env, job.tz ?? "UTC", /* catchupMissed */ true, /* maxMissedRuns */ 3 ); if (outcome === "exhausted") { // log + surface to ops dashboard } } function onComplete(job: ScheduledJob): void { reenqueueCron(job, env, job.tz ?? "UTC"); } ``` ## API Reference - `type ReenqueueOutcome = "scheduled" | "exhausted"`. - `interface CronEnv { clock, events, queue, expressionOf }`. - `reenqueueCron(job, env, tz): ReenqueueOutcome`. - `applyCatchupAndReenqueue(job, env, tz, catchupMissed, maxMissedRuns): ReenqueueOutcome`. ## Implementation Notes `expressionOf` is a thin lookup callback rather than a direct map reference so the scheduler can keep the `CronExpression` cache private — exposing it as a parameter would tempt callers to mutate the cache by side channel. The catchup branch uses `expression.nextRuns(maxMissedRuns, lastRun, tz)` from `@suaveplan/cron` rather than rolling its own iteration, ensuring DST transitions and timezone edge cases are handled identically to the rest of the cron stack. Errors other than `CRON.NEXT_RUN_NOT_FOUND` rethrow — they indicate a programming bug (e.g., an invalid tz that somehow slipped past validation) and must surface, not be swallowed.