# 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` — 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.