# @suaveplan/cron - Universal cron expression parser, next-fire calculator with timezone + DST support, and lightweight in-process scheduler ## Summary Universal cron expression parser, next-fire calculator with timezone + DST awareness, and an opt-in lightweight in-process scheduler. Zero external cron dependencies — written natively against `@suaveplan/error`, `@suaveplan/types`, and `Intl.DateTimeFormat`. ## Documentation tree - [errors](./src/errors/cron-error.md) — _(no submodules)_ - [cron-error](./src/errors/cron-error.md) — _(no submodules)_ - [fields](./src/fields/cron-fields.md) — _(no submodules)_ - [cron-fields](./src/fields/cron-fields.md) — _(no submodules)_ - [macros](./src/macros/macros.md) — _(no submodules)_ - [macros](./src/macros/macros.md) — _(no submodules)_ - [next-run](./src/next-run/next-run.md) — _(no submodules)_ - [next-run-fields](./src/next-run/next-run-fields.md) — `next-run-fields` houses the low-level field-walking helpers used by the forward and backward next-run search loops in… - [next-run](./src/next-run/next-run.md) — _(no submodules)_ - [otel](./src/otel/cron-otel.md) — _(no submodules)_ - [cron-otel](./src/otel/cron-otel.md) — _(no submodules)_ - [parse](./src/parse/parse-cron.md) — _(no submodules)_ - [parse-cron](./src/parse/parse-cron.md) — _(no submodules)_ - [scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_ - [cron-scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_ - [timezone](./src/timezone/timezone.md) — _(no submodules)_ - [timezone](./src/timezone/timezone.md) — _(no submodules)_ - [cron-error](./src/errors/cron-error.md) — _(no submodules)_- [fields](./src/fields/cron-fields.md) — _(no submodules)_- [cron-fields](./src/fields/cron-fields.md) — _(no submodules)_- [macros](./src/macros/macros.md) — _(no submodules)_- [macros](./src/macros/macros.md) — _(no submodules)_- [next-run](./src/next-run/next-run.md) — _(no submodules)_- [next-run-fields](./src/next-run/next-run-fields.md) — `next-run-fields` houses the low-level field-walking helpers used by the forward and backward next-run search loops in… - [next-run](./src/next-run/next-run.md) — _(no submodules)_- [otel](./src/otel/cron-otel.md) — _(no submodules)_- [cron-otel](./src/otel/cron-otel.md) — _(no submodules)_- [parse](./src/parse/parse-cron.md) — _(no submodules)_- [parse-cron](./src/parse/parse-cron.md) — _(no submodules)_- [scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_- [cron-scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_- [timezone](./src/timezone/timezone.md) — _(no submodules)_- [timezone](./src/timezone/timezone.md) — _(no submodules)_- [cron-error](./src/errors/cron-error.md) — _(no submodules)_- [fields](./src/fields/cron-fields.md) — _(no submodules)_- [cron-fields](./src/fields/cron-fields.md) — _(no submodules)_- [macros](./src/macros/macros.md) — _(no submodules)_- [macros](./src/macros/macros.md) — _(no submodules)_- [next-run](./src/next-run/next-run.md) — _(no submodules)_- [next-run-fields](./src/next-run/next-run-fields.md) — `next-run-fields` houses the low-level field-walking helpers used by the forward and backward next-run search loops in… - [next-run](./src/next-run/next-run.md) — _(no submodules)_- [otel](./src/otel/cron-otel.md) — _(no submodules)_- [cron-otel](./src/otel/cron-otel.md) — _(no submodules)_- [parse](./src/parse/parse-cron.md) — _(no submodules)_- [parse-cron](./src/parse/parse-cron.md) — _(no submodules)_- [scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_- [cron-scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_- [timezone](./src/timezone/timezone.md) — _(no submodules)_- [timezone](./src/timezone/timezone.md) — _(no submodules)_- [cron-error](./src/errors/cron-error.md) — _(no submodules)_- [fields](./src/fields/cron-fields.md) — _(no submodules)_- [cron-fields](./src/fields/cron-fields.md) — _(no submodules)_- [macros](./src/macros/macros.md) — _(no submodules)_- [macros](./src/macros/macros.md) — _(no submodules)_- [next-run](./src/next-run/next-run.md) — _(no submodules)_- [next-run-fields](./src/next-run/next-run-fields.md) — `next-run-fields` houses the low-level field-walking helpers used by the forward and backward next-run search loops in… - [next-run](./src/next-run/next-run.md) — _(no submodules)_- [otel](./src/otel/cron-otel.md) — _(no submodules)_- [cron-otel](./src/otel/cron-otel.md) — _(no submodules)_- [parse](./src/parse/parse-cron.md) — _(no submodules)_- [parse-cron](./src/parse/parse-cron.md) — _(no submodules)_- [scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_- [cron-scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_- [timezone](./src/timezone/timezone.md) — _(no submodules)_- [timezone](./src/timezone/timezone.md) — _(no submodules)_- [cron-error](./src/errors/cron-error.md) — _(no submodules)_- [fields](./src/fields/cron-fields.md) — _(no submodules)_ - [cron-fields](./src/fields/cron-fields.md) — _(no submodules)_- [macros](./src/macros/macros.md) — _(no submodules)_ - [macros](./src/macros/macros.md) — _(no submodules)_- [next-run](./src/next-run/next-run.md) — _(no submodules)_ - [next-run-fields](./src/next-run/next-run-fields.md) — `next-run-fields` houses the low-level field-walking helpers used by the forward and backward next-run search loops in… - [next-run](./src/next-run/next-run.md) — _(no submodules)_- [otel](./src/otel/cron-otel.md) — _(no submodules)_ - [cron-otel](./src/otel/cron-otel.md) — _(no submodules)_- [parse](./src/parse/parse-cron.md) — _(no submodules)_ - [parse-cron](./src/parse/parse-cron.md) — _(no submodules)_- [scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_ - [cron-scheduler](./src/scheduler/cron-scheduler.md) — _(no submodules)_- [timezone](./src/timezone/timezone.md) — _(no submodules)_ - [timezone](./src/timezone/timezone.md) — _(no submodules)_ ## Quick start ```typescript import { parseCron, isValidCron } from "@suaveplan/cron"; const expr = parseCron("*/15 * * * *"); console.log(expr.fields.minutes); // [0, 15, 30, 45] const next = expr.nextRun(Date.UTC(2026, 4, 11, 10, 7, 0), "America/New_York"); console.log(new Date(next as number).toISOString()); console.log(isValidCron("0 9 * * MON-FRI")); // true console.log(isValidCron("garbage")); // false ``` ## Scheduler example ```typescript import { CronScheduler } from "@suaveplan/cron"; const scheduler = new CronScheduler({ onError: (err, id) => console.error(`job ${id} failed`, err), }); scheduler.add("0 9 * * MON-FRI", async () => sendDailyDigest(), { tz: "America/New_York", }); scheduler.add("@hourly", () => collectMetrics()); scheduler.start(); process.on("SIGTERM", async () => { await scheduler.stop(); process.exit(0); }); ``` ## FakeClock test recipe ```typescript import { CronScheduler } from "@suaveplan/cron"; import type { SchedulerTimers } from "@suaveplan/cron"; const now = { value: Date.UTC(2026, 4, 11, 9, 59, 0) }; const pending = new Map void; fireAt: number }>(); let nextId = 1; const timers: SchedulerTimers = { setTimer: (cb, ms) => { const id = nextId++; pending.set(id, { cb, fireAt: now.value + ms }); return id; }, clearTimer: (id) => pending.delete(id), }; const scheduler = new CronScheduler({ clock: () => now.value, timers }); scheduler.add("0 * * * *", () => console.log("fired")); scheduler.start(); // Fast-forward 1 hour. now.value += 60 * 60 * 1000; for (const [id, { cb, fireAt }] of pending) { if (fireAt <= now.value) { pending.delete(id); cb(); } } await scheduler.stop(); ``` ## API reference | Surface | Purpose | | -------------------------------- | ------------------------------------------------------------------ | | `parseCron(expr, options?)` | Parse a 5/6/7-field expression into a frozen `CronExpression`. | | `isValidCron(expr, options?)` | Non-throwing parse check. | | `CronExpression.nextRun` | Next epoch instant strictly greater than `after`. | | `CronExpression.nextRuns` | Strictly monotonic array of `count` fire times. | | `CronExpression.previousRun` | Largest epoch instant strictly less than `before`. | | `CronExpression.matches` | Constant-time wall-clock membership check. | | `CronScheduler` | Lightweight in-process driver (no queue/retry/concurrency). | | `createCronScheduler` | Factory mirror. | | `parseField`, `rangeFor` | Single-field tokenizer + per-kind range. | | `expandMacro`, `macroTable` | Macro shortcut expansion (`@daily`, `@hourly`, …). | | `toWallClock`, `fromWallClock` | Bidirectional epoch ↔ wall-clock conversion in any IANA zone. | | `assertTimezone` | Eager IANA-zone validation. | | `detectDstTransition` | DST classifier between two adjacent instants. | | `CronError`, `CRON_CODES` | Typed error class + frozen code registry. | | `recordParse`, `recordNextRun` | OpenTelemetry instrumentation helpers. | ## Why not croner Croner is excellent reference material but ships as an opaque tarball, imposes its own API conventions, and is not universal in the sense. `@suaveplan/cron` lives at the same primitive tier as `@suaveplan/datetime` and `@suaveplan/timing`: zero external runtime deps beyond the foundation, typed errors via `defineCodes`, Zod boundary schemas, an OTel surface, and the standard five-files-per-feature layout. Splitting cron parsing out of the job-scheduler keeps the workflow-tier scheduler focused on queue / retry / concurrency concerns while letting validation packages and browser UIs consume parsing without dragging in the runtime. ## Coverage 100 % Istanbul coverage on every implementation file is gated by CI: ```bash bunx turbo test:coverage --filter=@suaveplan/cron ```