@dbos-inc/dbos-sdk 5.1.10 → 5.2.4-preview
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/dist/schemas/system_db_schema.d.ts +2 -1
- package/dist/schemas/system_db_schema.d.ts.map +1 -1
- package/dist/schemas/system_db_schema.js.map +1 -1
- package/dist/src/client.d.ts +3 -1
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +5 -4
- package/dist/src/client.js.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +2 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/context.d.ts +3 -0
- package/dist/src/context.d.ts.map +1 -1
- package/dist/src/context.js +13 -1
- package/dist/src/context.js.map +1 -1
- package/dist/src/dbos-executor.d.ts +15 -2
- package/dist/src/dbos-executor.d.ts.map +1 -1
- package/dist/src/dbos-executor.js +33 -32
- package/dist/src/dbos-executor.js.map +1 -1
- package/dist/src/dbos.d.ts.map +1 -1
- package/dist/src/dbos.js +5 -0
- package/dist/src/dbos.js.map +1 -1
- package/dist/src/error.d.ts +8 -0
- package/dist/src/error.d.ts.map +1 -1
- package/dist/src/error.js +17 -2
- package/dist/src/error.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/scheduler/crontab.d.ts +9 -2
- package/dist/src/scheduler/crontab.d.ts.map +1 -1
- package/dist/src/scheduler/crontab.js +418 -181
- package/dist/src/scheduler/crontab.js.map +1 -1
- package/dist/src/scheduler/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler/scheduler.js +14 -8
- package/dist/src/scheduler/scheduler.js.map +1 -1
- package/dist/src/sysdb_migrations/internal/migrations.js +5 -0
- package/dist/src/sysdb_migrations/internal/migrations.js.map +1 -1
- package/dist/src/system_database.d.ts +30 -14
- package/dist/src/system_database.d.ts.map +1 -1
- package/dist/src/system_database.js +291 -131
- package/dist/src/system_database.js.map +1 -1
- package/dist/src/wfqueue.d.ts.map +1 -1
- package/dist/src/wfqueue.js +12 -8
- package/dist/src/wfqueue.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// This code was based on code from node-cron:
|
|
2
|
+
// This code was based on code from node-cron 4.6.0:
|
|
3
3
|
// https://github.com/node-cron/node-cron
|
|
4
4
|
/*
|
|
5
5
|
ISC License
|
|
@@ -18,7 +18,19 @@ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
|
18
18
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
19
19
|
*/
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.TimeMatcher = exports.
|
|
21
|
+
exports.TimeMatcher = exports.validateTimezone = exports.validateCrontab = exports.convertExpression = void 0;
|
|
22
|
+
const NICKNAMES = {
|
|
23
|
+
'@yearly': '0 0 1 1 *',
|
|
24
|
+
'@annually': '0 0 1 1 *',
|
|
25
|
+
'@monthly': '0 0 1 * *',
|
|
26
|
+
'@weekly': '0 0 * * 0',
|
|
27
|
+
'@daily': '0 0 * * *',
|
|
28
|
+
'@midnight': '0 0 * * *',
|
|
29
|
+
'@hourly': '0 * * * *',
|
|
30
|
+
};
|
|
31
|
+
function resolveNickname(expression) {
|
|
32
|
+
return NICKNAMES[expression.trim().toLowerCase()] ?? expression;
|
|
33
|
+
}
|
|
22
34
|
function removeExtraSpaces(str) {
|
|
23
35
|
return str.replace(/\s{2,}/g, ' ').trim();
|
|
24
36
|
}
|
|
@@ -28,6 +40,14 @@ function prependSecondExpression(expressions) {
|
|
|
28
40
|
}
|
|
29
41
|
return expressions;
|
|
30
42
|
}
|
|
43
|
+
// `?` is an alias for `*`, accepted only as a whole day-of-month or day-of-week field
|
|
44
|
+
function convertQuestionMarks(expressions) {
|
|
45
|
+
if (expressions[3] === '?')
|
|
46
|
+
expressions[3] = '*';
|
|
47
|
+
if (expressions[5] === '?')
|
|
48
|
+
expressions[5] = '*';
|
|
49
|
+
return expressions;
|
|
50
|
+
}
|
|
31
51
|
const months = [
|
|
32
52
|
'january',
|
|
33
53
|
'february',
|
|
@@ -63,29 +83,15 @@ function convertWeekDayName(weekExpression, items) {
|
|
|
63
83
|
return weekExpression;
|
|
64
84
|
}
|
|
65
85
|
function weekDayNamesConversion(expression) {
|
|
66
|
-
// Don't fold 7 (Sunday) into 0 here: pre-expansion it corrupts ranges like 5-7. See convertSundaySeven.
|
|
67
86
|
expression = convertWeekDayName(expression, weekDays);
|
|
68
87
|
return convertWeekDayName(expression, shortWeekDays);
|
|
69
88
|
}
|
|
70
|
-
//
|
|
71
|
-
function convertSundaySeven(expressions) {
|
|
72
|
-
const seen = new Set();
|
|
73
|
-
const folded = [];
|
|
74
|
-
for (const value of expressions[5].split(',')) {
|
|
75
|
-
const day = value === '7' ? '0' : value;
|
|
76
|
-
if (!seen.has(day)) {
|
|
77
|
-
seen.add(day);
|
|
78
|
-
folded.push(day);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
expressions[5] = folded.join(',');
|
|
82
|
-
return expressions;
|
|
83
|
-
}
|
|
89
|
+
// Converts every `*` token in a comma list, not just the first
|
|
84
90
|
function convertAsterisk(expression, replacement) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
return expression
|
|
92
|
+
.split(',')
|
|
93
|
+
.map((token) => (token.indexOf('*') !== -1 ? token.replace('*', replacement) : token))
|
|
94
|
+
.join(',');
|
|
89
95
|
}
|
|
90
96
|
// Based on position in full crontab, convert asterisk to appropriate range
|
|
91
97
|
function convertAsterisksToRanges(expressions) {
|
|
@@ -97,98 +103,193 @@ function convertAsterisksToRanges(expressions) {
|
|
|
97
103
|
expressions[5] = convertAsterisk(expressions[5], '0-6');
|
|
98
104
|
return expressions;
|
|
99
105
|
}
|
|
100
|
-
|
|
106
|
+
// Only whole `n-n` or `n-n/step` tokens are expanded; malformed ones are left for validation to reject
|
|
107
|
+
const rangeRegEx = /^(\d+)-(\d+)(?:\/(\d+))?$/;
|
|
108
|
+
const FIELD_BOUNDS = [
|
|
109
|
+
{ min: 0, max: 59 },
|
|
110
|
+
{ min: 0, max: 59 },
|
|
111
|
+
{ min: 0, max: 23 },
|
|
112
|
+
{ min: 1, max: 31 },
|
|
113
|
+
{ min: 1, max: 12 },
|
|
114
|
+
{ min: 0, max: 6 },
|
|
115
|
+
];
|
|
116
|
+
function expandRange(initTxt, endTxt, stepTxt, bounds) {
|
|
117
|
+
const step = parseInt(stepTxt, 10);
|
|
118
|
+
// A non-positive step would never terminate; leave the token for validation to reject.
|
|
119
|
+
if (!(step >= 1))
|
|
120
|
+
return `${initTxt}-${endTxt}/${stepTxt}`;
|
|
121
|
+
const first = parseInt(initTxt, 10);
|
|
122
|
+
const last = parseInt(endTxt, 10);
|
|
101
123
|
const numbers = [];
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
for (let i = first; i <= last; i++) {
|
|
109
|
-
numbers.push(i.toString());
|
|
124
|
+
if (first <= last) {
|
|
125
|
+
for (let i = first; i <= last; i += step) {
|
|
126
|
+
numbers.push(i);
|
|
127
|
+
}
|
|
128
|
+
return numbers.join();
|
|
110
129
|
}
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
// An inverted range wraps through the field's upper bound (e.g. hours `22-2` -> 22,23,0,1,2)
|
|
131
|
+
const { min, max } = bounds;
|
|
132
|
+
const size = max - min + 1;
|
|
133
|
+
const span = (((last - first) % size) + size) % size;
|
|
134
|
+
for (let offset = 0; offset <= span; offset += step) {
|
|
135
|
+
let value = first + offset;
|
|
136
|
+
if (value > max)
|
|
137
|
+
value -= size;
|
|
138
|
+
numbers.push(value);
|
|
119
139
|
}
|
|
120
|
-
return
|
|
140
|
+
return numbers.join();
|
|
141
|
+
}
|
|
142
|
+
function convertRange(expression, bounds) {
|
|
143
|
+
return expression
|
|
144
|
+
.split(',')
|
|
145
|
+
.map((token) => {
|
|
146
|
+
const match = rangeRegEx.exec(token.trim());
|
|
147
|
+
return match ? expandRange(match[1], match[2], match[3] || '1', bounds) : token;
|
|
148
|
+
})
|
|
149
|
+
.join();
|
|
121
150
|
}
|
|
122
151
|
function convertAllRanges(expressions) {
|
|
123
152
|
for (let i = 0; i < expressions.length; i++) {
|
|
124
|
-
expressions[i] = convertRange(expressions[i]);
|
|
153
|
+
expressions[i] = convertRange(expressions[i], FIELD_BOUNDS[i]);
|
|
125
154
|
}
|
|
126
155
|
return expressions;
|
|
127
156
|
}
|
|
128
|
-
|
|
129
|
-
const stepValuePattern = /^(.+)\/(\w+)$/;
|
|
130
|
-
for (let i = 0; i < expressions.length; i++) {
|
|
131
|
-
const match = stepValuePattern.exec(expressions[i]);
|
|
132
|
-
const isStepValue = match !== null && match.length > 0;
|
|
133
|
-
if (isStepValue) {
|
|
134
|
-
const baseDivider = match[2];
|
|
135
|
-
if (isNaN(parseInt(baseDivider))) {
|
|
136
|
-
throw new Error(baseDivider + ' is not a valid step value');
|
|
137
|
-
}
|
|
138
|
-
const values = match[1].split(',');
|
|
139
|
-
const stepValues = [];
|
|
140
|
-
const divider = parseInt(baseDivider, 10);
|
|
141
|
-
for (let j = 0; j <= values.length; j++) {
|
|
142
|
-
const value = parseInt(values[j], 10);
|
|
143
|
-
if (value % divider === 0) {
|
|
144
|
-
stepValues.push(`${value}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
expressions[i] = stepValues.join(',');
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return expressions;
|
|
151
|
-
}
|
|
152
|
-
// Function that takes care of normalization.
|
|
157
|
+
// Parses integers, keeping the L / L-n / nW / LW / nL / n#m tokens as uppercase literals
|
|
153
158
|
function normalizeIntegers(expressions) {
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
159
|
+
return expressions.map((expression) => expression.split(',').map((raw) => {
|
|
160
|
+
const token = raw.trim();
|
|
161
|
+
if (/^\d+$/.test(token))
|
|
162
|
+
return parseInt(token, 10);
|
|
163
|
+
if (/^l$/i.test(token) || /^l-\d{1,2}$/i.test(token) || /^[0-7]l$/i.test(token) || /w/i.test(token)) {
|
|
164
|
+
return token.toUpperCase();
|
|
158
165
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
// Anything else is kept verbatim so validation rejects it rather than parseInt truncating it
|
|
167
|
+
return token;
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
/*
|
|
171
|
+
* Converts a crontab into six arrays of allowed values, translating month and week day names,
|
|
172
|
+
* asterisks, ranges and steps into integers.
|
|
173
|
+
*
|
|
174
|
+
* Month names example:
|
|
175
|
+
* - expression 0 1 1 January,Sep *
|
|
176
|
+
* - Will be translated to 0 1 1 1,9 *
|
|
177
|
+
*
|
|
178
|
+
* Week day names example:
|
|
179
|
+
* - expression 0 1 1 2 Monday,Sat
|
|
180
|
+
* - Will be translated to 0 1 1 1,5 *
|
|
181
|
+
*
|
|
182
|
+
* Ranges example:
|
|
183
|
+
* - expression 1-5 * * * *
|
|
184
|
+
* - Will be translated to 1,2,3,4,5 * * * *
|
|
185
|
+
*/
|
|
186
|
+
function convertExpression(crontab) {
|
|
187
|
+
let expressions = removeExtraSpaces(resolveNickname(crontab)).split(' ');
|
|
188
|
+
expressions = prependSecondExpression(expressions);
|
|
189
|
+
expressions = convertQuestionMarks(expressions);
|
|
190
|
+
expressions[4] = monthNamesConversion(expressions[4]);
|
|
191
|
+
expressions[5] = weekDayNamesConversion(expressions[5]);
|
|
192
|
+
expressions = convertAsterisksToRanges(expressions);
|
|
193
|
+
expressions = convertAllRanges(expressions);
|
|
194
|
+
const fields = normalizeIntegers(expressions);
|
|
195
|
+
// Fold day-of-week 7 (Sunday) into 0 only after range expansion, so ranges like 5-7 survive
|
|
196
|
+
fields[5] = [
|
|
197
|
+
...new Set(fields[5].map((value) => {
|
|
198
|
+
if (value === 7)
|
|
199
|
+
return 0;
|
|
200
|
+
if (typeof value === 'string' && value.startsWith('7'))
|
|
201
|
+
return '0' + value.slice(1);
|
|
202
|
+
return value;
|
|
203
|
+
})),
|
|
204
|
+
];
|
|
205
|
+
return fields;
|
|
162
206
|
}
|
|
207
|
+
exports.convertExpression = convertExpression;
|
|
208
|
+
//////////
|
|
209
|
+
/// Validation
|
|
210
|
+
//////////
|
|
163
211
|
const validationRegex = /^(?:\d+|\*|\*\/\d+)$/;
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
// `#` is allowed for the day-of-week `n#m` token and `?` for the day-field alias
|
|
213
|
+
const ALLOWED_CHARS_REGEX = /^[a-zA-Z0-9-*/,#? ]+$/;
|
|
214
|
+
// Check a field's values to see if they are in range
|
|
215
|
+
function isValidExpression(values, min, max) {
|
|
216
|
+
for (const value of values) {
|
|
217
|
+
const valueAsInt = parseInt(`${value}`, 10);
|
|
218
|
+
if ((!Number.isNaN(valueAsInt) && (valueAsInt < min || valueAsInt > max)) || !validationRegex.test(`${value}`)) {
|
|
170
219
|
return false;
|
|
171
220
|
}
|
|
172
221
|
}
|
|
173
222
|
return true;
|
|
174
223
|
}
|
|
175
|
-
function isInvalidSecond(
|
|
176
|
-
return !isValidExpression(
|
|
224
|
+
function isInvalidSecond(values) {
|
|
225
|
+
return !isValidExpression(values, 0, 59);
|
|
177
226
|
}
|
|
178
|
-
function isInvalidMinute(
|
|
179
|
-
return !isValidExpression(
|
|
227
|
+
function isInvalidMinute(values) {
|
|
228
|
+
return !isValidExpression(values, 0, 59);
|
|
180
229
|
}
|
|
181
|
-
function isInvalidHour(
|
|
182
|
-
return !isValidExpression(
|
|
230
|
+
function isInvalidHour(values) {
|
|
231
|
+
return !isValidExpression(values, 0, 23);
|
|
183
232
|
}
|
|
184
|
-
|
|
185
|
-
|
|
233
|
+
const DAY_OF_MONTH_W_TOKEN = /^(\d{1,2}|L)W$/;
|
|
234
|
+
const DAY_OF_MONTH_OFFSET_TOKEN = /^L-(\d{1,2})$/;
|
|
235
|
+
function isInvalidDayOfMonth(values) {
|
|
236
|
+
// L, LW, nW (1-31) and L-n (1-30) are valid only in this field; everything else must be a day number
|
|
237
|
+
const days = values.filter((value) => {
|
|
238
|
+
if (value === 'L')
|
|
239
|
+
return false;
|
|
240
|
+
const weekday = DAY_OF_MONTH_W_TOKEN.exec(`${value}`);
|
|
241
|
+
if (weekday) {
|
|
242
|
+
if (weekday[1] === 'L')
|
|
243
|
+
return false;
|
|
244
|
+
const target = parseInt(weekday[1], 10);
|
|
245
|
+
return target < 1 || target > 31;
|
|
246
|
+
}
|
|
247
|
+
const offset = DAY_OF_MONTH_OFFSET_TOKEN.exec(`${value}`);
|
|
248
|
+
if (offset) {
|
|
249
|
+
const n = parseInt(offset[1], 10);
|
|
250
|
+
return n < 1 || n > 30;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
});
|
|
254
|
+
return !isValidExpression(days, 1, 31);
|
|
186
255
|
}
|
|
187
|
-
|
|
188
|
-
|
|
256
|
+
// `W` is only valid on a single day (`15W`) or `L` (`LW`); checked before range expansion hides misuse like `1-15W`
|
|
257
|
+
function hasInvalidWModifier(rawDayOfMonth) {
|
|
258
|
+
if (!/w/i.test(rawDayOfMonth))
|
|
259
|
+
return false;
|
|
260
|
+
return rawDayOfMonth.split(',').some((token) => {
|
|
261
|
+
const value = token.trim();
|
|
262
|
+
return /w/i.test(value) && !/^(\d{1,2}|L)W$/i.test(value);
|
|
263
|
+
});
|
|
189
264
|
}
|
|
190
|
-
function
|
|
191
|
-
return !isValidExpression(
|
|
265
|
+
function isInvalidMonth(values) {
|
|
266
|
+
return !isValidExpression(values, 1, 12);
|
|
267
|
+
}
|
|
268
|
+
function isInvalidWeekDay(values) {
|
|
269
|
+
// `n#m` (mth n-day of the month) and `nL` (last n-day of the month) are valid only in this field
|
|
270
|
+
const days = values.filter((value) => !isNthWeekdayToken(value) && !/^[0-7]L$/.test(`${value}`));
|
|
271
|
+
return !isValidExpression(days, 0, 7);
|
|
272
|
+
}
|
|
273
|
+
// The last day each month can ever reach, counting February as 29
|
|
274
|
+
const MAX_DAYS_IN_MONTH = {
|
|
275
|
+
1: 31,
|
|
276
|
+
2: 29,
|
|
277
|
+
3: 31,
|
|
278
|
+
4: 30,
|
|
279
|
+
5: 31,
|
|
280
|
+
6: 30,
|
|
281
|
+
7: 31,
|
|
282
|
+
8: 31,
|
|
283
|
+
9: 30,
|
|
284
|
+
10: 31,
|
|
285
|
+
11: 30,
|
|
286
|
+
12: 31,
|
|
287
|
+
};
|
|
288
|
+
// True when no listed day exists in any listed month (e.g. `30 2`), ignoring date-dependent tokens
|
|
289
|
+
function isImpossibleDayOfMonth(days, months) {
|
|
290
|
+
if (days.some((day) => typeof day !== 'number'))
|
|
291
|
+
return false;
|
|
292
|
+
return !months.some((month) => days.some((day) => day <= MAX_DAYS_IN_MONTH[month]));
|
|
192
293
|
}
|
|
193
294
|
function validateFields(patterns, executablePatterns) {
|
|
194
295
|
if (isInvalidSecond(executablePatterns[0]))
|
|
@@ -197,26 +298,35 @@ function validateFields(patterns, executablePatterns) {
|
|
|
197
298
|
throw new Error(`${patterns[1]} is a invalid expression for minute`);
|
|
198
299
|
if (isInvalidHour(executablePatterns[2]))
|
|
199
300
|
throw new Error(`${patterns[2]} is a invalid expression for hour`);
|
|
200
|
-
if (isInvalidDayOfMonth(executablePatterns[3]))
|
|
301
|
+
if (isInvalidDayOfMonth(executablePatterns[3]) || hasInvalidWModifier(patterns[3]))
|
|
201
302
|
throw new Error(`${patterns[3]} is a invalid expression for day of month`);
|
|
202
303
|
if (isInvalidMonth(executablePatterns[4]))
|
|
203
304
|
throw new Error(`${patterns[4]} is a invalid expression for month`);
|
|
204
305
|
if (isInvalidWeekDay(executablePatterns[5]))
|
|
205
306
|
throw new Error(`${patterns[5]} is a invalid expression for week day`);
|
|
307
|
+
if (isImpossibleDayOfMonth(executablePatterns[3], executablePatterns[4]))
|
|
308
|
+
throw new Error(`${patterns[3]} ${patterns[4]} is an impossible day of month for the given month`);
|
|
309
|
+
}
|
|
310
|
+
function parseCrontab(pattern) {
|
|
311
|
+
if (typeof pattern !== 'string')
|
|
312
|
+
throw new TypeError('pattern must be a string!');
|
|
313
|
+
const resolved = resolveNickname(pattern);
|
|
314
|
+
if (!ALLOWED_CHARS_REGEX.test(resolved))
|
|
315
|
+
throw new TypeError('pattern includes illegal characters!');
|
|
316
|
+
const raw = removeExtraSpaces(resolved).split(' ');
|
|
317
|
+
if (raw.length !== 5 && raw.length !== 6)
|
|
318
|
+
throw new Error(`expected 5 or 6 fields but got ${raw.length}`);
|
|
319
|
+
const patterns = raw.length === 5 ? ['0', ...raw] : raw;
|
|
320
|
+
const executablePatterns = convertExpression(resolved);
|
|
321
|
+
validateFields(patterns, executablePatterns);
|
|
322
|
+
return executablePatterns;
|
|
206
323
|
}
|
|
207
324
|
/**
|
|
208
325
|
* Validates a Cron-Job expression pattern.
|
|
209
326
|
* Throws on error.
|
|
210
327
|
*/
|
|
211
328
|
function validateCrontab(pattern) {
|
|
212
|
-
|
|
213
|
-
throw new TypeError('pattern must be a string!');
|
|
214
|
-
const patterns = pattern.split(' ');
|
|
215
|
-
const executablePatterns = convertExpression(pattern).split(' ');
|
|
216
|
-
if (patterns.length === 5)
|
|
217
|
-
patterns.unshift('0');
|
|
218
|
-
validateFields(patterns, executablePatterns);
|
|
219
|
-
return executablePatterns.join(' ');
|
|
329
|
+
parseCrontab(pattern);
|
|
220
330
|
}
|
|
221
331
|
exports.validateCrontab = validateCrontab;
|
|
222
332
|
/**
|
|
@@ -232,103 +342,230 @@ function validateTimezone(timezone) {
|
|
|
232
342
|
}
|
|
233
343
|
}
|
|
234
344
|
exports.validateTimezone = validateTimezone;
|
|
235
|
-
/*
|
|
236
|
-
* The node-cron core allows only numbers (including multiple numbers e.g 1,2).
|
|
237
|
-
* This module is going to translate the month names, week day names and ranges
|
|
238
|
-
* to integers relatives.
|
|
239
|
-
*
|
|
240
|
-
* Month names example:
|
|
241
|
-
* - expression 0 1 1 January,Sep *
|
|
242
|
-
* - Will be translated to 0 1 1 1,9 *
|
|
243
|
-
*
|
|
244
|
-
* Week day names example:
|
|
245
|
-
* - expression 0 1 1 2 Monday,Sat
|
|
246
|
-
* - Will be translated to 0 1 1 1,5 *
|
|
247
|
-
*
|
|
248
|
-
* Ranges example:
|
|
249
|
-
* - expression 1-5 * * * *
|
|
250
|
-
* - Will be translated to 1,2,3,4,5 * * * *
|
|
251
|
-
*/
|
|
252
|
-
function convertExpression(crontab) {
|
|
253
|
-
let expressions = removeExtraSpaces(crontab).split(' ');
|
|
254
|
-
expressions = prependSecondExpression(expressions);
|
|
255
|
-
expressions[4] = monthNamesConversion(expressions[4]);
|
|
256
|
-
expressions[5] = weekDayNamesConversion(expressions[5]);
|
|
257
|
-
expressions = convertAsterisksToRanges(expressions);
|
|
258
|
-
expressions = convertAllRanges(expressions);
|
|
259
|
-
expressions = convertSteps(expressions);
|
|
260
|
-
expressions = normalizeIntegers(expressions);
|
|
261
|
-
expressions = convertSundaySeven(expressions);
|
|
262
|
-
return expressions.join(' ');
|
|
263
|
-
}
|
|
264
|
-
exports.convertExpression = convertExpression;
|
|
265
345
|
//////////
|
|
266
|
-
///
|
|
346
|
+
/// Day matching
|
|
267
347
|
//////////
|
|
268
|
-
function
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
348
|
+
function weekdayOf(year, month, day) {
|
|
349
|
+
return new Date(Date.UTC(year, month - 1, day)).getUTCDay();
|
|
350
|
+
}
|
|
351
|
+
function lastDayOfMonth(year, month) {
|
|
352
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
353
|
+
}
|
|
354
|
+
// The nearest weekday (Mon-Fri) to `target` without leaving the month, or -1 if the month has no such day
|
|
355
|
+
function nearestWeekday(year, month, target) {
|
|
356
|
+
const last = lastDayOfMonth(year, month);
|
|
357
|
+
if (target < 1 || target > last)
|
|
358
|
+
return -1;
|
|
359
|
+
const weekday = weekdayOf(year, month, target);
|
|
360
|
+
if (weekday === 6)
|
|
361
|
+
return target === 1 ? target + 2 : target - 1;
|
|
362
|
+
if (weekday === 0)
|
|
363
|
+
return target === last ? target - 2 : target + 1;
|
|
364
|
+
return target;
|
|
365
|
+
}
|
|
366
|
+
function matchesDayOfMonth(field, year, month, day) {
|
|
367
|
+
for (const value of field) {
|
|
368
|
+
if (value === day)
|
|
369
|
+
return true;
|
|
370
|
+
if (typeof value !== 'string')
|
|
371
|
+
continue;
|
|
372
|
+
if (value === 'L' && day === lastDayOfMonth(year, month))
|
|
373
|
+
return true;
|
|
374
|
+
const weekdayMatch = DAY_OF_MONTH_W_TOKEN.exec(value);
|
|
375
|
+
if (weekdayMatch) {
|
|
376
|
+
const target = weekdayMatch[1] === 'L' ? lastDayOfMonth(year, month) : parseInt(weekdayMatch[1], 10);
|
|
377
|
+
if (nearestWeekday(year, month, target) === day)
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
const offsetMatch = DAY_OF_MONTH_OFFSET_TOKEN.exec(value);
|
|
381
|
+
if (offsetMatch) {
|
|
382
|
+
const target = lastDayOfMonth(year, month) - parseInt(offsetMatch[1], 10);
|
|
383
|
+
if (target >= 1 && target === day)
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
273
386
|
}
|
|
274
|
-
return
|
|
387
|
+
return false;
|
|
275
388
|
}
|
|
389
|
+
const LAST_WEEKDAY_REGEX = /^([0-7])L$/;
|
|
390
|
+
const NTH_WEEKDAY_REGEX = /^([0-7])#([1-5])$/;
|
|
391
|
+
function isNthWeekdayToken(value) {
|
|
392
|
+
return typeof value === 'string' && NTH_WEEKDAY_REGEX.test(value);
|
|
393
|
+
}
|
|
394
|
+
function matchesDayOfWeek(field, year, month, day) {
|
|
395
|
+
const weekday = weekdayOf(year, month, day);
|
|
396
|
+
for (const value of field) {
|
|
397
|
+
if (value === weekday)
|
|
398
|
+
return true;
|
|
399
|
+
if (typeof value !== 'string')
|
|
400
|
+
continue;
|
|
401
|
+
const nth = NTH_WEEKDAY_REGEX.exec(value);
|
|
402
|
+
if (nth) {
|
|
403
|
+
if (parseInt(nth[1], 10) % 7 === weekday && Math.floor((day - 1) / 7) + 1 === parseInt(nth[2], 10))
|
|
404
|
+
return true;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const last = LAST_WEEKDAY_REGEX.exec(value);
|
|
408
|
+
if (last && parseInt(last[1], 10) % 7 === weekday && day + 7 > lastDayOfMonth(year, month))
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
414
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
415
|
+
// Constructing a DateTimeFormat is expensive, so reuse one per timezone
|
|
416
|
+
const formatters = new Map();
|
|
417
|
+
function getFormatter(timezone) {
|
|
418
|
+
const key = timezone ?? '';
|
|
419
|
+
let formatter = formatters.get(key);
|
|
420
|
+
if (!formatter) {
|
|
421
|
+
formatter = new Intl.DateTimeFormat('en-US', {
|
|
422
|
+
year: 'numeric',
|
|
423
|
+
month: 'numeric',
|
|
424
|
+
day: 'numeric',
|
|
425
|
+
hour: 'numeric',
|
|
426
|
+
minute: 'numeric',
|
|
427
|
+
second: 'numeric',
|
|
428
|
+
hourCycle: 'h23',
|
|
429
|
+
timeZone: timezone,
|
|
430
|
+
});
|
|
431
|
+
formatters.set(key, formatter);
|
|
432
|
+
}
|
|
433
|
+
return formatter;
|
|
434
|
+
}
|
|
435
|
+
function wallClock(ms, timezone) {
|
|
436
|
+
const parts = {};
|
|
437
|
+
for (const part of getFormatter(timezone).formatToParts(ms)) {
|
|
438
|
+
if (part.type !== 'literal')
|
|
439
|
+
parts[part.type] = parseInt(part.value, 10);
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
year: parts.year,
|
|
443
|
+
month: parts.month,
|
|
444
|
+
day: parts.day,
|
|
445
|
+
hour: parts.hour % 24,
|
|
446
|
+
minute: parts.minute,
|
|
447
|
+
second: parts.second,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
// The timezone's UTC offset (local minus UTC) in effect at the given instant
|
|
451
|
+
function offsetAt(ms, timezone) {
|
|
452
|
+
const w = wallClock(ms, timezone);
|
|
453
|
+
return Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second) - Math.floor(ms / 1000) * 1000;
|
|
454
|
+
}
|
|
455
|
+
// A century is far beyond any real recurrence, so a schedule not found within it never fires
|
|
456
|
+
const MAX_SEARCH_DAYS = 366 * 100;
|
|
276
457
|
class TimeMatcher {
|
|
277
|
-
#pattern;
|
|
278
|
-
#expressions;
|
|
279
|
-
#dtf;
|
|
280
458
|
#timezone;
|
|
459
|
+
#seconds;
|
|
460
|
+
#minutes;
|
|
461
|
+
#hours;
|
|
462
|
+
#daysOfMonth;
|
|
463
|
+
#months;
|
|
464
|
+
#daysOfWeek;
|
|
465
|
+
#dayOffsets;
|
|
281
466
|
constructor(pattern, timezone) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
this.#timezone = timezone;
|
|
285
|
-
|
|
286
|
-
this.#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
second: '2-digit',
|
|
294
|
-
hourCycle: 'h23',
|
|
295
|
-
fractionalSecondDigits: 3,
|
|
296
|
-
timeZone: this.#timezone,
|
|
297
|
-
})
|
|
298
|
-
: null;
|
|
467
|
+
const fields = parseCrontab(pattern);
|
|
468
|
+
// An empty timezone means the process's local timezone
|
|
469
|
+
this.#timezone = timezone || undefined;
|
|
470
|
+
// Fail fast on an invalid timezone
|
|
471
|
+
getFormatter(this.#timezone);
|
|
472
|
+
this.#seconds = [...fields[0]].sort((a, b) => a - b);
|
|
473
|
+
this.#minutes = [...fields[1]].sort((a, b) => a - b);
|
|
474
|
+
this.#hours = [...fields[2]].sort((a, b) => a - b);
|
|
475
|
+
this.#daysOfMonth = fields[3];
|
|
476
|
+
this.#months = fields[4];
|
|
477
|
+
this.#daysOfWeek = fields[5];
|
|
299
478
|
}
|
|
300
479
|
match(date) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return runOnSecond && runOnMinute && runOnHour && runOnDay;
|
|
480
|
+
const w = wallClock(typeof date === 'number' ? date : date.getTime(), this.#timezone);
|
|
481
|
+
return (this.#seconds.includes(w.second) &&
|
|
482
|
+
this.#minutes.includes(w.minute) &&
|
|
483
|
+
this.#hours.includes(w.hour) &&
|
|
484
|
+
this.#runsThisDay(w.year, w.month, w.day));
|
|
307
485
|
}
|
|
308
|
-
#runsThisDay(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return runOnDay && runOnMonth && runOnWeekDay;
|
|
486
|
+
#runsThisDay(year, month, day) {
|
|
487
|
+
return (this.#months.includes(month) &&
|
|
488
|
+
matchesDayOfMonth(this.#daysOfMonth, year, month, day) &&
|
|
489
|
+
matchesDayOfWeek(this.#daysOfWeek, year, month, day));
|
|
313
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Returns the first matching instant strictly after `date`, walking the calendar day by day
|
|
493
|
+
* instead of scanning every second. If the schedule can never fire, returns a non-matching
|
|
494
|
+
* instant at the end of the search horizon.
|
|
495
|
+
*/
|
|
314
496
|
nextWakeupTime(date) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
let
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
497
|
+
const baseMs = typeof date === 'number' ? date : date.getTime();
|
|
498
|
+
let { year, month, day } = wallClock(baseMs, this.#timezone);
|
|
499
|
+
for (let i = 0; i < MAX_SEARCH_DAYS; i++) {
|
|
500
|
+
if (this.#runsThisDay(year, month, day)) {
|
|
501
|
+
const next = this.#firstMatchOnDay(year, month, day, baseMs);
|
|
502
|
+
if (next !== undefined)
|
|
503
|
+
return new Date(next);
|
|
504
|
+
}
|
|
505
|
+
const nextDay = new Date(Date.UTC(year, month - 1, day + 1));
|
|
506
|
+
year = nextDay.getUTCFullYear();
|
|
507
|
+
month = nextDay.getUTCMonth() + 1;
|
|
508
|
+
day = nextDay.getUTCDate();
|
|
509
|
+
}
|
|
510
|
+
return new Date(baseMs + MAX_SEARCH_DAYS * DAY_MS);
|
|
511
|
+
}
|
|
512
|
+
// The offset before and after the (at most one) UTC offset change affecting this local day
|
|
513
|
+
#offsetsForDay(dayStart) {
|
|
514
|
+
if (this.#dayOffsets?.dayStart === dayStart)
|
|
515
|
+
return this.#dayOffsets;
|
|
516
|
+
// Every instant whose local date is this day lies within this window, as offsets span UTC-12 to UTC+14
|
|
517
|
+
let lo = dayStart - 14 * HOUR_MS;
|
|
518
|
+
let hi = dayStart + DAY_MS + 12 * HOUR_MS;
|
|
519
|
+
const before = offsetAt(lo, this.#timezone);
|
|
520
|
+
const after = offsetAt(hi, this.#timezone);
|
|
521
|
+
if (before !== after) {
|
|
522
|
+
// Binary search for the first second on the new offset
|
|
523
|
+
while (hi - lo > 1000) {
|
|
524
|
+
const mid = lo + Math.floor((hi - lo) / 2000) * 1000;
|
|
525
|
+
if (offsetAt(mid, this.#timezone) === before)
|
|
526
|
+
lo = mid;
|
|
527
|
+
else
|
|
528
|
+
hi = mid;
|
|
529
|
+
}
|
|
324
530
|
}
|
|
325
|
-
|
|
531
|
+
this.#dayOffsets = { dayStart, before, after, transition: hi };
|
|
532
|
+
return this.#dayOffsets;
|
|
326
533
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
534
|
+
// Earliest instant after baseMs whose local time on this day matches the time-of-day fields
|
|
535
|
+
#firstMatchOnDay(year, month, day, baseMs) {
|
|
536
|
+
const dayStart = Date.UTC(year, month - 1, day);
|
|
537
|
+
const { before, after, transition } = this.#offsetsForDay(dayStart);
|
|
538
|
+
const minOffset = Math.min(before, after);
|
|
539
|
+
const maxOffset = Math.max(before, after);
|
|
540
|
+
// Local times are ascending, so stop once no later one can map to an instant before `best`
|
|
541
|
+
let best = Infinity;
|
|
542
|
+
for (const hour of this.#hours) {
|
|
543
|
+
const hourStart = dayStart + hour * HOUR_MS;
|
|
544
|
+
if (hourStart - maxOffset >= best)
|
|
545
|
+
break;
|
|
546
|
+
if (hourStart + HOUR_MS - minOffset <= baseMs)
|
|
547
|
+
continue;
|
|
548
|
+
for (const minute of this.#minutes) {
|
|
549
|
+
const minuteStart = hourStart + minute * 60_000;
|
|
550
|
+
if (minuteStart - maxOffset >= best)
|
|
551
|
+
break;
|
|
552
|
+
if (minuteStart + 60_000 - minOffset <= baseMs)
|
|
553
|
+
continue;
|
|
554
|
+
for (const second of this.#seconds) {
|
|
555
|
+
const local = minuteStart + second * 1000;
|
|
556
|
+
if (local - maxOffset >= best)
|
|
557
|
+
break;
|
|
558
|
+
// A local time repeated by a fall-back exists under both offsets; one skipped by a spring-forward under neither
|
|
559
|
+
const withBefore = local - before;
|
|
560
|
+
if (withBefore > baseMs && withBefore < best && withBefore < transition)
|
|
561
|
+
best = withBefore;
|
|
562
|
+
const withAfter = local - after;
|
|
563
|
+
if (withAfter > baseMs && withAfter < best && withAfter >= transition)
|
|
564
|
+
best = withAfter;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
330
567
|
}
|
|
331
|
-
return
|
|
568
|
+
return best === Infinity ? undefined : best;
|
|
332
569
|
}
|
|
333
570
|
}
|
|
334
571
|
exports.TimeMatcher = TimeMatcher;
|