@zudojs/scheduler 1.1.0 → 1.1.1
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/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
In-process scheduling for delayed, recurring and cron-driven jobs.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-scheduler](https://zudojs.oyinlola.site/docs/packages-scheduler) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-scheduler.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -151,6 +157,13 @@ for a recurring schedule whose process was blocked past its fire time.
|
|
|
151
157
|
scheduler.every("1h", "hourly-rollup", { misfire: "catch-up" });
|
|
152
158
|
```
|
|
153
159
|
|
|
160
|
+
A cron expression that can never fire (`0 0 30 2 *`, 30 February) is not a
|
|
161
|
+
misfire: `cron()` throws `InvalidScheduleError` at registration.
|
|
162
|
+
|
|
163
|
+
Schedules may be added before or after `start()`. One added to a running
|
|
164
|
+
scheduler re-arms the timer immediately, so it fires on time even when it is
|
|
165
|
+
due sooner than every existing schedule.
|
|
166
|
+
|
|
154
167
|
## Execution history
|
|
155
168
|
|
|
156
169
|
The scheduler keeps the last 100 executions (`MAX_EXECUTION_HISTORY`), each a
|
|
@@ -211,6 +211,12 @@ export class Scheduler {
|
|
|
211
211
|
const scheduleId = crypto.randomUUID();
|
|
212
212
|
const now = this.clock.now();
|
|
213
213
|
let nextRunAt = trigger.next(now);
|
|
214
|
+
if (nextRunAt === null && (type === "cron" || type === "interval")) {
|
|
215
|
+
// A recurring trigger with no next fire time is unsatisfiable (30
|
|
216
|
+
// February) — not a misfire. Running it "once now" fired a job at an
|
|
217
|
+
// arbitrary moment and then retired it silently.
|
|
218
|
+
throw new InvalidScheduleError(`Recurring trigger has no future fire time${expression === undefined ? "" : ` ("${expression}")`}.`, jobId);
|
|
219
|
+
}
|
|
214
220
|
if (nextRunAt === null) {
|
|
215
221
|
// The fire time has already passed. That is the misfire case, not an
|
|
216
222
|
// error — `at(pastDate)` and a schedule restored after a restart both
|
|
@@ -235,6 +241,10 @@ export class Scheduler {
|
|
|
235
241
|
};
|
|
236
242
|
this.schedules.set(scheduleId, record);
|
|
237
243
|
this.queue.enqueue(schedule);
|
|
244
|
+
// A schedule added after start() must re-arm the timer: it may be due
|
|
245
|
+
// sooner than whatever the timer is currently armed for (up to ~24.8
|
|
246
|
+
// days on an empty scheduler).
|
|
247
|
+
this.rearm();
|
|
238
248
|
// The handle holds a reference to this scheduler, so pause, resume and
|
|
239
249
|
// cancel actually reach the queue instead of mutating a detached copy.
|
|
240
250
|
return new ScheduleHandleImpl(scheduleId, "active", {
|
|
@@ -172,7 +172,10 @@ export function nextCronDate(parsed, after, utc = false) {
|
|
|
172
172
|
};
|
|
173
173
|
// Start at the next whole minute after `after`, with seconds cleared.
|
|
174
174
|
const candidate = new Date(after.getTime());
|
|
175
|
-
|
|
175
|
+
if (utc)
|
|
176
|
+
candidate.setUTCSeconds(0, 0);
|
|
177
|
+
else
|
|
178
|
+
candidate.setSeconds(0, 0);
|
|
176
179
|
candidate.setTime(candidate.getTime() + 60_000);
|
|
177
180
|
const limitYear = get.year(after) + MAX_SEARCH_YEARS;
|
|
178
181
|
while (get.year(candidate) <= limitYear) {
|
|
@@ -185,7 +188,7 @@ export function nextCronDate(parsed, after, utc = false) {
|
|
|
185
188
|
continue;
|
|
186
189
|
}
|
|
187
190
|
if (!parsed.hour.has(get.hour(candidate))) {
|
|
188
|
-
advanceHour(candidate);
|
|
191
|
+
advanceHour(candidate, utc);
|
|
189
192
|
continue;
|
|
190
193
|
}
|
|
191
194
|
if (!parsed.minute.has(get.minute(candidate))) {
|
|
@@ -246,9 +249,18 @@ function advanceDay(candidate, utc) {
|
|
|
246
249
|
* Moves to the top of the next hour.
|
|
247
250
|
*
|
|
248
251
|
* Uses wall-clock arithmetic rather than adding an hour of milliseconds, so a
|
|
249
|
-
* DST transition does not skip or repeat an hour of scheduling.
|
|
252
|
+
* DST transition does not skip or repeat an hour of scheduling. In UTC mode
|
|
253
|
+
* the top of the hour is taken in UTC: a host on a half-hour offset
|
|
254
|
+
* (Asia/Kolkata) would otherwise land every skip on :30 UTC and never visit
|
|
255
|
+
* minutes 0-29 of a restricted hour.
|
|
250
256
|
*/
|
|
251
|
-
function advanceHour(candidate) {
|
|
257
|
+
function advanceHour(candidate, utc) {
|
|
258
|
+
if (utc) {
|
|
259
|
+
candidate.setUTCMinutes(0, 0, 0);
|
|
260
|
+
candidate.setTime(candidate.getTime() + 3_600_000);
|
|
261
|
+
candidate.setUTCMinutes(0, 0, 0);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
252
264
|
candidate.setMinutes(0, 0, 0);
|
|
253
265
|
candidate.setTime(candidate.getTime() + 3_600_000);
|
|
254
266
|
candidate.setMinutes(0, 0, 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/scheduler",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Scheduled task and job infrastructure with cron-like scheduling, persistence, and worker management.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"node": ">=24.0.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@zudojs/errors": "1.0
|
|
31
|
-
"@zudojs/constants": "1.0
|
|
32
|
-
"@zudojs/types": "1.
|
|
30
|
+
"@zudojs/errors": "1.1.0",
|
|
31
|
+
"@zudojs/constants": "1.1.0",
|
|
32
|
+
"@zudojs/types": "1.1.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"typescript": "7.0.2",
|