@zudojs/scheduler 0.1.0 → 1.1.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/LICENSE +21 -0
- package/README.md +170 -16
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -3
- package/dist/scheduler/constants/index.d.ts +1 -1
- package/dist/scheduler/constants/index.js +1 -1
- package/dist/scheduler/constants/schedulerConstants.core.d.ts +0 -4
- package/dist/scheduler/constants/schedulerConstants.core.js +0 -4
- package/dist/scheduler/duration/duration.parser.d.ts +13 -5
- package/dist/scheduler/duration/duration.parser.js +57 -19
- package/dist/scheduler/errors/index.d.ts +1 -1
- package/dist/scheduler/errors/index.js +1 -1
- package/dist/scheduler/errors/scheduler.errors.d.ts +7 -1
- package/dist/scheduler/errors/scheduler.errors.js +7 -1
- package/dist/scheduler/executor/index.d.ts +1 -1
- package/dist/scheduler/executor/index.js +1 -1
- package/dist/scheduler/executor/jobExecutor.core.d.ts +24 -4
- package/dist/scheduler/executor/jobExecutor.core.js +154 -28
- package/dist/scheduler/job/jobOptions.type.d.ts +11 -0
- package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.d.ts +12 -0
- package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.js +32 -5
- package/dist/scheduler/registry/jobRegistry.core.d.ts +8 -0
- package/dist/scheduler/registry/jobRegistry.core.js +14 -0
- package/dist/scheduler/schedule/schedule.type.d.ts +11 -6
- package/dist/scheduler/schedule/schedule.type.js +10 -1
- package/dist/scheduler/schedule/scheduleOptions.type.d.ts +24 -1
- package/dist/scheduler/scheduleHandle/index.d.ts +1 -0
- package/dist/scheduler/scheduleHandle/scheduleHandle.type.d.ts +26 -3
- package/dist/scheduler/scheduleHandle/scheduleHandle.type.js +23 -7
- package/dist/scheduler/scheduler.core.d.ts +135 -11
- package/dist/scheduler/scheduler.core.js +478 -53
- package/dist/scheduler/trigger/cron.parser.d.ts +44 -0
- package/dist/scheduler/trigger/cron.parser.js +256 -0
- package/dist/scheduler/trigger/index.d.ts +2 -0
- package/dist/scheduler/trigger/index.js +1 -0
- package/dist/scheduler/trigger/schedulerTrigger.core.d.ts +22 -1
- package/dist/scheduler/trigger/schedulerTrigger.core.js +69 -6
- package/dist/scheduler/types/index.d.ts +12 -1
- package/dist/scheduler/types/index.js +7 -0
- package/dist/scheduler/types/schedulerTypes.core.d.ts +7 -87
- package/dist/scheduler/types/schedulerTypes.core.js +7 -1
- package/package.json +26 -15
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/scheduler/clock/index.d.ts.map +0 -1
- package/dist/scheduler/clock/index.js.map +0 -1
- package/dist/scheduler/clock/schedulerClock.type.d.ts.map +0 -1
- package/dist/scheduler/clock/schedulerClock.type.js.map +0 -1
- package/dist/scheduler/constants/index.d.ts.map +0 -1
- package/dist/scheduler/constants/index.js.map +0 -1
- package/dist/scheduler/constants/schedulerConstants.core.d.ts.map +0 -1
- package/dist/scheduler/constants/schedulerConstants.core.js.map +0 -1
- package/dist/scheduler/duration/duration.parser.d.ts.map +0 -1
- package/dist/scheduler/duration/duration.parser.js.map +0 -1
- package/dist/scheduler/duration/index.d.ts.map +0 -1
- package/dist/scheduler/duration/index.js.map +0 -1
- package/dist/scheduler/errors/index.d.ts.map +0 -1
- package/dist/scheduler/errors/index.js.map +0 -1
- package/dist/scheduler/errors/scheduler.errors.d.ts.map +0 -1
- package/dist/scheduler/errors/scheduler.errors.js.map +0 -1
- package/dist/scheduler/executor/index.d.ts.map +0 -1
- package/dist/scheduler/executor/index.js.map +0 -1
- package/dist/scheduler/executor/jobExecutor.core.d.ts.map +0 -1
- package/dist/scheduler/executor/jobExecutor.core.js.map +0 -1
- package/dist/scheduler/job/index.d.ts.map +0 -1
- package/dist/scheduler/job/index.js.map +0 -1
- package/dist/scheduler/job/jobContext.type.d.ts.map +0 -1
- package/dist/scheduler/job/jobContext.type.js.map +0 -1
- package/dist/scheduler/job/jobDefinition.type.d.ts.map +0 -1
- package/dist/scheduler/job/jobDefinition.type.js.map +0 -1
- package/dist/scheduler/job/jobHandler.type.d.ts.map +0 -1
- package/dist/scheduler/job/jobHandler.type.js.map +0 -1
- package/dist/scheduler/job/jobOptions.type.d.ts.map +0 -1
- package/dist/scheduler/job/jobOptions.type.js.map +0 -1
- package/dist/scheduler/priorityQueue/index.d.ts.map +0 -1
- package/dist/scheduler/priorityQueue/index.js.map +0 -1
- package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.d.ts.map +0 -1
- package/dist/scheduler/priorityQueue/schedulerPriorityQueue.core.js.map +0 -1
- package/dist/scheduler/registry/index.d.ts.map +0 -1
- package/dist/scheduler/registry/index.js.map +0 -1
- package/dist/scheduler/registry/jobRegistry.core.d.ts.map +0 -1
- package/dist/scheduler/registry/jobRegistry.core.js.map +0 -1
- package/dist/scheduler/schedule/index.d.ts.map +0 -1
- package/dist/scheduler/schedule/index.js.map +0 -1
- package/dist/scheduler/schedule/schedule.type.d.ts.map +0 -1
- package/dist/scheduler/schedule/schedule.type.js.map +0 -1
- package/dist/scheduler/schedule/scheduleOptions.type.d.ts.map +0 -1
- package/dist/scheduler/schedule/scheduleOptions.type.js.map +0 -1
- package/dist/scheduler/scheduleHandle/index.d.ts.map +0 -1
- package/dist/scheduler/scheduleHandle/index.js.map +0 -1
- package/dist/scheduler/scheduleHandle/scheduleHandle.type.d.ts.map +0 -1
- package/dist/scheduler/scheduleHandle/scheduleHandle.type.js.map +0 -1
- package/dist/scheduler/scheduler.core.d.ts.map +0 -1
- package/dist/scheduler/scheduler.core.js.map +0 -1
- package/dist/scheduler/trigger/index.d.ts.map +0 -1
- package/dist/scheduler/trigger/index.js.map +0 -1
- package/dist/scheduler/trigger/schedulerTrigger.core.d.ts.map +0 -1
- package/dist/scheduler/trigger/schedulerTrigger.core.js.map +0 -1
- package/dist/scheduler/trigger/trigger.type.d.ts.map +0 -1
- package/dist/scheduler/trigger/trigger.type.js.map +0 -1
- package/dist/scheduler/types/index.d.ts.map +0 -1
- package/dist/scheduler/types/index.js.map +0 -1
- package/dist/scheduler/types/schedulerTypes.core.d.ts.map +0 -1
- package/dist/scheduler/types/schedulerTypes.core.js.map +0 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/scheduler/trigger/cron
|
|
3
|
+
*
|
|
4
|
+
* Standard five-field cron expression parsing and next-fire computation.
|
|
5
|
+
*
|
|
6
|
+
* Fields, in order: minute, hour, day-of-month, month, day-of-week.
|
|
7
|
+
* Each supports `*`, a value, a `a-b` range, a `a-b/n` or `*\/n` step, and a
|
|
8
|
+
* comma-separated list of any of those. Month and day-of-week also accept the
|
|
9
|
+
* usual three-letter names. A leading `@yearly`-style macro is expanded first.
|
|
10
|
+
*/
|
|
11
|
+
import { CronParseError } from "../errors/scheduler.errors.js";
|
|
12
|
+
/** Inclusive bounds for each cron field. */
|
|
13
|
+
const FIELD_BOUNDS = [
|
|
14
|
+
{ name: "minute", min: 0, max: 59 },
|
|
15
|
+
{ name: "hour", min: 0, max: 23 },
|
|
16
|
+
{ name: "dayOfMonth", min: 1, max: 31 },
|
|
17
|
+
{ name: "month", min: 1, max: 12 },
|
|
18
|
+
{ name: "dayOfWeek", min: 0, max: 6 },
|
|
19
|
+
];
|
|
20
|
+
/** Named aliases accepted in the month field. */
|
|
21
|
+
const MONTH_NAMES = {
|
|
22
|
+
jan: 1,
|
|
23
|
+
feb: 2,
|
|
24
|
+
mar: 3,
|
|
25
|
+
apr: 4,
|
|
26
|
+
may: 5,
|
|
27
|
+
jun: 6,
|
|
28
|
+
jul: 7,
|
|
29
|
+
aug: 8,
|
|
30
|
+
sep: 9,
|
|
31
|
+
oct: 10,
|
|
32
|
+
nov: 11,
|
|
33
|
+
dec: 12,
|
|
34
|
+
};
|
|
35
|
+
/** Named aliases accepted in the day-of-week field. */
|
|
36
|
+
const DAY_NAMES = {
|
|
37
|
+
sun: 0,
|
|
38
|
+
mon: 1,
|
|
39
|
+
tue: 2,
|
|
40
|
+
wed: 3,
|
|
41
|
+
thu: 4,
|
|
42
|
+
fri: 5,
|
|
43
|
+
sat: 6,
|
|
44
|
+
};
|
|
45
|
+
/** Shorthand macros. */
|
|
46
|
+
const MACROS = {
|
|
47
|
+
"@yearly": "0 0 1 1 *",
|
|
48
|
+
"@annually": "0 0 1 1 *",
|
|
49
|
+
"@monthly": "0 0 1 * *",
|
|
50
|
+
"@weekly": "0 0 * * 0",
|
|
51
|
+
"@daily": "0 0 * * *",
|
|
52
|
+
"@midnight": "0 0 * * *",
|
|
53
|
+
"@hourly": "0 * * * *",
|
|
54
|
+
};
|
|
55
|
+
/** How far ahead {@link nextCronDate} will search before giving up. */
|
|
56
|
+
const MAX_SEARCH_YEARS = 5;
|
|
57
|
+
/**
|
|
58
|
+
* Parses a cron expression into the set of values each field permits.
|
|
59
|
+
*
|
|
60
|
+
* @param expression - A five-field cron expression or a supported macro.
|
|
61
|
+
* @returns The parsed expression.
|
|
62
|
+
* @throws {CronParseError} when the expression is not valid.
|
|
63
|
+
*/
|
|
64
|
+
export function parseCron(expression) {
|
|
65
|
+
const trimmed = expression.trim().toLowerCase();
|
|
66
|
+
if (trimmed.length === 0) {
|
|
67
|
+
throw new CronParseError("Cron expression cannot be empty", expression);
|
|
68
|
+
}
|
|
69
|
+
const expanded = MACROS[trimmed] ?? trimmed;
|
|
70
|
+
const fields = expanded.split(/\s+/);
|
|
71
|
+
if (fields.length !== 5) {
|
|
72
|
+
throw new CronParseError(`Cron expression must have 5 fields (minute hour day-of-month month day-of-week), got ${fields.length}`, expression);
|
|
73
|
+
}
|
|
74
|
+
const sets = FIELD_BOUNDS.map((bounds, index) => parseField(fields[index] ?? "", bounds, expression));
|
|
75
|
+
return {
|
|
76
|
+
minute: sets[0] ?? new Set(),
|
|
77
|
+
hour: sets[1] ?? new Set(),
|
|
78
|
+
dayOfMonth: sets[2] ?? new Set(),
|
|
79
|
+
month: sets[3] ?? new Set(),
|
|
80
|
+
dayOfWeek: sets[4] ?? new Set(),
|
|
81
|
+
dayOfMonthUnrestricted: (fields[2] ?? "") === "*",
|
|
82
|
+
dayOfWeekUnrestricted: (fields[4] ?? "") === "*",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Parses a single cron field into the set of values it permits. */
|
|
86
|
+
function parseField(field, bounds, expression) {
|
|
87
|
+
if (field.length === 0) {
|
|
88
|
+
throw new CronParseError(`Cron field "${bounds.name}" is empty`, expression);
|
|
89
|
+
}
|
|
90
|
+
const values = new Set();
|
|
91
|
+
for (const part of field.split(",")) {
|
|
92
|
+
const [rangePart, stepPart] = part.split("/");
|
|
93
|
+
if (stepPart !== undefined && !/^\d+$/.test(stepPart)) {
|
|
94
|
+
throw new CronParseError(`Cron field "${bounds.name}" has an invalid step: "${part}"`, expression);
|
|
95
|
+
}
|
|
96
|
+
const step = stepPart === undefined ? 1 : Number(stepPart);
|
|
97
|
+
if (step === 0) {
|
|
98
|
+
throw new CronParseError(`Cron field "${bounds.name}" has a zero step: "${part}"`, expression);
|
|
99
|
+
}
|
|
100
|
+
let start;
|
|
101
|
+
let end;
|
|
102
|
+
if (rangePart === "*" || rangePart === undefined || rangePart === "") {
|
|
103
|
+
start = bounds.min;
|
|
104
|
+
end = bounds.max;
|
|
105
|
+
}
|
|
106
|
+
else if (rangePart.includes("-")) {
|
|
107
|
+
const [from, to] = rangePart.split("-");
|
|
108
|
+
start = resolveValue(from ?? "", bounds, expression);
|
|
109
|
+
end = resolveValue(to ?? "", bounds, expression);
|
|
110
|
+
if (start > end) {
|
|
111
|
+
throw new CronParseError(`Cron field "${bounds.name}" has an inverted range: "${rangePart}"`, expression);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
start = resolveValue(rangePart, bounds, expression);
|
|
116
|
+
// A bare value with a step means "from here to the end of the field".
|
|
117
|
+
end = stepPart === undefined ? start : bounds.max;
|
|
118
|
+
}
|
|
119
|
+
for (let v = start; v <= end; v += step) {
|
|
120
|
+
values.add(v);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (values.size === 0) {
|
|
124
|
+
throw new CronParseError(`Cron field "${bounds.name}" matches no values: "${field}"`, expression);
|
|
125
|
+
}
|
|
126
|
+
return values;
|
|
127
|
+
}
|
|
128
|
+
/** Resolves a numeric or named field value, checking it against the bounds. */
|
|
129
|
+
function resolveValue(raw, bounds, expression) {
|
|
130
|
+
let value;
|
|
131
|
+
if (/^\d+$/.test(raw)) {
|
|
132
|
+
value = Number(raw);
|
|
133
|
+
// Both 0 and 7 are Sunday in common cron dialects.
|
|
134
|
+
if (bounds.name === "dayOfWeek" && value === 7) {
|
|
135
|
+
value = 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (bounds.name === "month" && raw in MONTH_NAMES) {
|
|
139
|
+
value = MONTH_NAMES[raw];
|
|
140
|
+
}
|
|
141
|
+
else if (bounds.name === "dayOfWeek" && raw in DAY_NAMES) {
|
|
142
|
+
value = DAY_NAMES[raw];
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw new CronParseError(`Cron field "${bounds.name}" has an invalid value: "${raw}"`, expression);
|
|
146
|
+
}
|
|
147
|
+
if (value < bounds.min || value > bounds.max) {
|
|
148
|
+
throw new CronParseError(`Cron field "${bounds.name}" value ${value} is outside ${bounds.min}-${bounds.max}`, expression);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Computes the next time a parsed cron expression fires, strictly after `after`.
|
|
154
|
+
*
|
|
155
|
+
* Search is minute-by-minute with whole-field skips, bounded by
|
|
156
|
+
* {@link MAX_SEARCH_YEARS} so an unsatisfiable expression (30 February) fails
|
|
157
|
+
* rather than looping.
|
|
158
|
+
*
|
|
159
|
+
* @param parsed - The parsed expression.
|
|
160
|
+
* @param after - The instant to search forward from (exclusive).
|
|
161
|
+
* @param utc - Interpret the fields in UTC rather than local time.
|
|
162
|
+
* @returns The next fire time, or null when none exists within the horizon.
|
|
163
|
+
*/
|
|
164
|
+
export function nextCronDate(parsed, after, utc = false) {
|
|
165
|
+
const get = {
|
|
166
|
+
minute: (d) => (utc ? d.getUTCMinutes() : d.getMinutes()),
|
|
167
|
+
hour: (d) => (utc ? d.getUTCHours() : d.getHours()),
|
|
168
|
+
date: (d) => (utc ? d.getUTCDate() : d.getDate()),
|
|
169
|
+
month: (d) => (utc ? d.getUTCMonth() : d.getMonth()) + 1,
|
|
170
|
+
day: (d) => (utc ? d.getUTCDay() : d.getDay()),
|
|
171
|
+
year: (d) => (utc ? d.getUTCFullYear() : d.getFullYear()),
|
|
172
|
+
};
|
|
173
|
+
// Start at the next whole minute after `after`, with seconds cleared.
|
|
174
|
+
const candidate = new Date(after.getTime());
|
|
175
|
+
candidate.setSeconds(0, 0);
|
|
176
|
+
candidate.setTime(candidate.getTime() + 60_000);
|
|
177
|
+
const limitYear = get.year(after) + MAX_SEARCH_YEARS;
|
|
178
|
+
while (get.year(candidate) <= limitYear) {
|
|
179
|
+
if (!parsed.month.has(get.month(candidate))) {
|
|
180
|
+
advanceMonth(candidate, utc);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (!matchesDay(parsed, candidate, get.date, get.day)) {
|
|
184
|
+
advanceDay(candidate, utc);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!parsed.hour.has(get.hour(candidate))) {
|
|
188
|
+
advanceHour(candidate);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!parsed.minute.has(get.minute(candidate))) {
|
|
192
|
+
candidate.setTime(candidate.getTime() + 60_000);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
return candidate;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Applies the cron day-matching rule.
|
|
201
|
+
*
|
|
202
|
+
* When both day-of-month and day-of-week are restricted, cron matches if
|
|
203
|
+
* *either* does — an inconsistency in the original spec that every
|
|
204
|
+
* implementation preserves, because `0 0 1,15 * mon` is widely used to mean
|
|
205
|
+
* "the 1st, the 15th, and every Monday".
|
|
206
|
+
*/
|
|
207
|
+
function matchesDay(parsed, candidate, getDate, getDay) {
|
|
208
|
+
const domMatches = parsed.dayOfMonth.has(getDate(candidate));
|
|
209
|
+
const dowMatches = parsed.dayOfWeek.has(getDay(candidate));
|
|
210
|
+
if (parsed.dayOfMonthUnrestricted && parsed.dayOfWeekUnrestricted) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (parsed.dayOfMonthUnrestricted) {
|
|
214
|
+
return dowMatches;
|
|
215
|
+
}
|
|
216
|
+
if (parsed.dayOfWeekUnrestricted) {
|
|
217
|
+
return domMatches;
|
|
218
|
+
}
|
|
219
|
+
return domMatches || dowMatches;
|
|
220
|
+
}
|
|
221
|
+
/** Moves to 00:00 on the first day of the next month. */
|
|
222
|
+
function advanceMonth(candidate, utc) {
|
|
223
|
+
if (utc) {
|
|
224
|
+
candidate.setUTCDate(1);
|
|
225
|
+
candidate.setUTCHours(0, 0, 0, 0);
|
|
226
|
+
candidate.setUTCMonth(candidate.getUTCMonth() + 1);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
candidate.setDate(1);
|
|
230
|
+
candidate.setHours(0, 0, 0, 0);
|
|
231
|
+
candidate.setMonth(candidate.getMonth() + 1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Moves to 00:00 on the next day. */
|
|
235
|
+
function advanceDay(candidate, utc) {
|
|
236
|
+
if (utc) {
|
|
237
|
+
candidate.setUTCHours(0, 0, 0, 0);
|
|
238
|
+
candidate.setUTCDate(candidate.getUTCDate() + 1);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
candidate.setHours(0, 0, 0, 0);
|
|
242
|
+
candidate.setDate(candidate.getDate() + 1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Moves to the top of the next hour.
|
|
247
|
+
*
|
|
248
|
+
* 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.
|
|
250
|
+
*/
|
|
251
|
+
function advanceHour(candidate) {
|
|
252
|
+
candidate.setMinutes(0, 0, 0);
|
|
253
|
+
candidate.setTime(candidate.getTime() + 3_600_000);
|
|
254
|
+
candidate.setMinutes(0, 0, 0);
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=cron.parser.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export type { Trigger } from "./trigger.type.js";
|
|
2
2
|
export { DateTrigger, DelayTrigger, IntervalTrigger, CronTrigger, } from "./schedulerTrigger.core.js";
|
|
3
|
+
export { parseCron, nextCronDate } from "./cron.parser.js";
|
|
4
|
+
export type { ParsedCron } from "./cron.parser.js";
|
|
3
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { Trigger } from "./trigger.type.js";
|
|
2
2
|
/**
|
|
3
3
|
* Trigger that fires once at a specific date.
|
|
4
|
+
*
|
|
5
|
+
* A date already in the past yields `null` from {@link DateTrigger.next}, which
|
|
6
|
+
* the scheduler interprets through the schedule's misfire policy rather than
|
|
7
|
+
* treating as an error.
|
|
4
8
|
*/
|
|
5
9
|
export declare class DateTrigger implements Trigger {
|
|
6
10
|
private readonly date;
|
|
7
11
|
constructor(date: Date);
|
|
12
|
+
/** The instant this trigger fires at. */
|
|
13
|
+
get fireAt(): Date;
|
|
8
14
|
next(after: Date): Date | null;
|
|
9
15
|
}
|
|
10
16
|
/**
|
|
@@ -21,15 +27,30 @@ export declare class DelayTrigger implements Trigger {
|
|
|
21
27
|
export declare class IntervalTrigger implements Trigger {
|
|
22
28
|
private readonly intervalMs;
|
|
23
29
|
constructor(intervalMs: number);
|
|
30
|
+
/** The interval, in milliseconds. */
|
|
31
|
+
get interval(): number;
|
|
24
32
|
next(after: Date): Date;
|
|
25
33
|
}
|
|
26
34
|
/**
|
|
27
35
|
* Trigger that fires according to a cron expression.
|
|
36
|
+
*
|
|
37
|
+
* The expression is parsed once at construction, so an invalid one fails where
|
|
38
|
+
* the schedule is declared rather than silently firing on some other cadence.
|
|
28
39
|
*/
|
|
29
40
|
export declare class CronTrigger implements Trigger {
|
|
30
41
|
private readonly expression;
|
|
31
|
-
private readonly
|
|
42
|
+
private readonly parsed;
|
|
43
|
+
private readonly utc;
|
|
44
|
+
/**
|
|
45
|
+
* @param expression - A five-field cron expression, or a macro such as `@daily`.
|
|
46
|
+
* @param timezone - Pass `"UTC"` to interpret the fields in UTC. Any other
|
|
47
|
+
* value is rejected: honouring an arbitrary IANA zone needs real zone data,
|
|
48
|
+
* and silently ignoring the argument is what made the previous
|
|
49
|
+
* implementation's `timezone` parameter meaningless.
|
|
50
|
+
*/
|
|
32
51
|
constructor(expression: string, timezone?: string);
|
|
52
|
+
/** The expression this trigger was built from. */
|
|
53
|
+
get source(): string;
|
|
33
54
|
next(after: Date): Date | null;
|
|
34
55
|
}
|
|
35
56
|
//# sourceMappingURL=schedulerTrigger.core.d.ts.map
|
|
@@ -1,16 +1,29 @@
|
|
|
1
|
+
import { parseCron, nextCronDate } from "./cron.parser.js";
|
|
2
|
+
import { InvalidScheduleError } from "../errors/scheduler.errors.js";
|
|
1
3
|
/**
|
|
2
4
|
* Trigger that fires once at a specific date.
|
|
5
|
+
*
|
|
6
|
+
* A date already in the past yields `null` from {@link DateTrigger.next}, which
|
|
7
|
+
* the scheduler interprets through the schedule's misfire policy rather than
|
|
8
|
+
* treating as an error.
|
|
3
9
|
*/
|
|
4
10
|
export class DateTrigger {
|
|
5
11
|
date;
|
|
6
12
|
constructor(date) {
|
|
7
|
-
|
|
13
|
+
if (Number.isNaN(date.getTime())) {
|
|
14
|
+
throw new InvalidScheduleError("DateTrigger requires a valid Date", "date-trigger");
|
|
15
|
+
}
|
|
16
|
+
this.date = new Date(date.getTime());
|
|
17
|
+
}
|
|
18
|
+
/** The instant this trigger fires at. */
|
|
19
|
+
get fireAt() {
|
|
20
|
+
return new Date(this.date.getTime());
|
|
8
21
|
}
|
|
9
22
|
next(after) {
|
|
10
|
-
if (after >= this.date) {
|
|
23
|
+
if (after.getTime() >= this.date.getTime()) {
|
|
11
24
|
return null;
|
|
12
25
|
}
|
|
13
|
-
return this.date;
|
|
26
|
+
return new Date(this.date.getTime());
|
|
14
27
|
}
|
|
15
28
|
}
|
|
16
29
|
/**
|
|
@@ -19,6 +32,7 @@ export class DateTrigger {
|
|
|
19
32
|
export class DelayTrigger {
|
|
20
33
|
delayMs;
|
|
21
34
|
constructor(delayMs) {
|
|
35
|
+
assertUsableInterval(delayMs, "DelayTrigger");
|
|
22
36
|
this.delayMs = delayMs;
|
|
23
37
|
}
|
|
24
38
|
next(after) {
|
|
@@ -31,24 +45,73 @@ export class DelayTrigger {
|
|
|
31
45
|
export class IntervalTrigger {
|
|
32
46
|
intervalMs;
|
|
33
47
|
constructor(intervalMs) {
|
|
48
|
+
assertUsableInterval(intervalMs, "IntervalTrigger");
|
|
34
49
|
this.intervalMs = intervalMs;
|
|
35
50
|
}
|
|
51
|
+
/** The interval, in milliseconds. */
|
|
52
|
+
get interval() {
|
|
53
|
+
return this.intervalMs;
|
|
54
|
+
}
|
|
36
55
|
next(after) {
|
|
37
56
|
return new Date(after.getTime() + this.intervalMs);
|
|
38
57
|
}
|
|
39
58
|
}
|
|
40
59
|
/**
|
|
41
60
|
* Trigger that fires according to a cron expression.
|
|
61
|
+
*
|
|
62
|
+
* The expression is parsed once at construction, so an invalid one fails where
|
|
63
|
+
* the schedule is declared rather than silently firing on some other cadence.
|
|
42
64
|
*/
|
|
43
65
|
export class CronTrigger {
|
|
44
66
|
expression;
|
|
45
|
-
|
|
67
|
+
parsed;
|
|
68
|
+
utc;
|
|
69
|
+
/**
|
|
70
|
+
* @param expression - A five-field cron expression, or a macro such as `@daily`.
|
|
71
|
+
* @param timezone - Pass `"UTC"` to interpret the fields in UTC. Any other
|
|
72
|
+
* value is rejected: honouring an arbitrary IANA zone needs real zone data,
|
|
73
|
+
* and silently ignoring the argument is what made the previous
|
|
74
|
+
* implementation's `timezone` parameter meaningless.
|
|
75
|
+
*/
|
|
46
76
|
constructor(expression, timezone) {
|
|
47
77
|
this.expression = expression;
|
|
48
|
-
this.
|
|
78
|
+
this.parsed = parseCron(expression);
|
|
79
|
+
if (timezone === undefined) {
|
|
80
|
+
this.utc = false;
|
|
81
|
+
}
|
|
82
|
+
else if (/^utc$/i.test(timezone) || timezone === "Etc/UTC") {
|
|
83
|
+
this.utc = true;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
throw new InvalidScheduleError(`CronTrigger supports only "UTC" or the system local zone, got "${timezone}"`, expression);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The expression this trigger was built from. */
|
|
90
|
+
get source() {
|
|
91
|
+
return this.expression;
|
|
49
92
|
}
|
|
50
93
|
next(after) {
|
|
51
|
-
return
|
|
94
|
+
return nextCronDate(this.parsed, after, this.utc);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Rejects an interval that cannot produce a usable schedule.
|
|
99
|
+
*
|
|
100
|
+
* A zero or negative interval would fire continuously, and one large enough to
|
|
101
|
+
* overflow the Date range produces an invalid date whose `getTime()` is `NaN` —
|
|
102
|
+
* which then poisons every comparison in the scheduler's heap.
|
|
103
|
+
*/
|
|
104
|
+
function assertUsableInterval(ms, what) {
|
|
105
|
+
if (!Number.isFinite(ms)) {
|
|
106
|
+
throw new InvalidScheduleError(`${what} requires a finite interval, got: ${ms}`, what);
|
|
107
|
+
}
|
|
108
|
+
if (ms <= 0) {
|
|
109
|
+
throw new InvalidScheduleError(`${what} requires a positive interval, got: ${ms}`, what);
|
|
110
|
+
}
|
|
111
|
+
// The Date range is ±8.64e15 ms from the epoch; anything approaching it
|
|
112
|
+
// cannot be added to "now" and still yield a valid date.
|
|
113
|
+
if (ms > 8.64e15) {
|
|
114
|
+
throw new InvalidScheduleError(`${what} interval ${ms}ms exceeds the representable date range`, what);
|
|
52
115
|
}
|
|
53
116
|
}
|
|
54
117
|
//# sourceMappingURL=schedulerTrigger.core.js.map
|
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The package's public type surface.
|
|
3
|
+
*
|
|
4
|
+
* Identity, state and policy vocabulary comes from `schedulerTypes.core.ts`;
|
|
5
|
+
* every structural type is re-exported from the module that owns it, so the
|
|
6
|
+
* type a consumer imports is the one the implementation actually uses.
|
|
7
|
+
*/
|
|
8
|
+
export type { SchedulerJobId, ScheduleId, ExecutionId, ScheduleType, ScheduleState, JobState, OverlapPolicy, MisfirePolicy, JobExecution, JobExecutionResult, } from "./schedulerTypes.core.js";
|
|
9
|
+
export type { JobOptions, RetryPolicy, RetryStrategy, JobDefinition, JobHandler, JobContext, } from "../job/index.js";
|
|
10
|
+
export type { Schedule, ScheduleOptions } from "../schedule/index.js";
|
|
11
|
+
export type { ScheduleHandle } from "../scheduleHandle/index.js";
|
|
12
|
+
export type { Trigger } from "../trigger/index.js";
|
|
2
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's public type surface.
|
|
3
|
+
*
|
|
4
|
+
* Identity, state and policy vocabulary comes from `schedulerTypes.core.ts`;
|
|
5
|
+
* every structural type is re-exported from the module that owns it, so the
|
|
6
|
+
* type a consumer imports is the one the implementation actually uses.
|
|
7
|
+
*/
|
|
1
8
|
export {};
|
|
2
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/scheduler/types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The scheduler's identity, state and policy vocabulary.
|
|
5
|
+
*
|
|
6
|
+
* Structural types — jobs, schedules, handles, triggers — live with the code
|
|
7
|
+
* that builds them and are re-exported by `types/index.ts`. They used to be
|
|
8
|
+
* declared a second time here, and the copies had drifted: the public
|
|
9
|
+
* `ScheduleOptions` was missing `overlap` and `data`, so correct calls to
|
|
10
|
+
* `scheduler.every(..., { overlap: "skip" })` did not typecheck.
|
|
5
11
|
*/
|
|
6
12
|
/**
|
|
7
13
|
* Unique identifier for a scheduled job.
|
|
@@ -35,54 +41,6 @@ export type OverlapPolicy = "allow" | "skip" | "queue" | "replace";
|
|
|
35
41
|
* Policy for handling missed executions.
|
|
36
42
|
*/
|
|
37
43
|
export type MisfirePolicy = "skip" | "run-once" | "catch-up";
|
|
38
|
-
/**
|
|
39
|
-
* Retry strategy.
|
|
40
|
-
*/
|
|
41
|
-
export type RetryStrategy = "fixed" | "linear" | "exponential";
|
|
42
|
-
/**
|
|
43
|
-
* Retry policy for job executions.
|
|
44
|
-
*/
|
|
45
|
-
export interface RetryPolicy {
|
|
46
|
-
readonly attempts: number;
|
|
47
|
-
readonly strategy: RetryStrategy;
|
|
48
|
-
readonly delay: number;
|
|
49
|
-
readonly maxDelay?: number;
|
|
50
|
-
readonly jitter?: boolean;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Options for a scheduled job.
|
|
54
|
-
*/
|
|
55
|
-
export interface JobOptions {
|
|
56
|
-
readonly timeout?: number;
|
|
57
|
-
readonly retry?: RetryPolicy;
|
|
58
|
-
readonly concurrency?: number;
|
|
59
|
-
readonly overlap?: OverlapPolicy;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Definition of a scheduled job.
|
|
63
|
-
*/
|
|
64
|
-
export interface JobDefinition {
|
|
65
|
-
readonly id: SchedulerJobId;
|
|
66
|
-
readonly name: string;
|
|
67
|
-
readonly handler: JobHandler;
|
|
68
|
-
readonly options?: JobOptions;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Handler for a scheduled job.
|
|
72
|
-
*/
|
|
73
|
-
export type JobHandler<T = unknown> = (context: JobContext<T>) => Promise<void> | void;
|
|
74
|
-
/**
|
|
75
|
-
* Context passed to a job handler during execution.
|
|
76
|
-
*/
|
|
77
|
-
export interface JobContext<T = unknown> {
|
|
78
|
-
readonly jobId: SchedulerJobId;
|
|
79
|
-
readonly executionId: ExecutionId;
|
|
80
|
-
readonly scheduledAt: Date;
|
|
81
|
-
readonly startedAt: Date;
|
|
82
|
-
readonly attempt: number;
|
|
83
|
-
readonly data: T;
|
|
84
|
-
readonly signal: AbortSignal;
|
|
85
|
-
}
|
|
86
44
|
/**
|
|
87
45
|
* Record of a job execution.
|
|
88
46
|
*/
|
|
@@ -105,42 +63,4 @@ export interface JobExecutionResult {
|
|
|
105
63
|
readonly success: boolean;
|
|
106
64
|
readonly error?: unknown;
|
|
107
65
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Schedule definition.
|
|
110
|
-
*/
|
|
111
|
-
export interface Schedule {
|
|
112
|
-
readonly id: ScheduleId;
|
|
113
|
-
readonly jobId: SchedulerJobId;
|
|
114
|
-
readonly type: ScheduleType;
|
|
115
|
-
readonly expression?: string;
|
|
116
|
-
readonly nextRunAt: Date;
|
|
117
|
-
readonly lastRunAt?: Date;
|
|
118
|
-
readonly state: ScheduleState;
|
|
119
|
-
readonly options?: ScheduleOptions;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Options for a schedule.
|
|
123
|
-
*/
|
|
124
|
-
export interface ScheduleOptions {
|
|
125
|
-
readonly timezone?: string;
|
|
126
|
-
readonly misfire?: MisfirePolicy;
|
|
127
|
-
readonly priority?: number;
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Handle for controlling a schedule.
|
|
131
|
-
*/
|
|
132
|
-
export interface ScheduleHandle {
|
|
133
|
-
readonly id: ScheduleId;
|
|
134
|
-
readonly state: ScheduleState;
|
|
135
|
-
pause(): Promise<void>;
|
|
136
|
-
resume(): Promise<void>;
|
|
137
|
-
cancel(): Promise<void>;
|
|
138
|
-
nextRun(): Date | undefined;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Trigger interface for calculating next execution time.
|
|
142
|
-
*/
|
|
143
|
-
export interface Trigger {
|
|
144
|
-
next(after: Date): Date | null;
|
|
145
|
-
}
|
|
146
66
|
//# sourceMappingURL=schedulerTypes.core.d.ts.map
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @zudojs/scheduler/types
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* The scheduler's identity, state and policy vocabulary.
|
|
5
|
+
*
|
|
6
|
+
* Structural types — jobs, schedules, handles, triggers — live with the code
|
|
7
|
+
* that builds them and are re-exported by `types/index.ts`. They used to be
|
|
8
|
+
* declared a second time here, and the copies had drifted: the public
|
|
9
|
+
* `ScheduleOptions` was missing `overlap` and `data`, so correct calls to
|
|
10
|
+
* `scheduler.every(..., { overlap: "skip" })` did not typecheck.
|
|
5
11
|
*/
|
|
6
12
|
export {};
|
|
7
13
|
//# sourceMappingURL=schedulerTypes.core.js.map
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/scheduler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Scheduled task and job infrastructure with cron-like scheduling, persistence, and worker management.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -14,25 +18,21 @@
|
|
|
14
18
|
}
|
|
15
19
|
},
|
|
16
20
|
"files": [
|
|
17
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"!dist/**/*.map",
|
|
23
|
+
"!dist/**/*.tsbuildinfo",
|
|
24
|
+
"!dist/.tsbuildinfo"
|
|
18
25
|
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
|
-
"clean": "rm -rf dist",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"test:watch": "vitest"
|
|
25
|
-
},
|
|
26
26
|
"engines": {
|
|
27
27
|
"node": ">=24.0.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@zudojs/errors": "0.1
|
|
31
|
-
"@zudojs/constants": "0.1
|
|
32
|
-
"@zudojs/types": "
|
|
30
|
+
"@zudojs/errors": "1.0.1",
|
|
31
|
+
"@zudojs/constants": "1.0.1",
|
|
32
|
+
"@zudojs/types": "1.0.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"typescript": "
|
|
35
|
+
"typescript": "7.0.2",
|
|
36
36
|
"vitest": "^4.1.11"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
@@ -45,8 +45,19 @@
|
|
|
45
45
|
"jobs"
|
|
46
46
|
],
|
|
47
47
|
"homepage": "https://github.com/oyinlola-tech/zudo#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
50
|
+
},
|
|
48
51
|
"repository": {
|
|
49
52
|
"type": "git",
|
|
50
|
-
"url": "https://github.com/oyinlola-tech/zudo"
|
|
53
|
+
"url": "https://github.com/oyinlola-tech/zudo",
|
|
54
|
+
"directory": "packages/scheduler"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.json",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
59
|
+
"clean": "rm -rf dist",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest"
|
|
51
62
|
}
|
|
52
|
-
}
|
|
63
|
+
}
|