first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
# CronScheduler
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`CronScheduler` is a deliberately minimal in-process cron driver. It
parses each registered expression once via `parseCron`, schedules a
single `setTimeout` per job pointing at the next computed fire time, and
re-schedules immediately after each handler completes. Queueing,
retries, concurrency limits, persistence, and lifecycle hooks are
intentionally out of scope — that's `@suaveplan/job-scheduler`'s
responsibility. This class is small enough to embed inside a CLI, a
browser admin panel, or a serverless worker without dragging in the
full job-runner footprint.
## Features
- `add(expr, handler, options?)` returns a string id (auto-generated via
`crypto.randomUUID()` when omitted).
- `remove(id)` clears the timer and removes the job.
- `list()` snapshots every registered job with its next-run time.
- `start()` / `stop()` are idempotent; `stop()` awaits every in-flight
handler before resolving.
- Handler errors route to `options.onError` when provided, otherwise are
silently swallowed.
- Injectable `Clock` and injectable `{ setTimer, clearTimer }` pair for
fast, deterministic tests.
## Usage: Basic Example
```typescript
import { CronScheduler } from "@suaveplan/cron";
const scheduler = new CronScheduler();
scheduler.add("0 * * * *", async () => {
console.log("hourly tick");
});
scheduler.start();
// later…
await scheduler.stop();
```
## Usage: Advanced Example
```typescript
import { createCronScheduler } from "@suaveplan/cron";
let fakeNow = Date.UTC(2026, 4, 11, 9, 59, 0);
const pending = new Map<number, { cb: () => void; fireAt: number }>();
let nextTimerId = 1;
const scheduler = createCronScheduler({
clock: () => fakeNow,
timers: {
setTimer: (cb, ms) => {
const id = nextTimerId++;
pending.set(id, { cb, fireAt: fakeNow + ms });
return id;
},
clearTimer: (id) => pending.delete(id),
},
onError: (err, id) => console.error(`job ${id}`, err),
});
scheduler.add("0 * * * *", () => console.log("fired"));
scheduler.start();
// fast-forward
fakeNow = Date.UTC(2026, 4, 11, 10, 0, 1);
for (const [id, { cb, fireAt }] of pending) {
if (fireAt <= fakeNow) {
pending.delete(id);
cb();
}
}
await scheduler.stop();
```
## API Reference
- `class CronScheduler` with `constructor(options?: CronSchedulerOptions)`.
- `add(expr: string, handler: () => void | Promise<void>,
options?: CronSchedulerAddOptions): string`.
- `remove(id: string): boolean`.
- `list(): readonly CronSchedulerJob[]`.
- `start(): void`.
- `stop(): Promise<void>`.
- `createCronScheduler(options?): CronScheduler` — factory mirror.
- `CronSchedulerOptions`, `CronSchedulerAddOptions`, `CronSchedulerJob`,
`SchedulerTimers` types.
## Implementation Notes
Each job tracks at most one outstanding timer id. `stop()` walks every
job, clears its timer, and then `await`s the union of every promise in
the `inFlight` set, so handlers mid-execution always run to completion
before the returned promise resolves. Handler errors are caught inside
`invokeHandler` and forwarded to `onError`; the post-handler
re-scheduling continues unconditionally so a transient handler failure
does not stop the job from firing again. The injectable `timers` pair
is the testing seam: production callers get the real `setTimeout`, while
tests bind it to a `FakeClock` so the integration suite can fire several
hours of cron triggers in milliseconds. Job ids are auto-generated via
`crypto.randomUUID` when available, falling back to a timestamp-plus-
random string in environments that lack it, so the scheduler stays
universal.