@warlock.js/scheduler 5.24.0 → 5.25.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 +6 -0
- package/cjs/index.cjs +49 -3
- package/cjs/index.cjs.map +1 -1
- package/esm/process-roles.mjs +37 -0
- package/esm/process-roles.mjs.map +1 -0
- package/esm/scheduler.d.mts +9 -2
- package/esm/scheduler.d.mts.map +1 -1
- package/esm/scheduler.mjs +15 -3
- package/esm/scheduler.mjs.map +1 -1
- package/llms-full.txt +683 -683
- package/package.json +2 -2
- package/skills/configure-retry-and-overlap/SKILL.md +152 -152
- package/skills/observe-scheduler/SKILL.md +153 -153
- package/skills/pin-schedule-timezone/SKILL.md +114 -114
- package/skills/schedule-fluently/SKILL.md +141 -141
- package/skills/schedule-with-cron/SKILL.md +123 -123
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pin-schedule-timezone
|
|
3
|
-
description: 'Pin a job to a specific IANA timezone via `.inTimezone(zone)` so its wall-clock fire time stays correct regardless of server location / DST. Triggers: `.inTimezone`, `America/New_York`, `Europe/Berlin`, `Asia/Tokyo`, `NODE_ICU_DATA`; "schedule job in a timezone", "DST drift on non-UTC server", "multi-region fan-out scheduling", "fire at 9am ET regardless of server"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: fluent interval methods — `@warlock.js/scheduler/schedule-fluently/SKILL.md`; cron syntax — `@warlock.js/scheduler/schedule-with-cron/SKILL.md`; competing libs `dayjs-timezone`, `luxon`, `date-fns-tz`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Per-job timezones
|
|
7
|
-
|
|
8
|
-
Every `Job` has its own timezone. The default is **UTC** — `.daily().at("09:00")` fires at 09:00 UTC, regardless of the server's locale or system timezone. Pin to wall-clock time with `.inTimezone(IANA string)`.
|
|
9
|
-
|
|
10
|
-
## Basic usage
|
|
11
|
-
|
|
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
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
`.inTimezone()` is chainable — typically placed after the time/day methods.
|
|
20
|
-
|
|
21
|
-
## Why the default is UTC
|
|
22
|
-
|
|
23
|
-
A server runs wherever ops put it (Frankfurt, Virginia, GCP us-central1, …). System-local time is unpredictable; UTC is stable. The framework picks the stable default so that `daily().at("09:00")` without further config is reproducible across deployments. Pin to wall-clock time when business hours actually matter.
|
|
24
|
-
|
|
25
|
-
## Multi-region fan-out
|
|
26
|
-
|
|
27
|
-
Same logical task, three regions:
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import { scheduler, job } from "@warlock.js/scheduler";
|
|
31
|
-
|
|
32
|
-
const regions = [
|
|
33
|
-
{ name: "us-east", tz: "America/New_York" },
|
|
34
|
-
{ name: "eu-west", tz: "Europe/Berlin" },
|
|
35
|
-
{ name: "asia", tz: "Asia/Tokyo" },
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
for (const { name, tz } of regions) {
|
|
39
|
-
scheduler.addJob(
|
|
40
|
-
job(`morning-report-${name}`, () => sendReport(name))
|
|
41
|
-
.daily()
|
|
42
|
-
.at("09:00")
|
|
43
|
-
.inTimezone(tz)
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
scheduler.start();
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Three separate `Job` instances, three separate `nextRun` calculations. Each fires once per day at its region's 09:00.
|
|
51
|
-
|
|
52
|
-
## DST is automatic
|
|
53
|
-
|
|
54
|
-
dayjs handles transitions correctly. A daily 09:00 job in `America/New_York` shifts between 13:00 UTC (EST winter) and 14:00 UTC (EDT summer) without intervention. The same job in a fixed-offset region (`Asia/Tokyo`, `Africa/Algiers`) stays at a constant UTC offset.
|
|
55
|
-
|
|
56
|
-
## Common IANA strings
|
|
57
|
-
|
|
58
|
-
| Region | TZ string |
|
|
59
|
-
| ------------------- | --------------------------- |
|
|
60
|
-
| UTC | `UTC` |
|
|
61
|
-
| US Eastern | `America/New_York` |
|
|
62
|
-
| US Central | `America/Chicago` |
|
|
63
|
-
| US Mountain | `America/Denver` |
|
|
64
|
-
| US Pacific | `America/Los_Angeles` |
|
|
65
|
-
| UK / Ireland | `Europe/London` |
|
|
66
|
-
| Central Europe | `Europe/Berlin` |
|
|
67
|
-
| Eastern Europe | `Europe/Kyiv` |
|
|
68
|
-
| India | `Asia/Kolkata` |
|
|
69
|
-
| Japan | `Asia/Tokyo` |
|
|
70
|
-
| Australia (Sydney) | `Australia/Sydney` |
|
|
71
|
-
| Egypt | `Africa/Cairo` |
|
|
72
|
-
|
|
73
|
-
Full list: [IANA Time Zone Database](https://www.iana.org/time-zones).
|
|
74
|
-
|
|
75
|
-
## Interaction with `at()`, `on()`, cron
|
|
76
|
-
|
|
77
|
-
The timezone applies to every interpretation of "now" and every constraint:
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
job("monday-standup", task)
|
|
81
|
-
.weekly()
|
|
82
|
-
.on("monday")
|
|
83
|
-
.at("09:00")
|
|
84
|
-
.inTimezone("Europe/Berlin");
|
|
85
|
-
// "Monday in Berlin local time" + "09:00 Berlin local time"
|
|
86
|
-
// — automatically shifts CET ↔ CEST as DST changes.
|
|
87
|
-
|
|
88
|
-
job("nightly-cron", task)
|
|
89
|
-
.cron("0 3 * * *")
|
|
90
|
-
.inTimezone("Asia/Tokyo");
|
|
91
|
-
// "0 3 * * *" interpreted in Tokyo local time → 03:00 JST = 18:00 UTC.
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Validation
|
|
95
|
-
|
|
96
|
-
`.inTimezone()` stores the string as-is, then immediately recomputes `nextRun` — and that recompute calls dayjs with the zone. An invalid IANA string makes dayjs throw a `RangeError: Invalid time zone specified` **synchronously, at that point in the chain**:
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
// Throws right here — the .inTimezone() call recomputes nextRun, which
|
|
100
|
-
// hits dayjs().tz("Asia/Whatever") and raises RangeError immediately.
|
|
101
|
-
const j = job("t", task).daily().at("09:00").inTimezone("Asia/Whatever");
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
A valid zone computes `nextRun` on the spot (non-null after the chain). So a typo fails fast at definition time, not at the first tick — no special unit test needed to surface it.
|
|
105
|
-
|
|
106
|
-
## Server-side caveats
|
|
107
|
-
|
|
108
|
-
Node ships full ICU on most platforms, so all IANA zones work out of the box. If you're running a small-ICU build (alpine without full ICU, or old Node versions with `--icu=small`), only a limited set of zones is recognized. Set the `NODE_ICU_DATA` env var or use the `full-icu` package in that case.
|
|
109
|
-
|
|
110
|
-
## See also
|
|
111
|
-
|
|
112
|
-
- [`@warlock.js/scheduler/schedule-fluently/SKILL.md`](@warlock.js/scheduler/schedule-fluently/SKILL.md) — `at()`, `on()`, `daily()`, etc.
|
|
113
|
-
- [`@warlock.js/scheduler/schedule-with-cron/SKILL.md`](@warlock.js/scheduler/schedule-with-cron/SKILL.md) — cron schedules are timezone-aware too
|
|
114
|
-
- [`@warlock.js/scheduler/observe-scheduler/SKILL.md`](@warlock.js/scheduler/observe-scheduler/SKILL.md) — `job.nextRun.toISOString()` always renders in UTC; format with `.tz(zone)` to see local time
|
|
1
|
+
---
|
|
2
|
+
name: pin-schedule-timezone
|
|
3
|
+
description: 'Pin a job to a specific IANA timezone via `.inTimezone(zone)` so its wall-clock fire time stays correct regardless of server location / DST. Triggers: `.inTimezone`, `America/New_York`, `Europe/Berlin`, `Asia/Tokyo`, `NODE_ICU_DATA`; "schedule job in a timezone", "DST drift on non-UTC server", "multi-region fan-out scheduling", "fire at 9am ET regardless of server"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: fluent interval methods — `@warlock.js/scheduler/schedule-fluently/SKILL.md`; cron syntax — `@warlock.js/scheduler/schedule-with-cron/SKILL.md`; competing libs `dayjs-timezone`, `luxon`, `date-fns-tz`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Per-job timezones
|
|
7
|
+
|
|
8
|
+
Every `Job` has its own timezone. The default is **UTC** — `.daily().at("09:00")` fires at 09:00 UTC, regardless of the server's locale or system timezone. Pin to wall-clock time with `.inTimezone(IANA string)`.
|
|
9
|
+
|
|
10
|
+
## Basic usage
|
|
11
|
+
|
|
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
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`.inTimezone()` is chainable — typically placed after the time/day methods.
|
|
20
|
+
|
|
21
|
+
## Why the default is UTC
|
|
22
|
+
|
|
23
|
+
A server runs wherever ops put it (Frankfurt, Virginia, GCP us-central1, …). System-local time is unpredictable; UTC is stable. The framework picks the stable default so that `daily().at("09:00")` without further config is reproducible across deployments. Pin to wall-clock time when business hours actually matter.
|
|
24
|
+
|
|
25
|
+
## Multi-region fan-out
|
|
26
|
+
|
|
27
|
+
Same logical task, three regions:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { scheduler, job } from "@warlock.js/scheduler";
|
|
31
|
+
|
|
32
|
+
const regions = [
|
|
33
|
+
{ name: "us-east", tz: "America/New_York" },
|
|
34
|
+
{ name: "eu-west", tz: "Europe/Berlin" },
|
|
35
|
+
{ name: "asia", tz: "Asia/Tokyo" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
for (const { name, tz } of regions) {
|
|
39
|
+
scheduler.addJob(
|
|
40
|
+
job(`morning-report-${name}`, () => sendReport(name))
|
|
41
|
+
.daily()
|
|
42
|
+
.at("09:00")
|
|
43
|
+
.inTimezone(tz)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
scheduler.start();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Three separate `Job` instances, three separate `nextRun` calculations. Each fires once per day at its region's 09:00.
|
|
51
|
+
|
|
52
|
+
## DST is automatic
|
|
53
|
+
|
|
54
|
+
dayjs handles transitions correctly. A daily 09:00 job in `America/New_York` shifts between 13:00 UTC (EST winter) and 14:00 UTC (EDT summer) without intervention. The same job in a fixed-offset region (`Asia/Tokyo`, `Africa/Algiers`) stays at a constant UTC offset.
|
|
55
|
+
|
|
56
|
+
## Common IANA strings
|
|
57
|
+
|
|
58
|
+
| Region | TZ string |
|
|
59
|
+
| ------------------- | --------------------------- |
|
|
60
|
+
| UTC | `UTC` |
|
|
61
|
+
| US Eastern | `America/New_York` |
|
|
62
|
+
| US Central | `America/Chicago` |
|
|
63
|
+
| US Mountain | `America/Denver` |
|
|
64
|
+
| US Pacific | `America/Los_Angeles` |
|
|
65
|
+
| UK / Ireland | `Europe/London` |
|
|
66
|
+
| Central Europe | `Europe/Berlin` |
|
|
67
|
+
| Eastern Europe | `Europe/Kyiv` |
|
|
68
|
+
| India | `Asia/Kolkata` |
|
|
69
|
+
| Japan | `Asia/Tokyo` |
|
|
70
|
+
| Australia (Sydney) | `Australia/Sydney` |
|
|
71
|
+
| Egypt | `Africa/Cairo` |
|
|
72
|
+
|
|
73
|
+
Full list: [IANA Time Zone Database](https://www.iana.org/time-zones).
|
|
74
|
+
|
|
75
|
+
## Interaction with `at()`, `on()`, cron
|
|
76
|
+
|
|
77
|
+
The timezone applies to every interpretation of "now" and every constraint:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
job("monday-standup", task)
|
|
81
|
+
.weekly()
|
|
82
|
+
.on("monday")
|
|
83
|
+
.at("09:00")
|
|
84
|
+
.inTimezone("Europe/Berlin");
|
|
85
|
+
// "Monday in Berlin local time" + "09:00 Berlin local time"
|
|
86
|
+
// — automatically shifts CET ↔ CEST as DST changes.
|
|
87
|
+
|
|
88
|
+
job("nightly-cron", task)
|
|
89
|
+
.cron("0 3 * * *")
|
|
90
|
+
.inTimezone("Asia/Tokyo");
|
|
91
|
+
// "0 3 * * *" interpreted in Tokyo local time → 03:00 JST = 18:00 UTC.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Validation
|
|
95
|
+
|
|
96
|
+
`.inTimezone()` stores the string as-is, then immediately recomputes `nextRun` — and that recompute calls dayjs with the zone. An invalid IANA string makes dayjs throw a `RangeError: Invalid time zone specified` **synchronously, at that point in the chain**:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// Throws right here — the .inTimezone() call recomputes nextRun, which
|
|
100
|
+
// hits dayjs().tz("Asia/Whatever") and raises RangeError immediately.
|
|
101
|
+
const j = job("t", task).daily().at("09:00").inTimezone("Asia/Whatever");
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
A valid zone computes `nextRun` on the spot (non-null after the chain). So a typo fails fast at definition time, not at the first tick — no special unit test needed to surface it.
|
|
105
|
+
|
|
106
|
+
## Server-side caveats
|
|
107
|
+
|
|
108
|
+
Node ships full ICU on most platforms, so all IANA zones work out of the box. If you're running a small-ICU build (alpine without full ICU, or old Node versions with `--icu=small`), only a limited set of zones is recognized. Set the `NODE_ICU_DATA` env var or use the `full-icu` package in that case.
|
|
109
|
+
|
|
110
|
+
## See also
|
|
111
|
+
|
|
112
|
+
- [`@warlock.js/scheduler/schedule-fluently/SKILL.md`](@warlock.js/scheduler/schedule-fluently/SKILL.md) — `at()`, `on()`, `daily()`, etc.
|
|
113
|
+
- [`@warlock.js/scheduler/schedule-with-cron/SKILL.md`](@warlock.js/scheduler/schedule-with-cron/SKILL.md) — cron schedules are timezone-aware too
|
|
114
|
+
- [`@warlock.js/scheduler/observe-scheduler/SKILL.md`](@warlock.js/scheduler/observe-scheduler/SKILL.md) — `job.nextRun.toISOString()` always renders in UTC; format with `.tz(zone)` to see local time
|
|
@@ -1,141 +1,141 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: schedule-fluently
|
|
3
|
-
description: 'Build a job schedule via `every*` / `daily` / `weekly` / `monthly` / `at` / `on` / `beginOf` / `endOf`. Triggers: `.everyMinutes`, `.daily`, `.weekly`, `.monthly`, `.at`, `.on`, `.every`, `.beginOf`, `.endOf`, `.twiceDaily`, `scheduler.newJob`; "run every 5 minutes", "daily at 3am", "monday standup", "first of every month", "last day of month"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: cron — `@warlock.js/scheduler/schedule-with-cron/SKILL.md`; timezone — `@warlock.js/scheduler/pin-schedule-timezone/SKILL.md`; retry — `@warlock.js/scheduler/configure-retry-and-overlap/SKILL.md`; competing `node-cron`, `node-schedule`, `agenda`; native `setInterval`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Fluent scheduling API
|
|
7
|
-
|
|
8
|
-
Chainable methods on a `Job` instance. Each method returns `this` for chaining; each call recomputes `job.nextRun` on the spot.
|
|
9
|
-
|
|
10
|
-
## Preset intervals
|
|
11
|
-
|
|
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
|
-
|
|
31
|
-
## Custom intervals — `.every(value, unit)`
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
job("t", task).every(5, "minute");
|
|
35
|
-
job("t", task).every(2, "hour");
|
|
36
|
-
job("t", task).every(3, "day");
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Units: `"second" | "minute" | "hour" | "day" | "week" | "month" | "year"`.
|
|
40
|
-
|
|
41
|
-
**Validation.** `every(value, unit)` throws at definition time if `value` is `0`, negative, `NaN`, or `Infinity`. This guards against the misconfigured-interval class of bugs that would otherwise spin the scheduler.
|
|
42
|
-
|
|
43
|
-
## Target a specific time — `.at("HH:mm" | "HH:mm:ss")`
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
job("nightly", fn).daily().at("03:00");
|
|
47
|
-
job("midday", fn).daily().at("12:30:15");
|
|
48
|
-
```
|
|
49
|
-
|
|
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).
|
|
51
|
-
|
|
52
|
-
## Target a specific day — `.on(day)`
|
|
53
|
-
|
|
54
|
-
Day-of-week (string) or day-of-month (number 1–31):
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
job("monday-standup", task).weekly().on("monday");
|
|
58
|
-
job("mid-month-sync", task).monthly().on(15);
|
|
59
|
-
job("end-of-month", task).monthly().on(31); // see beginOf/endOf for the right way
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Valid day-of-week strings: `"sunday"`, `"monday"`, `"tuesday"`, `"wednesday"`, `"thursday"`, `"friday"`, `"saturday"`.
|
|
63
|
-
|
|
64
|
-
**Validation.** Numeric `on(N)` throws if `N < 1 || N > 31`.
|
|
65
|
-
|
|
66
|
-
**Gotcha.** `monthly().on(31)` clamps to the actual month length in dayjs — February runs are unpredictable. Use `endOf("month")` for "last day" semantics instead.
|
|
67
|
-
|
|
68
|
-
## Boundary shortcuts — `.beginOf(type)` / `.endOf(type)`
|
|
69
|
-
|
|
70
|
-
Both accept `"day" | "month" | "year"`.
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
**`beginOf("year")` / `endOf("year")` lock the month** — always Jan 1 / Dec 31, never "1st/31st of whatever month the job was defined in."
|
|
84
|
-
|
|
85
|
-
## Cron is an alternative, not an addition
|
|
86
|
-
|
|
87
|
-
`.cron("…")` clears any prior interval/`at`/`on`/`beginOf`/`endOf` config — and vice versa. They're mutually exclusive. See [`@warlock.js/scheduler/schedule-with-cron/SKILL.md`](@warlock.js/scheduler/schedule-with-cron/SKILL.md).
|
|
88
|
-
|
|
89
|
-
## Chaining order doesn't matter (for the most part)
|
|
90
|
-
|
|
91
|
-
Every fluent method recomputes `nextRun` at the end. Putting `.at()` before `.daily()` produces the same schedule as putting `.daily()` before `.at()` — but for readability, the conventional order is **interval → day → time → timezone → execution options**:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
job("good", fn)
|
|
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
|
-
```
|
|
102
|
-
|
|
103
|
-
## Inline registration — `scheduler.newJob()`
|
|
104
|
-
|
|
105
|
-
For one-liners, the scheduler exposes a `newJob()` shortcut that creates, registers, and returns the job in one call:
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
scheduler
|
|
109
|
-
.newJob("cleanup", cleanupFn)
|
|
110
|
-
.daily()
|
|
111
|
-
.at("03:00");
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
To register several pre-built jobs at once, `scheduler.addJobs([...])` is the batch counterpart to `addJob` (both are chainable and preserve insertion order):
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
scheduler.addJobs([
|
|
118
|
-
job("cleanup", cleanupFn).daily().at("03:00"),
|
|
119
|
-
job("reports", sendReports).weekly().on("monday").at("09:00"),
|
|
120
|
-
]);
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
If the scheduler is already running, every job added via `addJob` / `addJobs` is prepared on the spot (its `nextRun` is computed) so it fires on the next tick.
|
|
124
|
-
|
|
125
|
-
## Reading state at runtime
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
const j = scheduler.getJob("nightly-cleanup");
|
|
129
|
-
|
|
130
|
-
j?.nextRun?.toISOString(); // next scheduled run (Dayjs)
|
|
131
|
-
j?.lastRun?.toISOString(); // last attempt — success OR failure
|
|
132
|
-
j?.isRunning; // currently executing?
|
|
133
|
-
j?.intervals; // { every?, day?, dayOfMonthMode?, month?, time? } (readonly)
|
|
134
|
-
j?.cronExpression; // null if using fluent API
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## See also
|
|
138
|
-
|
|
139
|
-
- [`@warlock.js/scheduler/schedule-with-cron/SKILL.md`](@warlock.js/scheduler/schedule-with-cron/SKILL.md) — when the fluent API isn't enough
|
|
140
|
-
- [`@warlock.js/scheduler/configure-retry-and-overlap/SKILL.md`](@warlock.js/scheduler/configure-retry-and-overlap/SKILL.md) — `.retry()` and `.preventOverlap()` on the same job
|
|
141
|
-
- [`@warlock.js/scheduler/pin-schedule-timezone/SKILL.md`](@warlock.js/scheduler/pin-schedule-timezone/SKILL.md) — `.inTimezone()`
|
|
1
|
+
---
|
|
2
|
+
name: schedule-fluently
|
|
3
|
+
description: 'Build a job schedule via `every*` / `daily` / `weekly` / `monthly` / `at` / `on` / `beginOf` / `endOf`. Triggers: `.everyMinutes`, `.daily`, `.weekly`, `.monthly`, `.at`, `.on`, `.every`, `.beginOf`, `.endOf`, `.twiceDaily`, `scheduler.newJob`; "run every 5 minutes", "daily at 3am", "monday standup", "first of every month", "last day of month"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: cron — `@warlock.js/scheduler/schedule-with-cron/SKILL.md`; timezone — `@warlock.js/scheduler/pin-schedule-timezone/SKILL.md`; retry — `@warlock.js/scheduler/configure-retry-and-overlap/SKILL.md`; competing `node-cron`, `node-schedule`, `agenda`; native `setInterval`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fluent scheduling API
|
|
7
|
+
|
|
8
|
+
Chainable methods on a `Job` instance. Each method returns `this` for chaining; each call recomputes `job.nextRun` on the spot.
|
|
9
|
+
|
|
10
|
+
## Preset intervals
|
|
11
|
+
|
|
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
|
+
|
|
31
|
+
## Custom intervals — `.every(value, unit)`
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
job("t", task).every(5, "minute");
|
|
35
|
+
job("t", task).every(2, "hour");
|
|
36
|
+
job("t", task).every(3, "day");
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Units: `"second" | "minute" | "hour" | "day" | "week" | "month" | "year"`.
|
|
40
|
+
|
|
41
|
+
**Validation.** `every(value, unit)` throws at definition time if `value` is `0`, negative, `NaN`, or `Infinity`. This guards against the misconfigured-interval class of bugs that would otherwise spin the scheduler.
|
|
42
|
+
|
|
43
|
+
## Target a specific time — `.at("HH:mm" | "HH:mm:ss")`
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
job("nightly", fn).daily().at("03:00");
|
|
47
|
+
job("midday", fn).daily().at("12:30:15");
|
|
48
|
+
```
|
|
49
|
+
|
|
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).
|
|
51
|
+
|
|
52
|
+
## Target a specific day — `.on(day)`
|
|
53
|
+
|
|
54
|
+
Day-of-week (string) or day-of-month (number 1–31):
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
job("monday-standup", task).weekly().on("monday");
|
|
58
|
+
job("mid-month-sync", task).monthly().on(15);
|
|
59
|
+
job("end-of-month", task).monthly().on(31); // see beginOf/endOf for the right way
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Valid day-of-week strings: `"sunday"`, `"monday"`, `"tuesday"`, `"wednesday"`, `"thursday"`, `"friday"`, `"saturday"`.
|
|
63
|
+
|
|
64
|
+
**Validation.** Numeric `on(N)` throws if `N < 1 || N > 31`.
|
|
65
|
+
|
|
66
|
+
**Gotcha.** `monthly().on(31)` clamps to the actual month length in dayjs — February runs are unpredictable. Use `endOf("month")` for "last day" semantics instead.
|
|
67
|
+
|
|
68
|
+
## Boundary shortcuts — `.beginOf(type)` / `.endOf(type)`
|
|
69
|
+
|
|
70
|
+
Both accept `"day" | "month" | "year"`.
|
|
71
|
+
|
|
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
|
+
|
|
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
|
+
|
|
83
|
+
**`beginOf("year")` / `endOf("year")` lock the month** — always Jan 1 / Dec 31, never "1st/31st of whatever month the job was defined in."
|
|
84
|
+
|
|
85
|
+
## Cron is an alternative, not an addition
|
|
86
|
+
|
|
87
|
+
`.cron("…")` clears any prior interval/`at`/`on`/`beginOf`/`endOf` config — and vice versa. They're mutually exclusive. See [`@warlock.js/scheduler/schedule-with-cron/SKILL.md`](@warlock.js/scheduler/schedule-with-cron/SKILL.md).
|
|
88
|
+
|
|
89
|
+
## Chaining order doesn't matter (for the most part)
|
|
90
|
+
|
|
91
|
+
Every fluent method recomputes `nextRun` at the end. Putting `.at()` before `.daily()` produces the same schedule as putting `.daily()` before `.at()` — but for readability, the conventional order is **interval → day → time → timezone → execution options**:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
job("good", fn)
|
|
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
|
+
```
|
|
102
|
+
|
|
103
|
+
## Inline registration — `scheduler.newJob()`
|
|
104
|
+
|
|
105
|
+
For one-liners, the scheduler exposes a `newJob()` shortcut that creates, registers, and returns the job in one call:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
scheduler
|
|
109
|
+
.newJob("cleanup", cleanupFn)
|
|
110
|
+
.daily()
|
|
111
|
+
.at("03:00");
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
To register several pre-built jobs at once, `scheduler.addJobs([...])` is the batch counterpart to `addJob` (both are chainable and preserve insertion order):
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
scheduler.addJobs([
|
|
118
|
+
job("cleanup", cleanupFn).daily().at("03:00"),
|
|
119
|
+
job("reports", sendReports).weekly().on("monday").at("09:00"),
|
|
120
|
+
]);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If the scheduler is already running, every job added via `addJob` / `addJobs` is prepared on the spot (its `nextRun` is computed) so it fires on the next tick.
|
|
124
|
+
|
|
125
|
+
## Reading state at runtime
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const j = scheduler.getJob("nightly-cleanup");
|
|
129
|
+
|
|
130
|
+
j?.nextRun?.toISOString(); // next scheduled run (Dayjs)
|
|
131
|
+
j?.lastRun?.toISOString(); // last attempt — success OR failure
|
|
132
|
+
j?.isRunning; // currently executing?
|
|
133
|
+
j?.intervals; // { every?, day?, dayOfMonthMode?, month?, time? } (readonly)
|
|
134
|
+
j?.cronExpression; // null if using fluent API
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## See also
|
|
138
|
+
|
|
139
|
+
- [`@warlock.js/scheduler/schedule-with-cron/SKILL.md`](@warlock.js/scheduler/schedule-with-cron/SKILL.md) — when the fluent API isn't enough
|
|
140
|
+
- [`@warlock.js/scheduler/configure-retry-and-overlap/SKILL.md`](@warlock.js/scheduler/configure-retry-and-overlap/SKILL.md) — `.retry()` and `.preventOverlap()` on the same job
|
|
141
|
+
- [`@warlock.js/scheduler/pin-schedule-timezone/SKILL.md`](@warlock.js/scheduler/pin-schedule-timezone/SKILL.md) — `.inTimezone()`
|