first commit
This commit is contained in:
144
docs/foundation/timing/cron/README.md
Normal file
144
docs/foundation/timing/cron/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# @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`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [errors](./src/errors/cron-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [cron-error](./src/errors/cron-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [cron-fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [next-run](./src/next-run/next-run.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [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) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [cron-otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [parse](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [parse-cron](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [cron-scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [cron-error](./src/errors/cron-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_- [next-run](./src/next-run/next-run.md) — <!-- begin:auto-docs --> _(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) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse-cron](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_- [scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-error](./src/errors/cron-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_- [next-run](./src/next-run/next-run.md) — <!-- begin:auto-docs --> _(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) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse-cron](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_- [scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-error](./src/errors/cron-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_- [next-run](./src/next-run/next-run.md) — <!-- begin:auto-docs --> _(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) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse-cron](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_- [scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-error](./src/errors/cron-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cron-fields](./src/fields/cron-fields.md) — <!-- begin:auto-docs --> _(no submodules)_- [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_ - [macros](./src/macros/macros.md) — <!-- begin:auto-docs --> _(no submodules)_- [next-run](./src/next-run/next-run.md) — <!-- begin:auto-docs --> _(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) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cron-otel](./src/otel/cron-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [parse](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_ - [parse-cron](./src/parse/parse-cron.md) — <!-- begin:auto-docs --> _(no submodules)_- [scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_ - [cron-scheduler](./src/scheduler/cron-scheduler.md) — <!-- begin:auto-docs --> _(no submodules)_- [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(no submodules)_ - [timezone](./src/timezone/timezone.md) — <!-- begin:auto-docs --> _(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<number, { cb: () => 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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user