# job-dispatcher ## Purpose Encapsulates the "hand a job to the executor" step of the scheduler's tick loop. The scheduler core decides *which* job runs next and *when*; this module is responsible for the *how* — wiring the per-attempt `AbortController`, tracking the in-flight promise, and updating per-job counters (`runs`, `failures`, `lastRun`) on settle. Splitting this out keeps the scheduler core under the 300-line file budget and isolates the small but error-prone bookkeeping work in one focused module. ## Features - Single `dispatch(job, scheduledAt, env)` function — no class, no state of its own. - Captures the in-flight promise into the scheduler-owned `Set` for `stop()` drain. - Atomically transitions the job to `running` and registers a fresh `AbortController` on the job record. - Invokes the scheduler's `afterRun` callback in `finally`, regardless of success or failure. ## Usage: Basic Example ```ts import { dispatch } from "@suaveplan/job-scheduler"; dispatch(scheduledJob, scheduledAt, { clock, logger, events, timers, inFlight, runningSet, afterRun: (job) => { // scheduler decides whether to re-enqueue or remove }, }); ``` ## Usage: Advanced Example ```ts import { dispatch, type ScheduledJob } from "@suaveplan/job-scheduler"; function pump(jobs: Iterable): void { for (const job of jobs) { if (job.status !== "pending") continue; dispatch(job, job.nextRun ?? clock(), { clock, logger, events, timers, inFlight, runningSet, afterRun: (j) => { if (j.kind === "queued") { // one-shot — let the scheduler garbage-collect return; } // re-enqueue cron jobs by computing the next fire }, }); } } ``` ## API Reference - `dispatch(job: ScheduledJob, scheduledAt: number, env: DispatchEnv): void` — fires the job. No return value; all bookkeeping happens via `env`. - `interface ScheduledJob` — internal record shape mutated in place. - `interface DispatchEnv` — `{ clock, logger, events, timers, inFlight, runningSet, afterRun }`. ## Implementation Notes The `afterRun` callback is the scheduler's hook for "what happens once the job settles" — for queued jobs the scheduler typically deletes the entry; for cron jobs it computes the next `nextRun` and re-enqueues. Putting this branch in the scheduler core (rather than passing a flag to `dispatch`) keeps `dispatch` polymorphism-free and makes the lifecycle of every cron / queued job obvious at the call site. The promise is tracked in `env.inFlight` so `JobScheduler.stop()` can `Promise.allSettled` over the set and drain the in-flight work cleanly.