-
Notifications
You must be signed in to change notification settings - Fork 2
Cronus Jobs
Job lifecycle, overlap prevention, run-once/run-now, and the event surface.
- Registering jobs
- Overlap prevention
- Run-once and run-now
- The run context
- Events
- Error isolation
- API Reference
- Related Documentation
import { Cronus } from '@tundralibs/cronus';
import type { CronusAction } from '@tundralibs/cronus/types';
declare function buildReport(): Promise<void>;
declare const betaSync: CronusAction;
const cron = new Cronus();
cron.add('report', '0 6 * * *', async (ctx) => {
await buildReport();
});
// Registered disabled — the ticker skips it until enable():
cron.add('beta', '*/5 * * * *', betaSync, { enabled: false });
cron.start();Registration is loud: a duplicate name, a malformed schedule, or a non-function action throw immediately (see Cronus-Errors) — never a silent never-fires. Jobs can be added and removed while the ticker runs.
A job never overlaps itself. While its action is running, matching
ticks are skipped (a skip event fires per skipped tick). A job
scheduled * * * * * whose run takes five minutes resumes on the
sixth minute — there is no queue and no pile-up:
minute 1 2 3 4 5 6
run──────────────────────┐ run
skip skip skip skip
Different jobs run independently — a slow job never delays others.
The guard is per registration, not per name. Removing a job and re-adding the same name while the old run is still in flight starts a fresh guard on a new internal record — so the old run (still executing its closure) and a new run under the same name can briefly execute concurrently. If that matters for your job, wait for the old run to settle (e.g. via the
finishevent) before re-adding.
trigger() makes the guard easy to see without waiting on the clock —
calling it twice back to back, the second call observes the job still
running and backs off instead of starting a second execution:
import { Cronus } from '@tundralibs/cronus';
const cron = new Cronus();
cron.add(
'slow',
'* * * * *',
() => new Promise((resolve) => setTimeout(resolve, 50)),
);
const first = cron.trigger('slow'); // starts running
const second = await cron.trigger('slow'); // sees running=true → backs off
console.assert(second === false);
const firstResult = await first;
console.assert(firstResult === true); // the original run still completes normallyimport { Cronus } from '@tundralibs/cronus';
declare function runMigration(): Promise<void>;
const cron = new Cronus();
cron.add('report', '0 6 * * *', () => {});
// Run ONCE at the next matching minute, then auto-remove:
cron.addOnce('migrate', '0 3 * * *', runMigration);
// Run NOW, bypassing the schedule:
const fired = await cron.trigger('report');trigger() respects the overlap guard — it resolves false (without
running) when the job is already mid-run, true after the run
settles. It works whether or not the ticker is started, and on
disabled jobs. A one-shot job auto-removes after its single run —
scheduled or triggered, success or error.
Actions must settle. An action that never resolves — a hung fetch, a lock that's never released — wedges its job forever: the overlap guard sees
runningstaytrueand skips every subsequent tick, and anytrigger()call on that job hangs itsawaitindefinitely. There is no timeout built in. Recovery isremove()+add()(see the per-registration guard note above — the new registration is not blocked by the old one's hang). If an action can genuinely take unbounded time (a remote call with no deadline), wrap it with your own timeout so a stuck dependency can't wedge the job.
Jobs registered while a tick is being evaluated wait for the next minute.
Every run receives a CronusRunContext:
| Field | Meaning |
|---|---|
runId |
Unique id for THIS run — a fresh UUID per firing |
name |
The job's registered name |
scheduledAt |
The minute boundary the run fired for (call time on trigger) |
runCount |
Runs STARTED since registration (failed runs and triggers count) |
triggered |
true when started via trigger()
|
Metadata only — never the action's arguments. Listeners are
isolated (inherited from @tundralibs/utils Events): a sync throw
or an async rejection is caught per listener and reported via
console.error — it never affects the run, the job's state, other
listeners, the ticker, or the process. off(event, callback) also
removes once listeners by their original callback. A listener that
re-triggers its own job during success/error/finish hits the
overlap guard (resolves false) — the guard releases only after the
emissions.
| Event | Fires when | Arguments |
|---|---|---|
run |
a run starts | (runId, name, scheduledAt) |
success |
a run returns | (runId, name, scheduledAt, elapsedMs, result) |
error |
a run throws (never escapes) | (runId, name, scheduledAt, elapsedMs, CronusError) |
finish |
a run settles (success or error) | (runId, name, scheduledAt, elapsedMs) |
skip |
a matching tick hit a running job | (name, scheduledAt) |
An action that throws is routed to the error event as a
CronusError (foreign errors are wrapped, the original preserved as
cause) and then finish fires. The throw never escapes the run: a
job that fails every single tick cannot stop the ticker or affect
other jobs. trigger() follows the same rule — it resolves normally
even when the action threw; subscribe to error for the failure.
add(name: string, schedule: string, action: CronusAction, options?: CronusJobOptions): thisParameters:
-
name- Unique job name. -
schedule- A 5-field cron expression (syntax). -
action- Sync or async function; receives aCronusRunContext. -
options-{ once?: boolean; enabled?: boolean }(defaults:false,true).
Throws:
-
DuplicateJobError- Whennameis already registered. -
InvalidScheduleError- Whenscheduleis malformed. -
InvalidActionError- Whenactionis not a function.
add(..., { once: true }) — runs once at the next matching minute,
then auto-removes. Same throws as add().
Manage and inspect registrations. get()/list() return snapshots
(CronusJobInfo) — mutating one never affects the scheduler.
lastRun is when the most recent run started (not completed);
runCount counts runs started, including failures and triggers.
remove()/get() throw JobNotFoundError for unknown names. A
removed job's in-flight run is not interrupted, and its completion
cannot touch a job registered later under the same name.
import { Cronus } from '@tundralibs/cronus';
const cron = new Cronus();
cron.add('report', '0 6 * * *', () => {});
console.assert(cron.has('report') === true);
console.assert(cron.get('report').runCount === 0); // no run has started yet
console.assert(cron.list().length === 1);
cron.remove('report');
console.assert(cron.has('report') === false);Toggle or inspect a job by name. Disabled jobs are skipped by the
ticker but remain runnable via trigger(). All throw
JobNotFoundError for unknown names.
import { Cronus } from '@tundralibs/cronus';
const cron = new Cronus();
cron.add('beta-sync', '*/5 * * * *', () => {});
console.assert(cron.get('beta-sync').enabled === true); // enabled by default
cron.disable('beta-sync'); // the ticker now skips it — trigger() still works
console.assert(cron.get('beta-sync').enabled === false);
console.assert(cron.isRunning('beta-sync') === false); // no run currently in flight
cron.enable('beta-sync');Start aligns to the next minute boundary and is idempotent; a job is
never fired for the minute in which start() was called. Minutes
missed while stopped, blocked, or suspended are not replayed (classic
cron behaviour — no catch-up). Stop clears the pending tick but does
not cancel a run already in flight; registrations survive and
start() resumes.
import { Cronus } from '@tundralibs/cronus';
const cron = new Cronus();
cron.add('report', '0 6 * * *', () => {});
console.assert(cron.active === false); // the ticker has not started
cron.start();
console.assert(cron.active === true);
cron.start(); // idempotent — a no-op while already active
cron.stop(); // clears the pending tick; a run already in flight is not cancelled
console.assert(cron.active === false);trigger(name: string): Promise<boolean>Run now, bypassing the schedule. Resolves false when the job is
already running (overlap guard), true after the run settles.
Throws:
-
JobNotFoundError- Whennameis not registered.
- Cronus-Schedule-Syntax - Expression syntax and matching semantics
- Cronus-Errors - The typed error hierarchy