3.6 KiB
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)withdelayMsor absoluterunAt. - Bounded
concurrencyenforced by the tick loop. - Per-job
retries,retryDelayMs, andtimeouthonoured by the execution module. pause/resumeat job level OR globally.cancelremoves from queue AND aborts the in-flight attempt.- Optional
catchupMissedreplay of past fires on resume, bounded bymaxMissedRuns. - Graceful drain on
stop()— every in-flight promise is awaited. - Cron-expression exhaustion handled via
job:scheduling_exhaustedevent andcron_exhausted_totalcounter.
Usage: Basic Example
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
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 JobSchedulerschedule(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.