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,86 @@
# job-scheduler
## Purpose
The `JobScheduler` class is the package's main entry point: it accepts both cron-scheduled and one-shot queued jobs, runs them through a single tick loop with bounded concurrency, and exposes lifecycle controls (`start`, `stop`, `pause`, `resume`, `cancel`) plus an observability snapshot via `jobs()`. Cron parsing is delegated to `@suaveplan/cron` so the scheduler never embeds its own expression parser. The injectable `Clock`, `timers`, `events`, and `generateId` make every test run fully deterministic without monkey-patching globals.
## Features
- Cron-scheduled jobs via `schedule(cronExpr, handler, options)`.
- One-shot queued jobs via `queue(handler, options)` with `delayMs` or absolute `runAt`.
- Bounded `concurrency` enforced by the tick loop.
- Per-job `retries`, `retryDelayMs`, and `timeout` honoured by the execution module.
- `pause` / `resume` at job level OR globally.
- `cancel` removes from queue AND aborts the in-flight attempt.
- Optional `catchupMissed` replay of past fires on resume, bounded by `maxMissedRuns`.
- Graceful drain on `stop()` — every in-flight promise is awaited.
- Cron-expression exhaustion handled via `job:scheduling_exhausted` event and `cron_exhausted_total` counter.
## Usage: Basic Example
```ts
import { createJobScheduler } from "@suaveplan/job-scheduler";
const scheduler = createJobScheduler();
scheduler.schedule("*/5 * * * *", async ({ logger }) => {
logger.info("five-minute heartbeat");
});
scheduler.start();
// later
await scheduler.stop();
```
## Usage: Advanced Example
```ts
import { createJobScheduler, JobRegistry } from "@suaveplan/job-scheduler";
import { EventBus } from "@suaveplan/events";
const events = new EventBus();
events.on("job:fail", ({ jobId, error }) => {
console.error("job failed", jobId, error);
});
const registry = new JobRegistry();
const scheduler = createJobScheduler({
concurrency: 10,
events,
catchupMissed: true,
maxMissedRuns: 3,
});
const id = scheduler.schedule(
"0 * * * *",
async ({ signal, attempt }) => {
await fetch("/sync", { signal });
if (attempt > 1) console.warn("recovered after retry");
},
{ retries: 2, retryDelayMs: 500, timeout: 30_000, tz: "UTC" }
);
registry.register("hourly-sync", id);
const adHoc = scheduler.queue(
async () => {
// one-shot job
},
{ delayMs: 5_000 }
);
scheduler.start();
```
## API Reference
- `class JobScheduler`
- `schedule(cronExpr, handler, options?): string` — parses via `@suaveplan/cron`; returns jobId.
- `queue(handler, options?): string` — schedules a one-shot run.
- `cancel(jobId): boolean` — cancels the job; aborts in-flight attempt if any.
- `pause(jobId?): void` / `resume(jobId?): void` — per-job or global.
- `jobs(): readonly JobStatus[]` — frozen snapshot.
- `start(): void` — idempotent.
- `stop(): Promise<void>` — drains every in-flight job before resolving.
- `createJobScheduler(options?): JobScheduler` — factory convenience.
## Implementation Notes
The tick loop is driven by the injected `timers.setTimer` rather than `setInterval` so a slow tick cannot pile up overlapping invocations — every tick schedules its successor in `finally`. The cron-exhaustion branch deliberately emits exactly one event per exhausted job by marking the status `cancelled` before re-entering `afterRun`. The `expressions` cache is private and only reachable through `cronEnv.expressionOf` so the rescheduler helper module cannot mutate it. `stop()` aborts every in-flight controller before awaiting `Promise.allSettled` so handlers that respect `ctx.signal` short-circuit rather than running to completion under shutdown.