Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# @suaveplan/job-scheduler - Universal in-process job scheduler with cron + queued runs, retries, timeouts, concurrency control, and OTel observability
|
||||
|
||||
## Summary
|
||||
|
||||
Universal in-process job scheduler with cron + queued runs, retries, timeouts, concurrency control, and OpenTelemetry observability. Built on `@suaveplan/cron` for expression parsing and next-fire computation.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [errors](./src/errors/job-scheduler-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [job-scheduler-error](./src/errors/job-scheduler-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [job-execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [job-scheduler-otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [job-queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [job-registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [cron-rescheduler](./src/scheduler/cron-rescheduler.md) — Houses the two cron-related decisions the scheduler makes after a job settles or resumes: (1) compute the next fire fro…
|
||||
- [job-dispatcher](./src/scheduler/job-dispatcher.md) — Encapsulates the "hand a job to the executor" step of the scheduler's tick loop. The scheduler core decides *which* job…
|
||||
- [job-scheduler](./src/scheduler/job-scheduler.md) — The `JobScheduler` class is the package's main entry point: it accepts both cron-scheduled and one-shot queued jobs, ru…
|
||||
- [job-snapshot](./src/scheduler/job-snapshot.md) — Projects a mutable `ScheduledJob` record (the scheduler's internal data structure) to an immutable `JobStatus` snapshot…
|
||||
<!-- end:auto-docs --> - [job-scheduler-error](./src/errors/job-scheduler-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_- [execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-scheduler-otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-rescheduler](./src/scheduler/cron-rescheduler.md) — Houses the two cron-related decisions the scheduler makes after a job settles or resumes: (1) compute the next fire fro…
|
||||
- [job-dispatcher](./src/scheduler/job-dispatcher.md) — Encapsulates the "hand a job to the executor" step of the scheduler's tick loop. The scheduler core decides _which_ job…
|
||||
- [job-scheduler](./src/scheduler/job-scheduler.md) — The `JobScheduler` class is the package's main entry point: it accepts both cron-scheduled and one-shot queued jobs, ru…
|
||||
- [job-snapshot](./src/scheduler/job-snapshot.md) — Projects a mutable `ScheduledJob` record (the scheduler's internal data structure) to an immutable `JobStatus` snapshot…
|
||||
- [job-scheduler-error](./src/errors/job-scheduler-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_- [execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-scheduler-otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-rescheduler](./src/scheduler/cron-rescheduler.md) — Houses the two cron-related decisions the scheduler makes after a job settles or resumes: (1) compute the next fire fro…
|
||||
- [job-dispatcher](./src/scheduler/job-dispatcher.md) — Encapsulates the "hand a job to the executor" step of the scheduler's tick loop. The scheduler core decides _which_ job…
|
||||
- [job-scheduler](./src/scheduler/job-scheduler.md) — The `JobScheduler` class is the package's main entry point: it accepts both cron-scheduled and one-shot queued jobs, ru…
|
||||
- [job-snapshot](./src/scheduler/job-snapshot.md) — Projects a mutable `ScheduledJob` record (the scheduler's internal data structure) to an immutable `JobStatus` snapshot…
|
||||
- [job-scheduler-error](./src/errors/job-scheduler-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_- [execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-scheduler-otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [job-registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-rescheduler](./src/scheduler/cron-rescheduler.md) — Houses the two cron-related decisions the scheduler makes after a job settles or resumes: (1) compute the next fire fro…
|
||||
- [job-dispatcher](./src/scheduler/job-dispatcher.md) — Encapsulates the "hand a job to the executor" step of the scheduler's tick loop. The scheduler core decides _which_ job…
|
||||
- [job-scheduler](./src/scheduler/job-scheduler.md) — The `JobScheduler` class is the package's main entry point: it accepts both cron-scheduled and one-shot queued jobs, ru…
|
||||
- [job-snapshot](./src/scheduler/job-snapshot.md) — Projects a mutable `ScheduledJob` record (the scheduler's internal data structure) to an immutable `JobStatus` snapshot…
|
||||
- [job-scheduler-error](./src/errors/job-scheduler-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_ - [events](./src/events/events.md) — <!-- begin:auto-docs --> _(no submodules)_- [execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_ - [job-execution](./src/execution/job-execution.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [job-scheduler-otel](./src/otel/job-scheduler-otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_ - [job-queue](./src/queue/job-queue.md) — <!-- begin:auto-docs --> _(no submodules)_- [registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_ - [job-registry](./src/registry/job-registry.md) — <!-- begin:auto-docs --> _(no submodules)_- [cron-rescheduler](./src/scheduler/cron-rescheduler.md) — Houses the two cron-related decisions the scheduler makes after a job settles or resumes: (1) compute the next fire fro…
|
||||
- [job-dispatcher](./src/scheduler/job-dispatcher.md) — Encapsulates the "hand a job to the executor" step of the scheduler's tick loop. The scheduler core decides _which_ job…
|
||||
- [job-scheduler](./src/scheduler/job-scheduler.md) — The `JobScheduler` class is the package's main entry point: it accepts both cron-scheduled and one-shot queued jobs, ru…
|
||||
- [job-snapshot](./src/scheduler/job-snapshot.md) — Projects a mutable `ScheduledJob` record (the scheduler's internal data structure) to an immutable `JobStatus` snapshot…
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/job-scheduler
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { createJobScheduler } from "@suaveplan/job-scheduler";
|
||||
|
||||
const scheduler = createJobScheduler();
|
||||
|
||||
scheduler.schedule("@hourly", async ({ logger }) => {
|
||||
logger.info("hourly heartbeat");
|
||||
});
|
||||
|
||||
scheduler.queue(async () => {
|
||||
await flushQueue();
|
||||
}, { delayMs: 5_000 });
|
||||
|
||||
scheduler.start();
|
||||
|
||||
// On graceful shutdown:
|
||||
await scheduler.stop();
|
||||
```
|
||||
|
||||
## Retries + timeout
|
||||
|
||||
```ts
|
||||
scheduler.schedule(
|
||||
"*/15 * * * *",
|
||||
async ({ signal, attempt }) => {
|
||||
const res = await fetch("/api/sync", { signal });
|
||||
if (!res.ok) throw new Error("upstream failed");
|
||||
if (attempt > 1) console.warn("recovered after retry");
|
||||
},
|
||||
{ retries: 3, retryDelayMs: 1_000, timeout: 30_000, tz: "UTC" }
|
||||
);
|
||||
```
|
||||
|
||||
## Deterministic testing with FakeClock
|
||||
|
||||
```ts
|
||||
import { createJobScheduler } from "@suaveplan/job-scheduler";
|
||||
|
||||
interface PendingTimer { fireAt: number; cb: () => void }
|
||||
|
||||
const state = { now: 0, nextId: 1, pending: new Map<number, PendingTimer>() };
|
||||
const timers = {
|
||||
setTimer: (cb: () => void, ms: number) => {
|
||||
const id = state.nextId++;
|
||||
state.pending.set(id, { fireAt: state.now + ms, cb });
|
||||
return id;
|
||||
},
|
||||
clearTimer: (h: unknown) => state.pending.delete(h as number),
|
||||
};
|
||||
|
||||
async function advance(target: number) {
|
||||
while (true) {
|
||||
let id: number | undefined;
|
||||
let at = Infinity;
|
||||
for (const [k, v] of state.pending) {
|
||||
if (v.fireAt <= target && v.fireAt < at) { id = k; at = v.fireAt; }
|
||||
}
|
||||
if (id === undefined) break;
|
||||
const t = state.pending.get(id)!;
|
||||
state.pending.delete(id);
|
||||
state.now = t.fireAt;
|
||||
t.cb();
|
||||
await Promise.resolve();
|
||||
}
|
||||
state.now = target;
|
||||
}
|
||||
|
||||
const scheduler = createJobScheduler({
|
||||
clock: () => state.now,
|
||||
timers,
|
||||
});
|
||||
```
|
||||
|
||||
## API summary
|
||||
|
||||
| Method | Purpose |
|
||||
| --------------------------------- | -------------------------------------------------------------------- |
|
||||
| `schedule(cronExpr, fn, opts?)` | Run `fn` on every cron fire; returns jobId. |
|
||||
| `queue(fn, opts?)` | One-shot run with `delayMs` or absolute `runAt`. |
|
||||
| `cancel(jobId)` | Remove from queue + abort in-flight attempt. |
|
||||
| `pause(jobId?)` / `resume(jobId?)`| Per-job or global pause/resume. |
|
||||
| `jobs()` | Frozen `JobStatus[]` snapshot for observability. |
|
||||
| `start()` | Begin the tick loop (idempotent). |
|
||||
| `stop()` | Drain in-flight + stop ticking (idempotent). |
|
||||
|
||||
## Relationship to `@suaveplan/cron`
|
||||
|
||||
This package consumes `@suaveplan/cron`'s `parseCron` + `CronExpression` and is the recommended entry point for any application that needs both cron-style and ad-hoc scheduling. The cron parser stays narrow and easy to audit; this scheduler adds retries, timeouts, concurrency, lifecycle, and observability on top.
|
||||
|
||||
## Events
|
||||
|
||||
The scheduler emits the following topics on the bus passed via `JobSchedulerOptions.events`:
|
||||
|
||||
- `job:start` — fired before each attempt.
|
||||
- `job:complete` — successful settle.
|
||||
- `job:fail` — terminal failure for an attempt (retries-permitting, the next attempt fires another `job:start`).
|
||||
- `job:scheduling_exhausted` — cron expression has no future fire; job is auto-cancelled.
|
||||
- `job:paused` / `job:resumed` / `job:cancelled` — lifecycle transitions.
|
||||
|
||||
Use the `JOB_SCHEDULER_EVENTS` constants instead of literal strings to get compile-time guarantees that subscribers match emitters.
|
||||
|
||||
## Observability
|
||||
|
||||
The package records into the `@suaveplan/job-scheduler` OTel meter:
|
||||
|
||||
- `job_scheduler.duration_ms` (histogram, labelled `outcome` + `kind`).
|
||||
- `job_scheduler.runs_total` (counter, labelled `outcome` + `kind`).
|
||||
- `job_scheduler.failures_total` (counter, labelled `kind`).
|
||||
- `job_scheduler.cron_exhausted_total` (counter).
|
||||
- `job_scheduler.queue_depth` (histogram, sampled per tick).
|
||||
|
||||
`outcome ∈ { success, error, timeout }` and `kind ∈ { cron, queued }` — both closed enums to keep label cardinality bounded.
|
||||
Reference in New Issue
Block a user