Scheduler
Cron-style task scheduler with cluster leader gating, deduplicated ticks, drift correction, and an audit event on every tick.
Two registration shapes share the same engine: 5-field POSIX cron ("0 2 * * *") for wall-clock schedules and every: ms (with an optional baseline: "HH:MM" anchor) for interval schedules. Timezones are IANA names; without one the schedule follows the server's local clock. Cron shorthands @hourly, @daily, @midnight, @weekly, @monthly, @yearly and @annually are accepted.
When opts.cluster is wired, fires are gated to the current leader. Every fire INSERTs a row into _blamejs_scheduler_ticks keyed on (name, scheduledAtUnix); the PRIMARY KEY race deduplicates across a split-brain window — losers increment task.tickClaimLost and skip. Tick-claim rows older than opts.tickRetentionMs (default 7 days) are pruned automatically by the leader, throttled to at most one sweep per opts.pruneIntervalMs (default 60s). Operators can force a sweep with sched.pruneTickClaims(olderThanMs?).
Drift correction: nextRun is computed forward from now (not from the nominal scheduled time) so a long-running fire never queues a backlog of catch-up ticks. A watchdog clears the running flag if a fire's promise hasn't settled after opts.maxJobMs (default 10 minutes) so a hung handler can't permanently lock out future fires. Every state transition emits an audit event under system.scheduler.* so operators see every fire, miss, watchdog reset, and tick-claim race in their audit log.
b.scheduler.parseCron(expr) #
Parse a 5-field POSIX cron expression (or one of the @hourly, @daily, @midnight, @weekly, @monthly, @yearly, @annually shorthands) into a struct of populated minute / hour / dom / month / dow sets plus the normalized expression text. Throws SchedulerError (scheduler/invalid-cron) on malformed input — empty fields, bad step / range syntax, or values outside each field's bounds. The dow field accepts both 0 and 7 for Sunday and normalizes to 0.
var cron = b.scheduler.parseCron("0 2 * * *");
cron.expr; // → "0 2 * * *"
cron.minute.has(0); // → true
cron.hour.has(2); // → true
var weekly = b.scheduler.parseCron("@weekly");
weekly.expr; // → "0 0 * * 0"
b.scheduler.nextCronFire(cron, after, timeZone) #
Earliest UTC millisecond strictly after after whose wall-clock in timeZone matches the parsed cron sets. Walks minute-by-minute; the search is bounded at one year plus a one-hour DST cushion before throwing SchedulerError (scheduler/cron-no-fire) so an impossible date constraint surfaces loudly instead of looping forever. Pass null for timeZone to follow the server's local clock.
var cron = b.scheduler.parseCron("0 2 * * *");
var when = b.scheduler.nextCronFire(cron, new Date("2026-05-09T00:00:00Z"), "UTC");
new Date(when).toISOString();
// → "2026-05-09T02:00:00.000Z"
b.scheduler.nextBaselineFire(timeOfDay, timeZone, after) #
Earliest UTC millisecond strictly after after whose wall-clock in timeZone matches the supplied HH:MM time-of-day. Used internally to anchor every-shaped tasks to a daily baseline; exposed so operators can compute the same instant for fixtures or external coordination. Throws SchedulerError on malformed input (scheduler/invalid-baseline) or on a no-fire-within-24h timezone bug (scheduler/baseline-no-fire). Pass null for timeZone to follow the server's local clock.
var when = b.scheduler.nextBaselineFire(
"02:30", "UTC", new Date("2026-05-09T01:00:00Z")
);
new Date(when).toISOString();
// → "2026-05-09T02:30:00.000Z"
b.scheduler.create(opts) #
{
jobs: object, // optional jobs instance for { job: "name" } tasks
cluster: object, // optional cluster instance — gates fires to leader
audit: boolean, // emit system.scheduler.* audit events (default true)
maxJobMs: number, // watchdog reset threshold (default 10 minutes)
tickRetentionMs: number, // tick-claim row retention (default 7 days)
pruneIntervalMs: number, // throttle for opportunistic prune (default 60s)
}
Build a scheduler instance. Returns a facade exposing schedule, register, start, stop, list, getStatus, and pruneTickClaims. Tasks are registered before start(); start() arms timers, stop() clears them and drops pending fires. When opts.cluster is supplied, fires are gated to the leader and a tick-claim row in _blamejs_scheduler_ticks deduplicates split-brain windows. When opts.jobs is supplied, tasks declared with { job: "name" } dispatch via the jobs queue; tasks declared with { run: fn } execute the function directly.
var sched = b.scheduler.create({ audit: true });
sched.schedule({
name: "nightly-cleanup",
cron: "0 2 * * *",
timezone: "UTC",
run: async function () { return "ok"; },
});
await sched.start();
var snapshot = sched.list();
snapshot[0].name; // → "nightly-cleanup"
await sched.stop();
Last updated 2026-08-08T16:39:49.652Z by seeder.