@warlock.js/scheduler 5.29.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +2 -4
- package/cjs/index.cjs +4 -4
- package/cjs/index.cjs.map +1 -1
- package/esm/cron-parser.d.mts +1 -1
- package/esm/cron-parser.d.mts.map +1 -1
- package/esm/cron-parser.mjs +1 -1
- package/esm/cron-parser.mjs.map +1 -1
- package/esm/job.d.mts +1 -1
- package/esm/job.d.mts.map +1 -1
- package/esm/job.mjs +1 -1
- package/esm/job.mjs.map +1 -1
- package/esm/process-roles.mjs +1 -1
- package/esm/process-roles.mjs.map +1 -1
- package/esm/scheduler.d.mts +1 -1
- package/esm/scheduler.d.mts.map +1 -1
- package/esm/scheduler.mjs +1 -1
- package/esm/scheduler.mjs.map +1 -1
- package/esm/types.d.mts +1 -1
- package/esm/types.d.mts.map +1 -1
- package/llms-full.txt +128 -143
- package/package.json +4 -3
- package/skills/configure-retry-and-overlap/SKILL.md +12 -22
- package/skills/index.md +4 -1
- package/skills/observe-scheduler/SKILL.md +31 -30
- package/skills/overview/SKILL.md +6 -0
- package/skills/pin-schedule-timezone/SKILL.md +21 -30
- package/skills/schedule-fluently/SKILL.md +40 -43
- package/skills/schedule-with-cron/SKILL.md +12 -12
- package/skills/scheduler-basics/SKILL.md +8 -8
package/skills/index.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: 'In-process job scheduler for Warlock.js with fluent and cron scheduling, retries, overlap prevention and timezones. Exports `Scheduler`, `scheduler`, `Job`, `job`, `CronParser`, `parseCron`, plus types `RetryConfig`, `JobResult`, `SchedulerEvents`. Use for: "run this every day at 3am", "run a cron expression on weekdays", "retry a failing job 3 times", "stop a job overlapping itself", "run in New York time", "log job failures". Not this package: durable queues and background workers are not scheduling; per-run locking across instances uses @warlock.js/cache.'
|
|
3
3
|
---
|
|
4
|
+
|
|
4
5
|
# @warlock.js/scheduler
|
|
5
6
|
|
|
6
7
|
Build a `job(name, fn)`, describe when it runs with the fluent API (`.daily().at("03:00")`) or `.cron("...")`, add it to a `Scheduler`, then `start()`. Events (`job:error` and others) provide observability; `shutdown()` stops gracefully.
|
|
7
8
|
|
|
8
9
|
## The 80% path
|
|
10
|
+
|
|
9
11
|
1. Orient: `overview.md`, `scheduler-basics.md`.
|
|
10
12
|
2. Define the schedule fluently: `schedule-fluently.md`, or by cron: `schedule-with-cron.md`.
|
|
11
13
|
3. Make it resilient: `configure-retry-and-overlap.md`.
|
|
@@ -13,6 +15,7 @@ Build a `job(name, fn)`, describe when it runs with the fluent API (`.daily().at
|
|
|
13
15
|
5. Watch it run: `observe-scheduler.md`.
|
|
14
16
|
|
|
15
17
|
## Conventions and pitfalls
|
|
18
|
+
|
|
16
19
|
- It is in-memory and per-process: each app instance runs its own copy. With multiple instances, guard jobs with a lock (for example `@warlock.js/cache`) or run the scheduler in one instance only.
|
|
17
20
|
- Missed runs while the process was down are not replayed.
|
|
18
21
|
- Set a timezone explicitly for jobs tied to business hours; otherwise the host's applies.
|
|
@@ -11,25 +11,25 @@ The `Scheduler` extends Node's `EventEmitter` with a fully-typed surface. Wire l
|
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
13
|
type SchedulerEvents = {
|
|
14
|
-
"job:start":
|
|
15
|
-
"job:complete":
|
|
16
|
-
"job:error":
|
|
17
|
-
"job:skip":
|
|
14
|
+
"job:start": [jobName: string];
|
|
15
|
+
"job:complete": [jobName: string, result: JobResult];
|
|
16
|
+
"job:error": [jobName: string, error: unknown];
|
|
17
|
+
"job:skip": [jobName: string, reason: string];
|
|
18
18
|
"scheduler:started": [];
|
|
19
19
|
"scheduler:stopped": [];
|
|
20
|
-
"scheduler:tick":
|
|
20
|
+
"scheduler:tick": [timestamp: Date];
|
|
21
21
|
};
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
| Event
|
|
25
|
-
|
|
|
26
|
-
| `scheduler:started`
|
|
27
|
-
| `scheduler:stopped`
|
|
28
|
-
| `scheduler:tick`
|
|
29
|
-
| `job:start`
|
|
30
|
-
| `job:complete`
|
|
31
|
-
| `job:error`
|
|
32
|
-
| `job:skip`
|
|
24
|
+
| Event | When it fires |
|
|
25
|
+
| ------------------- | -------------------------------------------------------------------------------------- |
|
|
26
|
+
| `scheduler:started` | Once, after `.start()` enters the tick loop |
|
|
27
|
+
| `scheduler:stopped` | When `.stop()` halts the loop. Not emitted if `.stop()` was a no-op (never started) |
|
|
28
|
+
| `scheduler:tick` | Every tick — once per `runEvery()` interval (default 1 s). Keep handlers lightweight. |
|
|
29
|
+
| `job:start` | Just before a job's callback is first invoked (NOT once per retry) |
|
|
30
|
+
| `job:complete` | When a job finishes successfully (possibly after retries) |
|
|
31
|
+
| `job:error` | When a job exhausts all retries and the final attempt still throws |
|
|
32
|
+
| `job:skip` | When a tick finds the job already running (see retry-and-overlap for when this occurs) |
|
|
33
33
|
|
|
34
34
|
**Contract for retries.** `job:start` and `job:complete`/`job:error` fire **once per fire**, not once per retry attempt. The retry count surfaces inside the `JobResult` passed to `job:complete`, or as `result.retries === maxRetries` on the failure path.
|
|
35
35
|
|
|
@@ -38,9 +38,9 @@ type SchedulerEvents = {
|
|
|
38
38
|
```ts
|
|
39
39
|
type JobResult = {
|
|
40
40
|
success: boolean;
|
|
41
|
-
duration: number;
|
|
42
|
-
error?: unknown;
|
|
43
|
-
retries?: number;
|
|
41
|
+
duration: number; // milliseconds, end-to-end including retry waits
|
|
42
|
+
error?: unknown; // present when success === false
|
|
43
|
+
retries?: number; // attempts that failed before the final outcome
|
|
44
44
|
};
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -52,12 +52,12 @@ type JobResult = {
|
|
|
52
52
|
import type { JobResult } from "@warlock.js/scheduler";
|
|
53
53
|
import { scheduler } from "@warlock.js/scheduler";
|
|
54
54
|
|
|
55
|
-
scheduler.on("job:start",
|
|
55
|
+
scheduler.on("job:start", (name) => log.debug({ name }, "job start"));
|
|
56
56
|
scheduler.on("job:complete", (name, result: JobResult) => {
|
|
57
57
|
log.info({ name, duration: result.duration, retries: result.retries }, "job complete");
|
|
58
58
|
});
|
|
59
|
-
scheduler.on("job:error",
|
|
60
|
-
scheduler.on("job:skip",
|
|
59
|
+
scheduler.on("job:error", (name, error) => log.error({ name, error }, "job failed"));
|
|
60
|
+
scheduler.on("job:skip", (name, reason) => log.warn({ name, reason }, "job skipped"));
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
`.on()`, `.once()`, `.off()` are all type-narrowed via `SchedulerEvents`.
|
|
@@ -69,6 +69,7 @@ scheduler.on("job:skip", (name, reason) => log.warn({ name, reason }, "job s
|
|
|
69
69
|
Prepares every registered job (computes initial `nextRun`) and enters the tick loop. Emits `scheduler:started`.
|
|
70
70
|
|
|
71
71
|
Throws if:
|
|
72
|
+
|
|
72
73
|
- already running (`"Scheduler is already running."`)
|
|
73
74
|
- zero jobs registered (`"Cannot start scheduler with no jobs."`)
|
|
74
75
|
|
|
@@ -104,7 +105,7 @@ import { logger } from "./logger";
|
|
|
104
105
|
import { metrics } from "./metrics";
|
|
105
106
|
import { alerts } from "./alerts";
|
|
106
107
|
|
|
107
|
-
scheduler.on("job:start", name => metrics.increment(`job.start.${name}`));
|
|
108
|
+
scheduler.on("job:start", (name) => metrics.increment(`job.start.${name}`));
|
|
108
109
|
|
|
109
110
|
scheduler.on("job:complete", (name, result) => {
|
|
110
111
|
metrics.increment(`job.success.${name}`);
|
|
@@ -131,20 +132,20 @@ scheduler.start();
|
|
|
131
132
|
## Inspection at runtime
|
|
132
133
|
|
|
133
134
|
```ts
|
|
134
|
-
scheduler.isRunning;
|
|
135
|
-
scheduler.jobCount;
|
|
136
|
-
scheduler.list();
|
|
137
|
-
scheduler.getJob("name");
|
|
135
|
+
scheduler.isRunning; // boolean
|
|
136
|
+
scheduler.jobCount; // number of registered jobs
|
|
137
|
+
scheduler.list(); // readonly Job[]
|
|
138
|
+
scheduler.getJob("name"); // Job | undefined
|
|
138
139
|
```
|
|
139
140
|
|
|
140
141
|
And on each `Job`:
|
|
141
142
|
|
|
142
143
|
```ts
|
|
143
|
-
job.nextRun;
|
|
144
|
-
job.lastRun;
|
|
145
|
-
job.isRunning;
|
|
146
|
-
job.intervals;
|
|
147
|
-
job.cronExpression;
|
|
144
|
+
job.nextRun; // Dayjs | null
|
|
145
|
+
job.lastRun; // Dayjs | null — success OR failure
|
|
146
|
+
job.isRunning; // boolean
|
|
147
|
+
job.intervals; // readonly schedule config
|
|
148
|
+
job.cronExpression; // string | null
|
|
148
149
|
```
|
|
149
150
|
|
|
150
151
|
## See also
|
package/skills/overview/SKILL.md
CHANGED
|
@@ -30,27 +30,33 @@ Six task skills cover the full surface. Most callers only need `scheduler-basics
|
|
|
30
30
|
### Foundations
|
|
31
31
|
|
|
32
32
|
#### `scheduler-basics`
|
|
33
|
+
|
|
33
34
|
Start here. The `job(name, fn)` factory, the `scheduler` singleton, the 2-primitive surface, UTC default, why the API is factory-first.
|
|
34
35
|
|
|
35
36
|
### Scheduling
|
|
36
37
|
|
|
37
38
|
#### `schedule-fluently`
|
|
39
|
+
|
|
38
40
|
Build schedules without writing cron: `.everyMinutes(N)`, `.daily().at("03:00")`, `.weekly().on("monday")`, `.monthly()`, `.beginOf("month")`, `.endOf("year")`, and friends. Reach for this 80% of the time.
|
|
39
41
|
|
|
40
42
|
#### `schedule-with-cron`
|
|
43
|
+
|
|
41
44
|
Drop down to `.cron("0 9 * * 1-5")` when the fluent API can't express it (Vixie OR semantics for DOM/DOW, complex multi-value lists). Includes `parseCron()` for previewing next-run times before deploy.
|
|
42
45
|
|
|
43
46
|
### Production concerns
|
|
44
47
|
|
|
45
48
|
#### `configure-retry-and-overlap`
|
|
49
|
+
|
|
46
50
|
`.retry(maxRetries, delay?, backoffMultiplier?)` for fixed or exponential backoff; `.preventOverlap()` so a slow run never collides with the next tick. Reach for both anytime a job calls external services.
|
|
47
51
|
|
|
48
52
|
#### `pin-schedule-timezone`
|
|
53
|
+
|
|
49
54
|
`.inTimezone("America/New_York")` pins wall-clock fire times across DST and across servers. The default is UTC — if your job description includes "9 AM," you almost certainly want this skill.
|
|
50
55
|
|
|
51
56
|
### Observability + shutdown
|
|
52
57
|
|
|
53
58
|
#### `observe-scheduler`
|
|
59
|
+
|
|
54
60
|
The seven typed `SchedulerEvents` (`job:start` / `complete` / `error` / `skip`, `scheduler:started` / `stopped` / `tick`), the `JobResult` payload, `start()` / `stop()` / `shutdown()`. Wire this for logging, metrics, alerting, and graceful SIGTERM handling.
|
|
55
61
|
|
|
56
62
|
## Quick taste
|
|
@@ -10,10 +10,7 @@ Every `Job` has its own timezone. The default is **UTC** — `.daily().at("09:00
|
|
|
10
10
|
## Basic usage
|
|
11
11
|
|
|
12
12
|
```ts
|
|
13
|
-
job("morning-digest", sendDailyDigest)
|
|
14
|
-
.daily()
|
|
15
|
-
.at("08:00")
|
|
16
|
-
.inTimezone("America/New_York"); // 8 AM ET, not 8 AM UTC
|
|
13
|
+
job("morning-digest", sendDailyDigest).daily().at("08:00").inTimezone("America/New_York"); // 8 AM ET, not 8 AM UTC
|
|
17
14
|
```
|
|
18
15
|
|
|
19
16
|
`.inTimezone()` is chainable — typically placed after the time/day methods.
|
|
@@ -30,9 +27,9 @@ Same logical task, three regions:
|
|
|
30
27
|
import { scheduler, job } from "@warlock.js/scheduler";
|
|
31
28
|
|
|
32
29
|
const regions = [
|
|
33
|
-
{ name: "us-east",
|
|
34
|
-
{ name: "eu-west",
|
|
35
|
-
{ name: "asia",
|
|
30
|
+
{ name: "us-east", tz: "America/New_York" },
|
|
31
|
+
{ name: "eu-west", tz: "Europe/Berlin" },
|
|
32
|
+
{ name: "asia", tz: "Asia/Tokyo" },
|
|
36
33
|
];
|
|
37
34
|
|
|
38
35
|
for (const { name, tz } of regions) {
|
|
@@ -40,7 +37,7 @@ for (const { name, tz } of regions) {
|
|
|
40
37
|
job(`morning-report-${name}`, () => sendReport(name))
|
|
41
38
|
.daily()
|
|
42
39
|
.at("09:00")
|
|
43
|
-
.inTimezone(tz)
|
|
40
|
+
.inTimezone(tz),
|
|
44
41
|
);
|
|
45
42
|
}
|
|
46
43
|
|
|
@@ -55,20 +52,20 @@ dayjs handles transitions correctly. A daily 09:00 job in `America/New_York` shi
|
|
|
55
52
|
|
|
56
53
|
## Common IANA strings
|
|
57
54
|
|
|
58
|
-
| Region
|
|
59
|
-
|
|
|
60
|
-
| UTC
|
|
61
|
-
| US Eastern
|
|
62
|
-
| US Central
|
|
63
|
-
| US Mountain
|
|
64
|
-
| US Pacific
|
|
65
|
-
| UK / Ireland
|
|
66
|
-
| Central Europe
|
|
67
|
-
| Eastern Europe
|
|
68
|
-
| India
|
|
69
|
-
| Japan
|
|
70
|
-
| Australia (Sydney)
|
|
71
|
-
| Egypt
|
|
55
|
+
| Region | TZ string |
|
|
56
|
+
| ------------------ | --------------------- |
|
|
57
|
+
| UTC | `UTC` |
|
|
58
|
+
| US Eastern | `America/New_York` |
|
|
59
|
+
| US Central | `America/Chicago` |
|
|
60
|
+
| US Mountain | `America/Denver` |
|
|
61
|
+
| US Pacific | `America/Los_Angeles` |
|
|
62
|
+
| UK / Ireland | `Europe/London` |
|
|
63
|
+
| Central Europe | `Europe/Berlin` |
|
|
64
|
+
| Eastern Europe | `Europe/Kyiv` |
|
|
65
|
+
| India | `Asia/Kolkata` |
|
|
66
|
+
| Japan | `Asia/Tokyo` |
|
|
67
|
+
| Australia (Sydney) | `Australia/Sydney` |
|
|
68
|
+
| Egypt | `Africa/Cairo` |
|
|
72
69
|
|
|
73
70
|
Full list: [IANA Time Zone Database](https://www.iana.org/time-zones).
|
|
74
71
|
|
|
@@ -77,17 +74,11 @@ Full list: [IANA Time Zone Database](https://www.iana.org/time-zones).
|
|
|
77
74
|
The timezone applies to every interpretation of "now" and every constraint:
|
|
78
75
|
|
|
79
76
|
```ts
|
|
80
|
-
job("monday-standup", task)
|
|
81
|
-
.weekly()
|
|
82
|
-
.on("monday")
|
|
83
|
-
.at("09:00")
|
|
84
|
-
.inTimezone("Europe/Berlin");
|
|
77
|
+
job("monday-standup", task).weekly().on("monday").at("09:00").inTimezone("Europe/Berlin");
|
|
85
78
|
// "Monday in Berlin local time" + "09:00 Berlin local time"
|
|
86
79
|
// — automatically shifts CET ↔ CEST as DST changes.
|
|
87
80
|
|
|
88
|
-
job("nightly-cron", task)
|
|
89
|
-
.cron("0 3 * * *")
|
|
90
|
-
.inTimezone("Asia/Tokyo");
|
|
81
|
+
job("nightly-cron", task).cron("0 3 * * *").inTimezone("Asia/Tokyo");
|
|
91
82
|
// "0 3 * * *" interpreted in Tokyo local time → 03:00 JST = 18:00 UTC.
|
|
92
83
|
```
|
|
93
84
|
|
|
@@ -9,24 +9,24 @@ Chainable methods on a `Job` instance. Each method returns `this` for chaining;
|
|
|
9
9
|
|
|
10
10
|
## Preset intervals
|
|
11
11
|
|
|
12
|
-
| Method
|
|
13
|
-
|
|
|
14
|
-
| `.everySecond()`
|
|
15
|
-
| `.everySeconds(N)`
|
|
16
|
-
| `.everyMinute()`
|
|
17
|
-
| `.everyMinutes(N)`
|
|
18
|
-
| `.everyHour()`
|
|
19
|
-
| `.everyHours(N)`
|
|
20
|
-
| `.everyDay()`
|
|
21
|
-
| `.daily()`
|
|
22
|
-
| `.twiceDaily()`
|
|
23
|
-
| `.everyWeek()`
|
|
24
|
-
| `.weekly()`
|
|
25
|
-
| `.everyMonth()`
|
|
26
|
-
| `.monthly()`
|
|
27
|
-
| `.everyYear()`
|
|
28
|
-
| `.yearly()`
|
|
29
|
-
| `.always()`
|
|
12
|
+
| Method | Equivalent of | Notes |
|
|
13
|
+
| ------------------ | --------------------- | ------------------------------ |
|
|
14
|
+
| `.everySecond()` | `.every(1, "second")` | High frequency — use sparingly |
|
|
15
|
+
| `.everySeconds(N)` | `.every(N, "second")` | |
|
|
16
|
+
| `.everyMinute()` | `.every(1, "minute")` | |
|
|
17
|
+
| `.everyMinutes(N)` | `.every(N, "minute")` | |
|
|
18
|
+
| `.everyHour()` | `.every(1, "hour")` | |
|
|
19
|
+
| `.everyHours(N)` | `.every(N, "hour")` | |
|
|
20
|
+
| `.everyDay()` | `.every(1, "day")` | Same as `.daily()` |
|
|
21
|
+
| `.daily()` | `.every(1, "day")` | |
|
|
22
|
+
| `.twiceDaily()` | `.every(12, "hour")` | Every 12 hours |
|
|
23
|
+
| `.everyWeek()` | `.every(1, "week")` | Same as `.weekly()` |
|
|
24
|
+
| `.weekly()` | `.every(1, "week")` | |
|
|
25
|
+
| `.everyMonth()` | `.every(1, "month")` | Same as `.monthly()` |
|
|
26
|
+
| `.monthly()` | `.every(1, "month")` | |
|
|
27
|
+
| `.everyYear()` | `.every(1, "year")` | Same as `.yearly()` |
|
|
28
|
+
| `.yearly()` | `.every(1, "year")` | |
|
|
29
|
+
| `.always()` | `.every(1, "minute")` | Continuous "tick" jobs |
|
|
30
30
|
|
|
31
31
|
## Custom intervals — `.every(value, unit)`
|
|
32
32
|
|
|
@@ -44,7 +44,7 @@ Units: `"second" | "minute" | "hour" | "day" | "week" | "month" | "year"`.
|
|
|
44
44
|
|
|
45
45
|
```ts
|
|
46
46
|
job("nightly", fn).daily().at("03:00");
|
|
47
|
-
job("midday",
|
|
47
|
+
job("midday", fn).daily().at("12:30:15");
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
**Validation.** `.at()` throws if the format is malformed (`"foo"`, `"9-30"`), or any component is out of range (hour > 23, minute > 59, second > 59).
|
|
@@ -56,7 +56,7 @@ Day-of-week (string) or day-of-month (number 1–31):
|
|
|
56
56
|
```ts
|
|
57
57
|
job("monday-standup", task).weekly().on("monday");
|
|
58
58
|
job("mid-month-sync", task).monthly().on(15);
|
|
59
|
-
job("end-of-month",
|
|
59
|
+
job("end-of-month", task).monthly().on(31); // see beginOf/endOf for the right way
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Valid day-of-week strings: `"sunday"`, `"monday"`, `"tuesday"`, `"wednesday"`, `"thursday"`, `"friday"`, `"saturday"`.
|
|
@@ -69,14 +69,14 @@ Valid day-of-week strings: `"sunday"`, `"monday"`, `"tuesday"`, `"wednesday"`, `
|
|
|
69
69
|
|
|
70
70
|
Both accept `"day" | "month" | "year"`.
|
|
71
71
|
|
|
72
|
-
| Method
|
|
73
|
-
|
|
|
74
|
-
| `beginOf("day")`
|
|
75
|
-
| `endOf("day")`
|
|
76
|
-
| `beginOf("month")`
|
|
77
|
-
| `endOf("month")`
|
|
78
|
-
| `beginOf("year")`
|
|
79
|
-
| `endOf("year")`
|
|
72
|
+
| Method | Fires at |
|
|
73
|
+
| ------------------ | ---------------------------------------------- |
|
|
74
|
+
| `beginOf("day")` | 00:00 every day |
|
|
75
|
+
| `endOf("day")` | 23:59 every day |
|
|
76
|
+
| `beginOf("month")` | 1st of every month at 00:00 |
|
|
77
|
+
| `endOf("month")` | Last day of every month at 23:59 (**dynamic**) |
|
|
78
|
+
| `beginOf("year")` | January 1 at 00:00 every year |
|
|
79
|
+
| `endOf("year")` | December 31 at 23:59 every year |
|
|
80
80
|
|
|
81
81
|
**`endOf("month")` is dynamic** — recomputes per cycle, so a job defined in February (28 days) still fires on March 31, April 30, etc. Leap years pick Feb 29 correctly.
|
|
82
82
|
|
|
@@ -92,12 +92,12 @@ Every fluent method recomputes `nextRun` at the end. Putting `.at()` before `.da
|
|
|
92
92
|
|
|
93
93
|
```ts
|
|
94
94
|
job("good", fn)
|
|
95
|
-
.weekly()
|
|
96
|
-
.on("monday")
|
|
97
|
-
.at("09:00")
|
|
98
|
-
.inTimezone("UTC")
|
|
99
|
-
.preventOverlap()
|
|
100
|
-
.retry(3, 1000);
|
|
95
|
+
.weekly() // interval
|
|
96
|
+
.on("monday") // day
|
|
97
|
+
.at("09:00") // time
|
|
98
|
+
.inTimezone("UTC") // timezone
|
|
99
|
+
.preventOverlap() // execution
|
|
100
|
+
.retry(3, 1000); // execution
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
## Inline registration — `scheduler.newJob()`
|
|
@@ -105,10 +105,7 @@ job("good", fn)
|
|
|
105
105
|
For one-liners, the scheduler exposes a `newJob()` shortcut that creates, registers, and returns the job in one call:
|
|
106
106
|
|
|
107
107
|
```ts
|
|
108
|
-
scheduler
|
|
109
|
-
.newJob("cleanup", cleanupFn)
|
|
110
|
-
.daily()
|
|
111
|
-
.at("03:00");
|
|
108
|
+
scheduler.newJob("cleanup", cleanupFn).daily().at("03:00");
|
|
112
109
|
```
|
|
113
110
|
|
|
114
111
|
To register several pre-built jobs at once, `scheduler.addJobs([...])` is the batch counterpart to `addJob` (both are chainable and preserve insertion order):
|
|
@@ -127,11 +124,11 @@ If the scheduler is already running, every job added via `addJob` / `addJobs` is
|
|
|
127
124
|
```ts
|
|
128
125
|
const j = scheduler.getJob("nightly-cleanup");
|
|
129
126
|
|
|
130
|
-
j?.nextRun?.toISOString();
|
|
131
|
-
j?.lastRun?.toISOString();
|
|
132
|
-
j?.isRunning;
|
|
133
|
-
j?.intervals;
|
|
134
|
-
j?.cronExpression;
|
|
127
|
+
j?.nextRun?.toISOString(); // next scheduled run (Dayjs)
|
|
128
|
+
j?.lastRun?.toISOString(); // last attempt — success OR failure
|
|
129
|
+
j?.isRunning; // currently executing?
|
|
130
|
+
j?.intervals; // { every?, day?, dayOfMonthMode?, month?, time? } (readonly)
|
|
131
|
+
j?.cronExpression; // null if using fluent API
|
|
135
132
|
```
|
|
136
133
|
|
|
137
134
|
## See also
|
|
@@ -23,14 +23,14 @@ The escape hatch when the fluent API can't express a schedule. Activating `.cron
|
|
|
23
23
|
|
|
24
24
|
## Operators
|
|
25
25
|
|
|
26
|
-
| Syntax
|
|
27
|
-
|
|
|
28
|
-
| `*`
|
|
29
|
-
| `5`
|
|
30
|
-
| `1,3,5`
|
|
31
|
-
| `1-5`
|
|
32
|
-
| `*/N`
|
|
33
|
-
| `A-B/N`
|
|
26
|
+
| Syntax | Meaning | Example |
|
|
27
|
+
| ------- | ---------------------- | ---------------- |
|
|
28
|
+
| `*` | Any value (full range) | `* * * * *` |
|
|
29
|
+
| `5` | Single value | `30 14 * * *` |
|
|
30
|
+
| `1,3,5` | List | `0,30 * * * *` |
|
|
31
|
+
| `1-5` | Inclusive range | `0 9-17 * * *` |
|
|
32
|
+
| `*/N` | Step over the wildcard | `*/5 * * * *` |
|
|
33
|
+
| `A-B/N` | Step over a range | `0 1-10/2 * * *` |
|
|
34
34
|
|
|
35
35
|
**Not supported (yet):** named tokens (`MON-FRI`, `JAN-DEC`), special strings (`@daily`, `@hourly`, `@reboot`), seconds field (6-field), Quartz modifiers (`L`, `W`, `#`).
|
|
36
36
|
|
|
@@ -102,13 +102,13 @@ import dayjs from "dayjs";
|
|
|
102
102
|
|
|
103
103
|
const parser = parseCron("0 9 * * 1-5");
|
|
104
104
|
|
|
105
|
-
parser.nextRun().toISOString();
|
|
105
|
+
parser.nextRun().toISOString(); // next weekday at 9 AM
|
|
106
106
|
parser.nextRun(dayjs("2026-12-31")).format(); // from a specific anchor
|
|
107
107
|
|
|
108
|
-
parser.matches(dayjs());
|
|
108
|
+
parser.matches(dayjs()); // is "now" a fire moment?
|
|
109
109
|
|
|
110
|
-
parser.fields;
|
|
111
|
-
parser.expression;
|
|
110
|
+
parser.fields; // parsed numeric arrays
|
|
111
|
+
parser.expression; // original string
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
Impossible expressions are rejected eagerly at construction (see Validation), so `nextRun(from?)` always resolves a satisfiable expression within a year — its one-year scan bound is just a defensive backstop, never the path that catches a bad expression.
|
|
@@ -36,7 +36,7 @@ The 9 things that are true in every scheduler use:
|
|
|
36
36
|
6. **Validation throws at definition time.** `every(0)`, `at("24:00")`, `on(32)`, `retry(-1)`, malformed cron — all throw immediately when you wire the job, not at runtime.
|
|
37
37
|
7. **`Job.run()` returns a `JobResult`, never throws.** Errors funnel into `result.error`. The scheduler emits `job:error` after all retries are exhausted.
|
|
38
38
|
8. **Parallel mode is within a tick, not across.** Even with `runInParallel(true)`, the NEXT tick waits for the current tick's jobs to all settle.
|
|
39
|
-
9. **Tick interval is drift-compensated.** A tick that takes 200 ms is followed by an 800 ms delay so the cadence between tick
|
|
39
|
+
9. **Tick interval is drift-compensated.** A tick that takes 200 ms is followed by an 800 ms delay so the cadence between tick _starts_ averages `tickInterval`, not `tickInterval + work-time`.
|
|
40
40
|
|
|
41
41
|
## 30-second example
|
|
42
42
|
|
|
@@ -53,7 +53,7 @@ scheduler.addJob(
|
|
|
53
53
|
.at("03:00")
|
|
54
54
|
.inTimezone("America/New_York")
|
|
55
55
|
.preventOverlap()
|
|
56
|
-
.retry(3, 1000)
|
|
56
|
+
.retry(3, 1000),
|
|
57
57
|
);
|
|
58
58
|
|
|
59
59
|
scheduler.start();
|
|
@@ -79,13 +79,13 @@ scheduler.around((job, run) => storage.run({ job: job.name }, run));
|
|
|
79
79
|
|
|
80
80
|
## Pick a skill
|
|
81
81
|
|
|
82
|
-
| If the task is about…
|
|
83
|
-
|
|
|
84
|
-
| Building schedules via `every*`/`daily`/`weekly`/`monthly`/`at`/`on`/`beginOf`/`endOf`
|
|
85
|
-
| Writing `.cron("…")` expressions, debugging DOM/DOW behavior, using `parseCron()` for preview
|
|
82
|
+
| If the task is about… | Load |
|
|
83
|
+
| ----------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
|
84
|
+
| Building schedules via `every*`/`daily`/`weekly`/`monthly`/`at`/`on`/`beginOf`/`endOf` | the `schedule-fluently` topic |
|
|
85
|
+
| Writing `.cron("…")` expressions, debugging DOM/DOW behavior, using `parseCron()` for preview | the `schedule-with-cron` topic |
|
|
86
86
|
| Configuring `.retry()` / exponential backoff, `.preventOverlap()`, understanding failure rescheduling | the `configure-retry-and-overlap` topic |
|
|
87
|
-
| Subscribing to scheduler events, reading `JobResult`, lifecycle, graceful shutdown
|
|
88
|
-
| Per-job `.inTimezone()`, multi-region patterns, DST handling
|
|
87
|
+
| Subscribing to scheduler events, reading `JobResult`, lifecycle, graceful shutdown | the `observe-scheduler` topic |
|
|
88
|
+
| Per-job `.inTimezone()`, multi-region patterns, DST handling | the `pin-schedule-timezone` topic |
|
|
89
89
|
|
|
90
90
|
## When NOT to use this skill
|
|
91
91
|
|