Files
skills/docs/foundation/flow/job-scheduler/src/scheduler/job-scheduler.md
2026-05-29 08:20:56 +01:00

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) 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

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 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.