croner 6.0.0-dev.0 → 6.0.0-dev.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/dist/croner.cjs +1653 -0
- package/package.json +2 -3
package/dist/croner.cjs
ADDED
|
@@ -0,0 +1,1653 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* ------------------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
minitz - MIT License - Hexagon <hexagon@56k.guru>
|
|
6
|
+
|
|
7
|
+
Version 4.0.4
|
|
8
|
+
|
|
9
|
+
------------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
License:
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2022 Hexagon <hexagon@56k.guru>
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
The above copyright notice and this permission notice shall be included in
|
|
22
|
+
all copies or substantial portions of the Software.
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
29
|
+
THE SOFTWARE.
|
|
30
|
+
|
|
31
|
+
------------------------------------------------------------------------------------ */
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {Object} TimePoint
|
|
35
|
+
* @property {Number} y - 1970--
|
|
36
|
+
* @property {Number} m - 1-12
|
|
37
|
+
* @property {Number} d - 1-31
|
|
38
|
+
* @property {Number} h - 0-24
|
|
39
|
+
* @property {Number} i - 0-60 Minute
|
|
40
|
+
* @property {Number} s - 0-60
|
|
41
|
+
* @property {string} tz - Time zone in IANA database format 'Europe/Stockholm'
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Converts a date/time from a specific timezone to a normal date object using the system local time
|
|
46
|
+
*
|
|
47
|
+
* Shortcut for minitz.fromTZ(minitz.tp(...));
|
|
48
|
+
*
|
|
49
|
+
* @constructor
|
|
50
|
+
*
|
|
51
|
+
* @param {Number} y - 1970--
|
|
52
|
+
* @param {Number} m - 1-12
|
|
53
|
+
* @param {Number} d - 1-31
|
|
54
|
+
* @param {Number} h - 0-24
|
|
55
|
+
* @param {Number} i - 0-60 Minute
|
|
56
|
+
* @param {Number} s - 0-60
|
|
57
|
+
* @param {string} tz - Time zone in IANA database format 'Europe/Stockholm'
|
|
58
|
+
* @param {boolean} [throwOnInvalid] - Default is to return the adjusted time if the call happens during a Daylight-Saving-Time switch.
|
|
59
|
+
* E.g. Value "01:01:01" is returned if input time is 00:01:01 while one hour got actually
|
|
60
|
+
* skipped, going from 23:59:59 to 01:00:00. Setting this flag makes the library throw an exception instead.
|
|
61
|
+
* @returns {date} - Normal date object with correct UTC and system local time
|
|
62
|
+
*
|
|
63
|
+
*/
|
|
64
|
+
function minitz(y, m, d, h, i, s, tz, throwOnInvalid) {
|
|
65
|
+
return minitz.fromTZ(minitz.tp(y, m, d, h, i, s, tz), throwOnInvalid);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Converts a date/time from a specific timezone to a normal date object using the system local time
|
|
70
|
+
*
|
|
71
|
+
* @public
|
|
72
|
+
* @static
|
|
73
|
+
*
|
|
74
|
+
* @param {string} localTimeStr - ISO8601 formatted local time string, non UTC
|
|
75
|
+
* @param {string} tz - Time zone in IANA database format 'Europe/Stockholm'
|
|
76
|
+
* @param {boolean} [throwOnInvalid] - Default is to return the adjusted time if the call happens during a Daylight-Saving-Time switch.
|
|
77
|
+
* E.g. Value "01:01:01" is returned if input time is 00:01:01 while one hour got actually
|
|
78
|
+
* skipped, going from 23:59:59 to 01:00:00. Setting this flag makes the library throw an exception instead.
|
|
79
|
+
* @return {date} - Normal date object
|
|
80
|
+
*
|
|
81
|
+
*/
|
|
82
|
+
minitz.fromTZISO = (localTimeStr, tz, throwOnInvalid) => {
|
|
83
|
+
return minitz.fromTZ(parseISOLocal(localTimeStr, tz), throwOnInvalid);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Converts a date/time from a specific timezone to a normal date object using the system local time
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
* @static
|
|
91
|
+
*
|
|
92
|
+
* @param {TimePoint} tp - Object with specified timezone
|
|
93
|
+
* @param {boolean} [throwOnInvalid] - Default is to return the adjusted time if the call happens during a Daylight-Saving-Time switch.
|
|
94
|
+
* E.g. Value "01:01:01" is returned if input time is 00:01:01 while one hour got actually
|
|
95
|
+
* skipped, going from 23:59:59 to 01:00:00. Setting this flag makes the library throw an exception instead.
|
|
96
|
+
* @returns {date} - Normal date object
|
|
97
|
+
*/
|
|
98
|
+
minitz.fromTZ = function(tp, throwOnInvalid) {
|
|
99
|
+
|
|
100
|
+
const
|
|
101
|
+
|
|
102
|
+
// Construct a fake Date object with UTC date/time set to local date/time in source timezone
|
|
103
|
+
inDate = new Date(Date.UTC(
|
|
104
|
+
tp.y,
|
|
105
|
+
tp.m - 1,
|
|
106
|
+
tp.d,
|
|
107
|
+
tp.h,
|
|
108
|
+
tp.i,
|
|
109
|
+
tp.s
|
|
110
|
+
)),
|
|
111
|
+
|
|
112
|
+
// Get offset between UTC and source timezone
|
|
113
|
+
offset = getTimezoneOffset(tp.tz, inDate),
|
|
114
|
+
|
|
115
|
+
// Remove offset from inDate to hopefully get a true date object
|
|
116
|
+
dateGuess = new Date(inDate.getTime() - offset),
|
|
117
|
+
|
|
118
|
+
// Get offset between UTC and guessed time in target timezone
|
|
119
|
+
dateOffsGuess = getTimezoneOffset(tp.tz, dateGuess);
|
|
120
|
+
|
|
121
|
+
// If offset between guessed true date object and UTC matches initial calculation, the guess
|
|
122
|
+
// was spot on
|
|
123
|
+
if ((dateOffsGuess - offset) === 0) {
|
|
124
|
+
return dateGuess;
|
|
125
|
+
} else {
|
|
126
|
+
// Not quite there yet, make a second try on guessing the local time, adjust by the offset indicated by the previous guess
|
|
127
|
+
// Try recreating input time again
|
|
128
|
+
// Then calculate and check the offset again
|
|
129
|
+
const
|
|
130
|
+
dateGuess2 = new Date(inDate.getTime() - dateOffsGuess),
|
|
131
|
+
dateOffsGuess2 = getTimezoneOffset(tp.tz, dateGuess2);
|
|
132
|
+
if ((dateOffsGuess2 - dateOffsGuess) === 0) {
|
|
133
|
+
// All good, return local time
|
|
134
|
+
return dateGuess2;
|
|
135
|
+
} else if(!throwOnInvalid && (dateOffsGuess2 - dateOffsGuess) > 0) {
|
|
136
|
+
// We're most probably dealing with a DST transition where we should use the offset of the second guess
|
|
137
|
+
return dateGuess2;
|
|
138
|
+
} else if (!throwOnInvalid) {
|
|
139
|
+
// We're most probably dealing with a DST transition where we should use the offset of the initial guess
|
|
140
|
+
return dateGuess;
|
|
141
|
+
} else {
|
|
142
|
+
// Input time is invalid, and the library is instructed to throw, so let's do it
|
|
143
|
+
throw new Error("Invalid date passed to fromTZ()");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Converts a date to a specific time zone and returns an object containing year, month,
|
|
150
|
+
* day, hour, (...) and timezone used for the conversion
|
|
151
|
+
*
|
|
152
|
+
* **Please note**: If you just want to _display_ date/time in another
|
|
153
|
+
* time zone, use vanilla JS. See the example below.
|
|
154
|
+
*
|
|
155
|
+
* @public
|
|
156
|
+
* @static
|
|
157
|
+
*
|
|
158
|
+
* @param {d} date - Input date
|
|
159
|
+
* @param {string} [tzStr] - Timezone string in Europe/Stockholm format
|
|
160
|
+
*
|
|
161
|
+
* @returns {TimePoint}
|
|
162
|
+
*
|
|
163
|
+
* @example <caption>Example using minitz:</caption>
|
|
164
|
+
* let normalDate = new Date(); // d is a normal Date instance, with local timezone and correct utc representation
|
|
165
|
+
*
|
|
166
|
+
* tzDate = minitz.toTZ(d, 'America/New_York');
|
|
167
|
+
*
|
|
168
|
+
* // Will result in the following object:
|
|
169
|
+
* // {
|
|
170
|
+
* // y: 2022,
|
|
171
|
+
* // m: 9,
|
|
172
|
+
* // d: 28,
|
|
173
|
+
* // h: 13,
|
|
174
|
+
* // i: 28,
|
|
175
|
+
* // s: 28,
|
|
176
|
+
* // tz: "America/New_York"
|
|
177
|
+
* // }
|
|
178
|
+
*
|
|
179
|
+
* @example <caption>Example using vanilla js:</caption>
|
|
180
|
+
* console.log(
|
|
181
|
+
* // Display current time in America/New_York, using sv-SE locale
|
|
182
|
+
* new Date().toLocaleTimeString("sv-SE", { timeZone: "America/New_York" }),
|
|
183
|
+
* );
|
|
184
|
+
*
|
|
185
|
+
*/
|
|
186
|
+
minitz.toTZ = function (d, tzStr) {
|
|
187
|
+
const td = new Date(d.toLocaleString("sv-SE", {timeZone: tzStr}));
|
|
188
|
+
return {
|
|
189
|
+
y: td.getFullYear(),
|
|
190
|
+
m: td.getMonth() + 1,
|
|
191
|
+
d: td.getDate(),
|
|
192
|
+
h: td.getHours(),
|
|
193
|
+
i: td.getMinutes(),
|
|
194
|
+
s: td.getSeconds(),
|
|
195
|
+
tz: tzStr
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Convenience function which returns a TimePoint object for later use in fromTZ
|
|
201
|
+
*
|
|
202
|
+
* @public
|
|
203
|
+
* @static
|
|
204
|
+
*
|
|
205
|
+
* @param {Number} y - 1970--
|
|
206
|
+
* @param {Number} m - 1-12
|
|
207
|
+
* @param {Number} d - 1-31
|
|
208
|
+
* @param {Number} h - 0-24
|
|
209
|
+
* @param {Number} i - 0-60 Minute
|
|
210
|
+
* @param {Number} s - 0-60
|
|
211
|
+
* @param {string} tz - Time zone in format 'Europe/Stockholm'
|
|
212
|
+
*
|
|
213
|
+
* @returns {TimePoint}
|
|
214
|
+
*
|
|
215
|
+
*/
|
|
216
|
+
minitz.tp = (y,m,d,h,i,s,tz) => { return { y, m, d, h, i, s, tz: tz }; };
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Helper function that returns the current UTC offset (in ms) for a specific timezone at a specific point in time
|
|
220
|
+
*
|
|
221
|
+
* @private
|
|
222
|
+
*
|
|
223
|
+
* @param {timeZone} string - Target time zone in IANA database format 'Europe/Stockholm'
|
|
224
|
+
* @param {date} [date] - Point in time to use as base for offset calculation
|
|
225
|
+
*
|
|
226
|
+
* @returns {number} - Offset in ms between UTC and timeZone
|
|
227
|
+
*/
|
|
228
|
+
function getTimezoneOffset(timeZone, date = new Date()) {
|
|
229
|
+
|
|
230
|
+
// Get timezone
|
|
231
|
+
const tz = date.toLocaleString("en", {timeZone, timeStyle: "long"}).split(" ").slice(-1)[0];
|
|
232
|
+
|
|
233
|
+
// Extract time in en-US format
|
|
234
|
+
// - replace narrow no break space with regular space to compensate for bug in Node.js 19.1
|
|
235
|
+
const dateString = date.toLocaleString("en-US").replace(/[\u202f]/," ");
|
|
236
|
+
|
|
237
|
+
// Check ms offset between GMT and extracted timezone
|
|
238
|
+
return Date.parse(`${dateString} GMT`) - Date.parse(`${dateString} ${tz}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Helper function that takes a ISO8001 local date time string and creates a Date object.
|
|
244
|
+
* Throws on failure. Throws on invalid date or time.
|
|
245
|
+
*
|
|
246
|
+
* @private
|
|
247
|
+
*
|
|
248
|
+
* @param {string} dtStr - an ISO 8601 format date and time string
|
|
249
|
+
* with all components, e.g. 2015-11-24T19:40:00
|
|
250
|
+
* @returns {TimePoint} - TimePoint instance from parsing the string
|
|
251
|
+
*/
|
|
252
|
+
function parseISOLocal(dtStr, tz) {
|
|
253
|
+
|
|
254
|
+
// Parse date using built in Date.parse
|
|
255
|
+
const pd = new Date(Date.parse(dtStr));
|
|
256
|
+
|
|
257
|
+
// Check for completeness
|
|
258
|
+
if (isNaN(pd)) {
|
|
259
|
+
throw new Error("minitz: Invalid ISO8601 passed to parser.");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// If
|
|
263
|
+
// * date/time is specified in UTC (Z-flag included)
|
|
264
|
+
// * or UTC offset is specified (+ or - included after character 9 (20200101 or 2020-01-0))
|
|
265
|
+
// Return time in utc, else return local time and include timezone identifier
|
|
266
|
+
const stringEnd = dtStr.substring(9);
|
|
267
|
+
if (dtStr.includes("Z") || stringEnd.includes("-") || stringEnd.includes("+")) {
|
|
268
|
+
return minitz.tp(pd.getUTCFullYear(), pd.getUTCMonth()+1, pd.getUTCDate(),pd.getUTCHours(), pd.getUTCMinutes(),pd.getUTCSeconds(), "Etc/UTC");
|
|
269
|
+
} else {
|
|
270
|
+
return minitz.tp(pd.getFullYear(), pd.getMonth()+1, pd.getDate(),pd.getHours(), pd.getMinutes(),pd.getSeconds(), tz);
|
|
271
|
+
}
|
|
272
|
+
// Treat date as local time, in target timezone
|
|
273
|
+
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
minitz.minitz = minitz;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @callback CatchCallbackFn
|
|
280
|
+
* @param {unknown} e
|
|
281
|
+
* @param {Cron} job
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @callback ProtectCallbackFn
|
|
286
|
+
* @param {Cron} job
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @typedef {Object} CronOptions - Cron scheduler options
|
|
291
|
+
* @property {string} [name] - Name of a job
|
|
292
|
+
* @property {boolean} [paused] - Job is paused
|
|
293
|
+
* @property {boolean} [kill] - Job is about to be killed or killed
|
|
294
|
+
* @property {boolean | CatchCallbackFn} [catch] - Continue exection even if a unhandled error is thrown by triggered function
|
|
295
|
+
* - If set to a function, execute function on catching the error.
|
|
296
|
+
* @property {boolean} [unref] - Abort job instantly if nothing else keeps the event loop running.
|
|
297
|
+
* @property {number} [maxRuns] - Maximum nuber of executions
|
|
298
|
+
* @property {number} [interval] - Minimum interval between executions, in seconds
|
|
299
|
+
* @property {boolean | ProtectCallbackFn} [protect] - Skip current run if job is already running
|
|
300
|
+
* @property {string | Date} [startAt] - When to start running
|
|
301
|
+
* @property {string | Date} [stopAt] - When to stop running
|
|
302
|
+
* @property {string} [timezone] - Time zone in Europe/Stockholm format
|
|
303
|
+
* @property {number} [utcOffset] - Offset from UTC in minutes
|
|
304
|
+
* @property {boolean} [legacyMode] - Combine day-of-month and day-of-week using true = OR, false = AND. Default is true = OR.
|
|
305
|
+
* @property {?} [context] - Used to pass any object to scheduled function
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Internal function that validates options, and sets defaults
|
|
310
|
+
* @private
|
|
311
|
+
*
|
|
312
|
+
* @param {CronOptions} options
|
|
313
|
+
* @returns {CronOptions}
|
|
314
|
+
*/
|
|
315
|
+
function CronOptions(options) {
|
|
316
|
+
|
|
317
|
+
// If no options are passed, create empty object
|
|
318
|
+
if (options === void 0) {
|
|
319
|
+
options = {};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Don't duplicate the 'name' property
|
|
323
|
+
delete options.name;
|
|
324
|
+
|
|
325
|
+
// Keep options, or set defaults
|
|
326
|
+
options.legacyMode = (options.legacyMode === void 0) ? true : options.legacyMode;
|
|
327
|
+
options.paused = (options.paused === void 0) ? false : options.paused;
|
|
328
|
+
options.maxRuns = (options.maxRuns === void 0) ? Infinity : options.maxRuns;
|
|
329
|
+
options.catch = (options.catch === void 0) ? false : options.catch;
|
|
330
|
+
options.interval = (options.interval === void 0) ? 0 : parseInt(options.interval, 10);
|
|
331
|
+
options.utcOffset = (options.utcOffset === void 0) ? void 0 : parseInt(options.utcOffset, 10);
|
|
332
|
+
options.unref = (options.unref === void 0) ? false : options.unref;
|
|
333
|
+
|
|
334
|
+
// startAt is set, validate it
|
|
335
|
+
if( options.startAt ) {
|
|
336
|
+
options.startAt = new CronDate(options.startAt, options.timezone);
|
|
337
|
+
}
|
|
338
|
+
if( options.stopAt ) {
|
|
339
|
+
options.stopAt = new CronDate(options.stopAt, options.timezone);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Validate interval
|
|
343
|
+
if (options.interval !== null) {
|
|
344
|
+
if (isNaN(options.interval)) {
|
|
345
|
+
throw new Error("CronOptions: Supplied value for interval is not a number");
|
|
346
|
+
} else if (options.interval < 0) {
|
|
347
|
+
throw new Error("CronOptions: Supplied value for interval can not be negative");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Validate utcOffset
|
|
352
|
+
if (options.utcOffset !== void 0) {
|
|
353
|
+
|
|
354
|
+
// Limit range for utcOffset
|
|
355
|
+
if (isNaN(options.utcOffset)) {
|
|
356
|
+
throw new Error("CronOptions: Invalid value passed for utcOffset, should be number representing minutes offset from UTC.");
|
|
357
|
+
} else if (options.utcOffset < -870 || options.utcOffset > 870 ) {
|
|
358
|
+
throw new Error("CronOptions: utcOffset out of bounds.");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Do not allow both timezone and utcOffset
|
|
362
|
+
if (options.utcOffset !== void 0 && options.timezone) {
|
|
363
|
+
throw new Error("CronOptions: Combining 'utcOffset' with 'timezone' is not allowed.");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Unref should be true, false or undefined
|
|
369
|
+
if (options.unref !== true && options.unref !== false) {
|
|
370
|
+
throw new Error("CronOptions: Unref should be either true, false or undefined(false).");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return options;
|
|
374
|
+
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Constant defining the minimum number of days per month where index 0 = January etc.
|
|
379
|
+
*
|
|
380
|
+
* Used to look if a date _could be_ out of bounds. The "could be" part is why february is pinned to 28 days.
|
|
381
|
+
*
|
|
382
|
+
* @private
|
|
383
|
+
*
|
|
384
|
+
* @constant
|
|
385
|
+
* @type {Number[]}
|
|
386
|
+
*
|
|
387
|
+
*/
|
|
388
|
+
const DaysOfMonth = [31,28,31,30,31,30,31,31,30,31,30,31];
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Array of work to be done, consisting of subarrays described below:
|
|
392
|
+
* @private
|
|
393
|
+
*
|
|
394
|
+
* @constant
|
|
395
|
+
*
|
|
396
|
+
* [
|
|
397
|
+
* First item is which member to process,
|
|
398
|
+
* Second item is which member to increment if we didn't find a mathch in current item,
|
|
399
|
+
* Third item is an offset. if months is handled 0-11 in js date object, and we get 1-12 from `this.minute`
|
|
400
|
+
* from pattern. Offset should be -1
|
|
401
|
+
* ]
|
|
402
|
+
*
|
|
403
|
+
*/
|
|
404
|
+
const RecursionSteps = [
|
|
405
|
+
["month", "year", 0],
|
|
406
|
+
["day", "month", -1],
|
|
407
|
+
["hour", "day", 0],
|
|
408
|
+
["minute", "hour", 0],
|
|
409
|
+
["second", "minute", 0],
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Converts date to CronDate
|
|
414
|
+
* @constructor
|
|
415
|
+
*
|
|
416
|
+
* @param {CronDate|Date|string} [d] - Input date, if using string representation ISO 8001 (2015-11-24T19:40:00) local timezone is expected
|
|
417
|
+
* @param {string|number} [tz] - String representation of target timezone in Europe/Stockholm format, or a number representing offset in minutes.
|
|
418
|
+
*/
|
|
419
|
+
function CronDate (d, tz) {
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* TimeZone
|
|
423
|
+
* @type {string|number|undefined}
|
|
424
|
+
*/
|
|
425
|
+
this.tz = tz;
|
|
426
|
+
|
|
427
|
+
// Populate object using input date, or throw
|
|
428
|
+
if (d && d instanceof Date) {
|
|
429
|
+
if (!isNaN(d)) {
|
|
430
|
+
this.fromDate(d);
|
|
431
|
+
} else {
|
|
432
|
+
throw new TypeError("CronDate: Invalid date passed to CronDate constructor");
|
|
433
|
+
}
|
|
434
|
+
} else if (d === void 0) {
|
|
435
|
+
this.fromDate(new Date());
|
|
436
|
+
} else if (d && typeof d === "string") {
|
|
437
|
+
this.fromString(d);
|
|
438
|
+
} else if (d instanceof CronDate) {
|
|
439
|
+
this.fromCronDate(d);
|
|
440
|
+
} else {
|
|
441
|
+
throw new TypeError("CronDate: Invalid type (" + typeof d + ") passed to CronDate constructor");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Sets internals using a Date
|
|
448
|
+
* @private
|
|
449
|
+
*
|
|
450
|
+
* @param {Date} inDate - Input date in local time
|
|
451
|
+
*/
|
|
452
|
+
CronDate.prototype.fromDate = function (inDate) {
|
|
453
|
+
|
|
454
|
+
/* If this instance of CronDate has a target timezone set,
|
|
455
|
+
* use minitz to convert input date object to target timezone
|
|
456
|
+
* before extracting hours, minutes, seconds etc.
|
|
457
|
+
*
|
|
458
|
+
* If not, extract all parts from inDate as-is.
|
|
459
|
+
*/
|
|
460
|
+
if (this.tz !== void 0) {
|
|
461
|
+
if (typeof this.tz === "number") {
|
|
462
|
+
this.ms = inDate.getUTCMilliseconds();
|
|
463
|
+
this.second = inDate.getUTCSeconds();
|
|
464
|
+
this.minute = inDate.getUTCMinutes()+this.tz;
|
|
465
|
+
this.hour = inDate.getUTCHours();
|
|
466
|
+
this.day = inDate.getUTCDate();
|
|
467
|
+
this.month = inDate.getUTCMonth();
|
|
468
|
+
this.year = inDate.getUTCFullYear();
|
|
469
|
+
// Minute could be out of bounds, apply
|
|
470
|
+
this.apply();
|
|
471
|
+
} else {
|
|
472
|
+
const d = minitz.toTZ(inDate, this.tz);
|
|
473
|
+
this.ms = inDate.getMilliseconds();
|
|
474
|
+
this.second = d.s;
|
|
475
|
+
this.minute = d.i;
|
|
476
|
+
this.hour = d.h;
|
|
477
|
+
this.day = d.d;
|
|
478
|
+
this.month = d.m - 1;
|
|
479
|
+
this.year = d.y;
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
this.ms = inDate.getMilliseconds();
|
|
483
|
+
this.second = inDate.getSeconds();
|
|
484
|
+
this.minute = inDate.getMinutes();
|
|
485
|
+
this.hour = inDate.getHours();
|
|
486
|
+
this.day = inDate.getDate();
|
|
487
|
+
this.month = inDate.getMonth();
|
|
488
|
+
this.year = inDate.getFullYear();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Sets internals by deep copying another CronDate
|
|
495
|
+
* @private
|
|
496
|
+
*
|
|
497
|
+
* @param {CronDate} d - Input date
|
|
498
|
+
*/
|
|
499
|
+
CronDate.prototype.fromCronDate = function (d) {
|
|
500
|
+
this.tz = d.tz;
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Current full year, in local time or target timezone specified by `this.tz`
|
|
504
|
+
* @type {number}
|
|
505
|
+
*/
|
|
506
|
+
this.year = d.year;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Current month (1-12), in local time or target timezone specified by `this.tz`
|
|
510
|
+
* @type {number}
|
|
511
|
+
*/
|
|
512
|
+
this.month = d.month;
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Current day (1-31), in local time or target timezone specified by `this.tz`
|
|
516
|
+
* @type {number}
|
|
517
|
+
*/
|
|
518
|
+
this.day = d.day;
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Current hour (0-23), in local time or target timezone specified by `this.tz`
|
|
522
|
+
* @type {number}
|
|
523
|
+
*/
|
|
524
|
+
this.hour = d.hour;
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Current minute (0-59), in local time or target timezone specified by `this.tz`
|
|
528
|
+
* @type {number}
|
|
529
|
+
*/
|
|
530
|
+
this.minute = d.minute;
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Current second (0-59), in local time or target timezone specified by `this.tz`
|
|
534
|
+
* @type {number}
|
|
535
|
+
*/
|
|
536
|
+
this.second = d.second;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Current milliseconds
|
|
540
|
+
* @type {number}
|
|
541
|
+
*/
|
|
542
|
+
this.ms = d.ms;
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Reset internal parameters (seconds, minutes, hours) if any of them have exceeded (or could have exceeded) their normal ranges
|
|
547
|
+
*
|
|
548
|
+
* Will alway return true on february 29th, as that is a date that _could_ be out of bounds
|
|
549
|
+
*
|
|
550
|
+
* @private
|
|
551
|
+
*/
|
|
552
|
+
CronDate.prototype.apply = function () {
|
|
553
|
+
// If any value could be out of bounds, apply
|
|
554
|
+
if (this.month>11||this.day>DaysOfMonth[this.month]||this.hour>59||this.minute>59||this.second>59||this.hour<0||this.minute<0||this.second<0) {
|
|
555
|
+
const d = new Date(Date.UTC(this.year, this.month, this.day, this.hour, this.minute, this.second, this.ms));
|
|
556
|
+
this.ms = d.getUTCMilliseconds();
|
|
557
|
+
this.second = d.getUTCSeconds();
|
|
558
|
+
this.minute = d.getUTCMinutes();
|
|
559
|
+
this.hour = d.getUTCHours();
|
|
560
|
+
this.day = d.getUTCDate();
|
|
561
|
+
this.month = d.getUTCMonth();
|
|
562
|
+
this.year = d.getUTCFullYear();
|
|
563
|
+
return true;
|
|
564
|
+
} else {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Sets internals by parsing a string
|
|
571
|
+
* @private
|
|
572
|
+
*
|
|
573
|
+
* @param {Date} date - Input date
|
|
574
|
+
*/
|
|
575
|
+
CronDate.prototype.fromString = function (str) {
|
|
576
|
+
return this.fromDate(minitz.fromTZISO(str, this.tz));
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Find next match of current part
|
|
581
|
+
* @private
|
|
582
|
+
*
|
|
583
|
+
* @param {CronOptions} options - Cron options used for incrementing
|
|
584
|
+
* @param {string} target
|
|
585
|
+
* @param {CronPattern} pattern
|
|
586
|
+
* @param {Number} offset
|
|
587
|
+
*
|
|
588
|
+
* @returns {boolean}
|
|
589
|
+
*
|
|
590
|
+
*/
|
|
591
|
+
CronDate.prototype.findNext = function (options, target, pattern, offset) {
|
|
592
|
+
const originalTarget = this[target];
|
|
593
|
+
|
|
594
|
+
// In the conditions below, local time is not relevant. And as new Date(Date.UTC(y,m,d)) is way faster
|
|
595
|
+
// than new Date(y,m,d). We use the UTC functions to set/get date parts.
|
|
596
|
+
|
|
597
|
+
// Pre-calculate last day of month if needed
|
|
598
|
+
let lastDayOfMonth;
|
|
599
|
+
if (pattern.lastDayOfMonth || pattern.lastWeekdayOfMonth) {
|
|
600
|
+
// This is an optimization for every month except february, which has different number of days different years
|
|
601
|
+
if (this.month !== 1) {
|
|
602
|
+
lastDayOfMonth = DaysOfMonth[this.month]; // About 20% performance increase when using L
|
|
603
|
+
} else {
|
|
604
|
+
lastDayOfMonth = new Date(Date.UTC(this.year, this.month+1, 0,0,0,0,0)).getUTCDate();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Pre-calculate weekday if needed
|
|
609
|
+
// Calculate offset weekday by ((fDomWeekDay + (targetDate - 1)) % 7)
|
|
610
|
+
const fDomWeekDay = (!pattern.starDOW && target == "day") ? new Date(Date.UTC(this.year, this.month, 1,0,0,0,0)).getUTCDay() : undefined;
|
|
611
|
+
|
|
612
|
+
for( let i = this[target] + offset; i < pattern[target].length; i++ ) {
|
|
613
|
+
|
|
614
|
+
// this applies to all "levels"
|
|
615
|
+
let match = pattern[target][i];
|
|
616
|
+
|
|
617
|
+
// Special case for last day of month
|
|
618
|
+
if (target === "day" && pattern.lastDayOfMonth && i-offset == lastDayOfMonth) {
|
|
619
|
+
match = true;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Special case for day of week
|
|
623
|
+
if (target === "day" && !pattern.starDOW) {
|
|
624
|
+
|
|
625
|
+
let dowMatch = pattern.dayOfWeek[(fDomWeekDay + ((i-offset) - 1)) % 7];
|
|
626
|
+
|
|
627
|
+
// Extra check for l-flag
|
|
628
|
+
if (dowMatch && pattern.lastWeekdayOfMonth) {
|
|
629
|
+
dowMatch = dowMatch && ( i-offset > lastDayOfMonth - 7 );
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// If we use legacyMode, and dayOfMonth is specified - use "OR" to combine day of week with day of month
|
|
633
|
+
// In all other cases use "AND"
|
|
634
|
+
if (options.legacyMode && !pattern.starDOM) {
|
|
635
|
+
match = match || dowMatch;
|
|
636
|
+
} else {
|
|
637
|
+
match = match && dowMatch;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (match) {
|
|
642
|
+
this[target] = i-offset;
|
|
643
|
+
|
|
644
|
+
// Return 2 if changed, 1 if unchanged
|
|
645
|
+
return (originalTarget !== this[target]) ? 2 : 1;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Return 3 if part was not matched
|
|
650
|
+
return 3;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Increment to next run time recursively
|
|
655
|
+
*
|
|
656
|
+
* This function is currently capped at year 3000. Do you have a reason to go further? Open an issue on GitHub!
|
|
657
|
+
|
|
658
|
+
* @private
|
|
659
|
+
*
|
|
660
|
+
* @param {string} pattern - The pattern used to increment current state
|
|
661
|
+
* @param {CronOptions} options - Cron options used for incrementing
|
|
662
|
+
* @param {integer} doing - Which part to increment, 0 represent first item of RecursionSteps-array etc.
|
|
663
|
+
* @return {CronDate|null} - Returns itthis for chaining, or null if increment wasnt possible
|
|
664
|
+
*/
|
|
665
|
+
CronDate.prototype.recurse = function (pattern, options, doing) {
|
|
666
|
+
|
|
667
|
+
// Find next month (or whichever part we're at)
|
|
668
|
+
const res = this.findNext(options, RecursionSteps[doing][0], pattern, RecursionSteps[doing][2]);
|
|
669
|
+
|
|
670
|
+
// Month (or whichever part we're at) changed
|
|
671
|
+
if (res > 1) {
|
|
672
|
+
// Flag following levels for reset
|
|
673
|
+
let resetLevel = doing + 1;
|
|
674
|
+
while(resetLevel < RecursionSteps.length) {
|
|
675
|
+
this[RecursionSteps[resetLevel][0]] = -RecursionSteps[resetLevel][2];
|
|
676
|
+
resetLevel++;
|
|
677
|
+
}
|
|
678
|
+
// Parent changed
|
|
679
|
+
if (res=== 3) {
|
|
680
|
+
// Do increment parent, and reset current level
|
|
681
|
+
this[RecursionSteps[doing][1]]++;
|
|
682
|
+
this[RecursionSteps[doing][0]] = -RecursionSteps[doing][2];
|
|
683
|
+
this.apply();
|
|
684
|
+
|
|
685
|
+
// Restart
|
|
686
|
+
return this.recurse(pattern, options, 0);
|
|
687
|
+
} else if (this.apply()) {
|
|
688
|
+
return this.recurse(pattern, options, doing-1);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Move to next level
|
|
694
|
+
doing += 1;
|
|
695
|
+
|
|
696
|
+
// Done?
|
|
697
|
+
if (doing >= RecursionSteps.length) {
|
|
698
|
+
return this;
|
|
699
|
+
|
|
700
|
+
// ... or out of bounds ?
|
|
701
|
+
} else if (this.year >= 3000) {
|
|
702
|
+
return null;
|
|
703
|
+
|
|
704
|
+
// ... oh, go to next part then
|
|
705
|
+
} else {
|
|
706
|
+
|
|
707
|
+
return this.recurse(pattern, options, doing);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Increment to next run time
|
|
714
|
+
* @public
|
|
715
|
+
*
|
|
716
|
+
* @param {string} pattern - The pattern used to increment current state
|
|
717
|
+
* @param {CronOptions} options - Cron options used for incrementing
|
|
718
|
+
* @param {boolean} [hasPreviousRun] - If this run should adhere to minimum interval
|
|
719
|
+
* @return {CronDate|null} - Returns itthis for chaining, or null if increment wasnt possible
|
|
720
|
+
*/
|
|
721
|
+
CronDate.prototype.increment = function (pattern, options, hasPreviousRun) {
|
|
722
|
+
|
|
723
|
+
// Move to next second, or increment according to minimum interval indicated by option `interval: x`
|
|
724
|
+
// Do not increment a full interval if this is the very first run
|
|
725
|
+
this.second += (options.interval > 1 && hasPreviousRun) ? options.interval : 1;
|
|
726
|
+
|
|
727
|
+
// Always reset milliseconds, so we are at the next second exactly
|
|
728
|
+
this.ms = 0;
|
|
729
|
+
|
|
730
|
+
// Make sure seconds has not gotten out of bounds
|
|
731
|
+
this.apply();
|
|
732
|
+
|
|
733
|
+
// Recursively change each part (y, m, d ...) until next match is found, return null on failure
|
|
734
|
+
return this.recurse(pattern, options, 0);
|
|
735
|
+
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Convert current state back to a javascript Date()
|
|
740
|
+
* @public
|
|
741
|
+
*
|
|
742
|
+
* @param {boolean} internal - If this is an internal call
|
|
743
|
+
* @returns {Date}
|
|
744
|
+
*/
|
|
745
|
+
CronDate.prototype.getDate = function (internal) {
|
|
746
|
+
// If this is an internal call, return the date as is
|
|
747
|
+
// Also use this option when no timezone or utcOffset is set
|
|
748
|
+
if (internal || this.tz === void 0) {
|
|
749
|
+
return new Date(this.year, this.month, this.day, this.hour, this.minute, this.second, this.ms);
|
|
750
|
+
} else {
|
|
751
|
+
// If .tz is a number, it indicates offset in minutes. UTC timestamp of the internal date objects will be off by the same number of minutes.
|
|
752
|
+
// Restore this, and return a date object with correct time set.
|
|
753
|
+
if (typeof this.tz === "number") {
|
|
754
|
+
return new Date(Date.UTC(this.year, this.month, this.day, this.hour, this.minute-this.tz, this.second, this.ms));
|
|
755
|
+
|
|
756
|
+
// If .tz is something else (hopefully a string), it indicates the timezone of the "local time" of the internal date object
|
|
757
|
+
// Use minitz to create a normal Date object, and return that.
|
|
758
|
+
} else {
|
|
759
|
+
return minitz(this.year, this.month+1, this.day, this.hour, this.minute, this.second, this.tz);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Convert current state back to a javascript Date() and return UTC milliseconds
|
|
766
|
+
* @public
|
|
767
|
+
*
|
|
768
|
+
* @returns {Date}
|
|
769
|
+
*/
|
|
770
|
+
CronDate.prototype.getTime = function () {
|
|
771
|
+
return this.getDate().getTime();
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Name for each part of the cron pattern
|
|
776
|
+
* @typedef {("second" | "minute" | "hour" | "day" | "month" | "dayOfWeek")} CronPatternPart
|
|
777
|
+
*/
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Offset, 0 or -1.
|
|
781
|
+
*
|
|
782
|
+
* 0 offset is used for seconds,minutes and hours as they start on 1.
|
|
783
|
+
* -1 on days and months, as they start on 0
|
|
784
|
+
*
|
|
785
|
+
* @typedef {Number} CronIndexOffset
|
|
786
|
+
*/
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Create a CronPattern instance from pattern string ('* * * * * *')
|
|
790
|
+
* @constructor
|
|
791
|
+
* @param {string} pattern - Input pattern
|
|
792
|
+
* @param {string} timezone - Input timezone, used for '?'-substitution
|
|
793
|
+
*/
|
|
794
|
+
function CronPattern (pattern, timezone) {
|
|
795
|
+
|
|
796
|
+
this.pattern = pattern;
|
|
797
|
+
this.timezone = timezone;
|
|
798
|
+
|
|
799
|
+
this.second = Array(60).fill(0); // 0-59
|
|
800
|
+
this.minute = Array(60).fill(0); // 0-59
|
|
801
|
+
this.hour = Array(24).fill(0); // 0-23
|
|
802
|
+
this.day = Array(31).fill(0); // 0-30 in array, 1-31 in config
|
|
803
|
+
this.month = Array(12).fill(0); // 0-11 in array, 1-12 in config
|
|
804
|
+
this.dayOfWeek = Array(8).fill(0); // 0-7 Where 0 = Sunday and 7=Sunday;
|
|
805
|
+
|
|
806
|
+
this.lastDayOfMonth = false;
|
|
807
|
+
this.lastWeekdayOfMonth = false;
|
|
808
|
+
|
|
809
|
+
this.starDOM = false; // Asterisk used for dayOfMonth
|
|
810
|
+
this.starDOW = false; // Asterisk used for dayOfWeek
|
|
811
|
+
|
|
812
|
+
this.parse();
|
|
813
|
+
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Parse current pattern, will throw on any type of failure
|
|
818
|
+
* @private
|
|
819
|
+
*/
|
|
820
|
+
CronPattern.prototype.parse = function () {
|
|
821
|
+
|
|
822
|
+
// Sanity check
|
|
823
|
+
if( !(typeof this.pattern === "string" || this.pattern.constructor === String) ) {
|
|
824
|
+
throw new TypeError("CronPattern: Pattern has to be of type string.");
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Handle @yearly, @monthly etc
|
|
828
|
+
if (this.pattern.indexOf("@") >= 0) this.pattern = this.handleNicknames(this.pattern).trim();
|
|
829
|
+
|
|
830
|
+
// Split configuration on whitespace
|
|
831
|
+
const parts = this.pattern.replace(/\s+/g, " ").split(" ");
|
|
832
|
+
|
|
833
|
+
// Validite number of configuration entries
|
|
834
|
+
if( parts.length < 5 || parts.length > 6 ) {
|
|
835
|
+
throw new TypeError("CronPattern: invalid configuration format ('" + this.pattern + "'), exacly five or six space separated parts required.");
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// If seconds is omitted, insert 0 for seconds
|
|
839
|
+
if( parts.length === 5) {
|
|
840
|
+
parts.unshift("0");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Convert 'L' to lastDayOfMonth flag in day-of-month field
|
|
844
|
+
if(parts[3].indexOf("L") >= 0) {
|
|
845
|
+
parts[3] = parts[3].replace("L","");
|
|
846
|
+
this.lastDayOfMonth = true;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Convert 'L' to lastWeekdayOfMonth flag in day-of-week field
|
|
850
|
+
if(parts[5].indexOf("L") >= 0) {
|
|
851
|
+
parts[5] = parts[5].replace("L","");
|
|
852
|
+
this.lastWeekdayOfMonth = true;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Check for starDOM
|
|
856
|
+
if(parts[3] == "*") {
|
|
857
|
+
this.starDOM = true;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Replace alpha representations
|
|
861
|
+
if (parts[4].length >= 3) parts[4] = this.replaceAlphaMonths(parts[4]);
|
|
862
|
+
if (parts[5].length >= 3) parts[5] = this.replaceAlphaDays(parts[5]);
|
|
863
|
+
|
|
864
|
+
// Check for starDOW
|
|
865
|
+
if(parts[5] == "*") {
|
|
866
|
+
this.starDOW = true;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Implement '?' in the simplest possible way - replace ? with current value, before further processing
|
|
870
|
+
if (this.pattern.indexOf("?") >= 0) {
|
|
871
|
+
const initDate = new CronDate(new Date(),this.timezone).getDate(true);
|
|
872
|
+
parts[0] = parts[0].replace("?", initDate.getSeconds());
|
|
873
|
+
parts[1] = parts[1].replace("?", initDate.getMinutes());
|
|
874
|
+
parts[2] = parts[2].replace("?", initDate.getHours());
|
|
875
|
+
if (!this.starDOM) parts[3] = parts[3].replace("?", initDate.getDate());
|
|
876
|
+
parts[4] = parts[4].replace("?", initDate.getMonth()+1); // getMonth is zero indexed while pattern starts from 1
|
|
877
|
+
if (!this.starDOW) parts[5] = parts[5].replace("?", initDate.getDay());
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Check part content
|
|
881
|
+
this.throwAtIllegalCharacters(parts);
|
|
882
|
+
|
|
883
|
+
// Parse parts into arrays, validates as we go
|
|
884
|
+
this.partToArray("second", parts[0], 0);
|
|
885
|
+
this.partToArray("minute", parts[1], 0);
|
|
886
|
+
this.partToArray("hour", parts[2], 0);
|
|
887
|
+
this.partToArray("day", parts[3], -1);
|
|
888
|
+
this.partToArray("month", parts[4], -1);
|
|
889
|
+
this.partToArray("dayOfWeek", parts[5], 0);
|
|
890
|
+
|
|
891
|
+
// 0 = Sunday, 7 = Sunday
|
|
892
|
+
if( this.dayOfWeek[7] ) {
|
|
893
|
+
this.dayOfWeek[0] = 1;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Convert current part (seconds/minutes etc) to an array of 1 or 0 depending on if the part is about to trigger a run or not.
|
|
900
|
+
* @private
|
|
901
|
+
*
|
|
902
|
+
* @param {CronPatternPart} type - Seconds/minutes etc
|
|
903
|
+
* @param {string} conf - Current pattern part - *, 0-1 etc
|
|
904
|
+
* @param {CronIndexOffset} valueIndexOffset
|
|
905
|
+
* @param {boolean} [recursed] - Is this a recursed call
|
|
906
|
+
*/
|
|
907
|
+
CronPattern.prototype.partToArray = function (type, conf, valueIndexOffset) {
|
|
908
|
+
|
|
909
|
+
const arr = this[type];
|
|
910
|
+
|
|
911
|
+
// First off, handle wildcard
|
|
912
|
+
if( conf === "*" ) return arr.fill(1);
|
|
913
|
+
|
|
914
|
+
// Handle separated entries (,) by recursion
|
|
915
|
+
const split = conf.split(",");
|
|
916
|
+
if( split.length > 1 ) {
|
|
917
|
+
for( let i = 0; i < split.length; i++ ) {
|
|
918
|
+
this.partToArray(type, split[i], valueIndexOffset);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Handle range with stepping (x-y/z)
|
|
922
|
+
} else if( conf.indexOf("-") !== -1 && conf.indexOf("/") !== -1 ) {
|
|
923
|
+
this.handleRangeWithStepping(conf, type, valueIndexOffset);
|
|
924
|
+
|
|
925
|
+
// Handle range
|
|
926
|
+
} else if( conf.indexOf("-") !== -1 ) {
|
|
927
|
+
this.handleRange(conf, type, valueIndexOffset);
|
|
928
|
+
|
|
929
|
+
// Handle stepping
|
|
930
|
+
} else if( conf.indexOf("/") !== -1 ) {
|
|
931
|
+
this.handleStepping(conf, type, valueIndexOffset);
|
|
932
|
+
|
|
933
|
+
// Anything left should be a number
|
|
934
|
+
} else if( conf !== "" ) {
|
|
935
|
+
this.handleNumber(conf, type, valueIndexOffset);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* After converting JAN-DEC, SUN-SAT only 0-9 * , / - are allowed, throw if anything else pops up
|
|
942
|
+
* @private
|
|
943
|
+
*
|
|
944
|
+
* @param {string[]} parts - Each part split as strings
|
|
945
|
+
*/
|
|
946
|
+
CronPattern.prototype.throwAtIllegalCharacters = function (parts) {
|
|
947
|
+
const reValidCron = /[^/*0-9,-]+/;
|
|
948
|
+
for(let i = 0; i < parts.length; i++) {
|
|
949
|
+
if( reValidCron.test(parts[i]) ) {
|
|
950
|
+
throw new TypeError("CronPattern: configuration entry " + i + " (" + parts[i] + ") contains illegal characters.");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Nothing but a number left, handle that
|
|
957
|
+
* @private
|
|
958
|
+
*
|
|
959
|
+
* @param {string} conf - Current part, expected to be a number, as a string
|
|
960
|
+
* @param {string} type - One of "seconds", "minutes" etc
|
|
961
|
+
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
962
|
+
*/
|
|
963
|
+
CronPattern.prototype.handleNumber = function (conf, type, valueIndexOffset) {
|
|
964
|
+
const i = (parseInt(conf, 10) + valueIndexOffset);
|
|
965
|
+
|
|
966
|
+
if( isNaN(i) ) {
|
|
967
|
+
throw new TypeError("CronPattern: " + type + " is not a number: '" + conf + "'");
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if( i < 0 || i >= this[type].length ) {
|
|
971
|
+
throw new TypeError("CronPattern: " + type + " value out of range: '" + conf + "'");
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
this[type][i] = 1;
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Take care of ranges with stepping (e.g. 3-23/5)
|
|
979
|
+
* @private
|
|
980
|
+
*
|
|
981
|
+
* @param {string} conf - Current part, expected to be a string like 3-23/5
|
|
982
|
+
* @param {string} type - One of "seconds", "minutes" etc
|
|
983
|
+
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
984
|
+
*/
|
|
985
|
+
CronPattern.prototype.handleRangeWithStepping = function (conf, type, valueIndexOffset) {
|
|
986
|
+
const matches = conf.match(/^(\d+)-(\d+)\/(\d+)$/);
|
|
987
|
+
|
|
988
|
+
if( matches === null ) throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '" + conf + "'");
|
|
989
|
+
|
|
990
|
+
let [, lower, upper, steps] = matches;
|
|
991
|
+
lower = parseInt(lower, 10) + valueIndexOffset;
|
|
992
|
+
upper = parseInt(upper, 10) + valueIndexOffset;
|
|
993
|
+
steps = parseInt(steps, 10);
|
|
994
|
+
|
|
995
|
+
if( isNaN(lower) ) throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");
|
|
996
|
+
if( isNaN(upper) ) throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
|
|
997
|
+
if( isNaN(steps) ) throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");
|
|
998
|
+
|
|
999
|
+
if( steps === 0 ) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
|
|
1000
|
+
if( steps > this[type].length ) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part ("+this[type].length+")");
|
|
1001
|
+
|
|
1002
|
+
if( lower < 0 || upper >= this[type].length ) throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
|
|
1003
|
+
if( lower > upper ) throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
|
|
1004
|
+
|
|
1005
|
+
for (let i = lower; i <= upper; i += steps) {
|
|
1006
|
+
this[type][i] = 1;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Take care of ranges (e.g. 1-20)
|
|
1012
|
+
* @private
|
|
1013
|
+
*
|
|
1014
|
+
* @param {string} conf - Current part, expected to be a string like 1-20
|
|
1015
|
+
* @param {string} type - One of "seconds", "minutes" etc
|
|
1016
|
+
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
1017
|
+
*/
|
|
1018
|
+
CronPattern.prototype.handleRange = function (conf, type, valueIndexOffset) {
|
|
1019
|
+
const split = conf.split("-");
|
|
1020
|
+
|
|
1021
|
+
if( split.length !== 2 ) {
|
|
1022
|
+
throw new TypeError("CronPattern: Syntax error, illegal range: '" + conf + "'");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const lower = parseInt(split[0], 10) + valueIndexOffset,
|
|
1026
|
+
upper = parseInt(split[1], 10) + valueIndexOffset;
|
|
1027
|
+
|
|
1028
|
+
if( isNaN(lower) ) {
|
|
1029
|
+
throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");
|
|
1030
|
+
} else if( isNaN(upper) ) {
|
|
1031
|
+
throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Check that value is within range
|
|
1035
|
+
if( lower < 0 || upper >= this[type].length ) {
|
|
1036
|
+
throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
//
|
|
1040
|
+
if( lower > upper ) {
|
|
1041
|
+
throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
for( let i = lower; i <= upper; i++ ) {
|
|
1045
|
+
this[type][i] = 1;
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Handle stepping (e.g. * / 14)
|
|
1051
|
+
* @private
|
|
1052
|
+
*
|
|
1053
|
+
* @param {string} conf - Current part, expected to be a string like * /20 (without the space)
|
|
1054
|
+
* @param {string} type - One of "seconds", "minutes" etc
|
|
1055
|
+
*/
|
|
1056
|
+
CronPattern.prototype.handleStepping = function (conf, type) {
|
|
1057
|
+
|
|
1058
|
+
const split = conf.split("/");
|
|
1059
|
+
|
|
1060
|
+
if( split.length !== 2 ) {
|
|
1061
|
+
throw new TypeError("CronPattern: Syntax error, illegal stepping: '" + conf + "'");
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
let start = 0;
|
|
1065
|
+
if( split[0] !== "*" ) {
|
|
1066
|
+
start = parseInt(split[0], 10);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const steps = parseInt(split[1], 10);
|
|
1070
|
+
|
|
1071
|
+
if( isNaN(steps) ) throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");
|
|
1072
|
+
if( steps === 0 ) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
|
|
1073
|
+
if( steps > this[type].length ) throw new TypeError("CronPattern: Syntax error, max steps for part is ("+this[type].length+")");
|
|
1074
|
+
|
|
1075
|
+
for( let i = start; i < this[type].length; i+= steps ) {
|
|
1076
|
+
this[type][i] = 1;
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Replace day name with day numbers
|
|
1083
|
+
* @private
|
|
1084
|
+
*
|
|
1085
|
+
* @param {string} conf - Current part, expected to be a string that might contain sun,mon etc.
|
|
1086
|
+
*
|
|
1087
|
+
* @returns {string} - conf with 0 instead of sun etc.
|
|
1088
|
+
*/
|
|
1089
|
+
CronPattern.prototype.replaceAlphaDays = function (conf) {
|
|
1090
|
+
return conf
|
|
1091
|
+
.replace(/-sun/gi, "-7") // choose 7 if sunday is the upper value of a range because the upper value must not be smaller than the lower value
|
|
1092
|
+
.replace(/sun/gi, "0")
|
|
1093
|
+
.replace(/mon/gi, "1")
|
|
1094
|
+
.replace(/tue/gi, "2")
|
|
1095
|
+
.replace(/wed/gi, "3")
|
|
1096
|
+
.replace(/thu/gi, "4")
|
|
1097
|
+
.replace(/fri/gi, "5")
|
|
1098
|
+
.replace(/sat/gi, "6");
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Replace month name with month numbers
|
|
1103
|
+
* @private
|
|
1104
|
+
*
|
|
1105
|
+
* @param {string} conf - Current part, expected to be a string that might contain jan,feb etc.
|
|
1106
|
+
*
|
|
1107
|
+
* @returns {string} - conf with 0 instead of sun etc.
|
|
1108
|
+
*/
|
|
1109
|
+
CronPattern.prototype.replaceAlphaMonths = function (conf) {
|
|
1110
|
+
return conf
|
|
1111
|
+
.replace(/jan/gi, "1")
|
|
1112
|
+
.replace(/feb/gi, "2")
|
|
1113
|
+
.replace(/mar/gi, "3")
|
|
1114
|
+
.replace(/apr/gi, "4")
|
|
1115
|
+
.replace(/may/gi, "5")
|
|
1116
|
+
.replace(/jun/gi, "6")
|
|
1117
|
+
.replace(/jul/gi, "7")
|
|
1118
|
+
.replace(/aug/gi, "8")
|
|
1119
|
+
.replace(/sep/gi, "9")
|
|
1120
|
+
.replace(/oct/gi, "10")
|
|
1121
|
+
.replace(/nov/gi, "11")
|
|
1122
|
+
.replace(/dec/gi, "12");
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Replace nicknames with actual cron patterns
|
|
1127
|
+
* @private
|
|
1128
|
+
*
|
|
1129
|
+
* @param {string} pattern - Pattern, may contain nicknames, or not
|
|
1130
|
+
*
|
|
1131
|
+
* @returns {string} - Pattern, with cron expression insted of nicknames
|
|
1132
|
+
*/
|
|
1133
|
+
CronPattern.prototype.handleNicknames = function (pattern) {
|
|
1134
|
+
// Replace textual representations of pattern
|
|
1135
|
+
const cleanPattern = pattern.trim().toLowerCase();
|
|
1136
|
+
if (cleanPattern === "@yearly" || cleanPattern === "@annually") {
|
|
1137
|
+
return "0 0 1 1 *";
|
|
1138
|
+
} else if (cleanPattern === "@monthly") {
|
|
1139
|
+
return "0 0 1 * *";
|
|
1140
|
+
} else if (cleanPattern === "@weekly") {
|
|
1141
|
+
return "0 0 * * 0";
|
|
1142
|
+
} else if (cleanPattern === "@daily") {
|
|
1143
|
+
return "0 0 * * *";
|
|
1144
|
+
} else if (cleanPattern === "@hourly") {
|
|
1145
|
+
return "0 * * * *";
|
|
1146
|
+
} else {
|
|
1147
|
+
return pattern;
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* Helper function to check if a variable is a function
|
|
1153
|
+
* @private
|
|
1154
|
+
*
|
|
1155
|
+
* @param {?} [v] - Variable to check
|
|
1156
|
+
* @returns {boolean}
|
|
1157
|
+
*/
|
|
1158
|
+
function isFunction(v) {
|
|
1159
|
+
return (
|
|
1160
|
+
Object.prototype.toString.call(v) === "[object Function]" ||
|
|
1161
|
+
"function" === typeof v ||
|
|
1162
|
+
v instanceof Function
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Helper function to unref a timer in both Deno and Node
|
|
1168
|
+
* @private
|
|
1169
|
+
* @param {unknown} [timer] - Timer to unref
|
|
1170
|
+
*/
|
|
1171
|
+
function unrefTimer(timer) {
|
|
1172
|
+
/* global Deno */
|
|
1173
|
+
if (typeof Deno !== "undefined" && typeof Deno.unrefTimer !== "undefined") {
|
|
1174
|
+
Deno.unrefTimer(timer);
|
|
1175
|
+
// Node
|
|
1176
|
+
} else if (timer && typeof timer.unref !== "undefined") {
|
|
1177
|
+
timer.unref();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/* ------------------------------------------------------------------------------------
|
|
1182
|
+
|
|
1183
|
+
Croner - MIT License - Hexagon <github.com/Hexagon>
|
|
1184
|
+
|
|
1185
|
+
Pure JavaScript Isomorphic cron parser and scheduler without dependencies.
|
|
1186
|
+
|
|
1187
|
+
------------------------------------------------------------------------------------
|
|
1188
|
+
|
|
1189
|
+
License:
|
|
1190
|
+
|
|
1191
|
+
Copyright (c) 2015-2022 Hexagon <github.com/Hexagon>
|
|
1192
|
+
|
|
1193
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
1194
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
1195
|
+
in the Software without restriction, including without limitation the rights
|
|
1196
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
1197
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
1198
|
+
furnished to do so, subject to the following conditions:
|
|
1199
|
+
The above copyright notice and this permission notice shall be included in
|
|
1200
|
+
all copies or substantial portions of the Software.
|
|
1201
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
1202
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
1203
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
1204
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
1205
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
1206
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
1207
|
+
THE SOFTWARE.
|
|
1208
|
+
|
|
1209
|
+
------------------------------------------------------------------------------------ */
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Many JS engines stores the delay as a 32-bit signed integer internally.
|
|
1213
|
+
* This causes an integer overflow when using delays larger than 2147483647,
|
|
1214
|
+
* resulting in the timeout being executed immediately.
|
|
1215
|
+
*
|
|
1216
|
+
* All JS engines implements an immediate execution of delays larger that a 32-bit
|
|
1217
|
+
* int to keep the behaviour concistent.
|
|
1218
|
+
*
|
|
1219
|
+
* @constant
|
|
1220
|
+
* @type {number}
|
|
1221
|
+
*/
|
|
1222
|
+
const maxDelay = Math.pow(2, 32 - 1) - 1;
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* An array containing all named cron jobs.
|
|
1226
|
+
*
|
|
1227
|
+
* @constant
|
|
1228
|
+
* @type {Cron[]}
|
|
1229
|
+
*/
|
|
1230
|
+
const scheduledJobs = [];
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Cron entrypoint
|
|
1234
|
+
*
|
|
1235
|
+
* @constructor
|
|
1236
|
+
* @param {string|Date} pattern - Input pattern, input date, or input ISO 8601 time string
|
|
1237
|
+
* @param {CronOptions|Function} [fnOrOptions1] - Options or function to be run each iteration of pattern
|
|
1238
|
+
* @param {CronOptions|Function} [fnOrOptions2] - Options or function to be run each iteration of pattern
|
|
1239
|
+
* @returns {Cron}
|
|
1240
|
+
*/
|
|
1241
|
+
function Cron(pattern, fnOrOptions1, fnOrOptions2) {
|
|
1242
|
+
// Optional "new" keyword
|
|
1243
|
+
if (!(this instanceof Cron)) {
|
|
1244
|
+
return new Cron(pattern, fnOrOptions1, fnOrOptions2);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Make options and func optional and interchangable
|
|
1248
|
+
let options, func;
|
|
1249
|
+
|
|
1250
|
+
if (isFunction(fnOrOptions1)) {
|
|
1251
|
+
func = fnOrOptions1;
|
|
1252
|
+
} else if (typeof fnOrOptions1 === "object") {
|
|
1253
|
+
options = fnOrOptions1;
|
|
1254
|
+
} else if (fnOrOptions1 !== void 0) {
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
"Cron: Invalid argument passed for optionsIn. Should be one of function, or object (options).",
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (isFunction(fnOrOptions2)) {
|
|
1261
|
+
func = fnOrOptions2;
|
|
1262
|
+
} else if (typeof fnOrOptions2 === "object") {
|
|
1263
|
+
options = fnOrOptions2;
|
|
1264
|
+
} else if (fnOrOptions2 !== void 0) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
"Cron: Invalid argument passed for funcIn. Should be one of function, or object (options).",
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* @public
|
|
1272
|
+
* @type {string|undefined} */
|
|
1273
|
+
this.name = options ? options.name : void 0;
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* @public
|
|
1277
|
+
* @type {CronOptions} */
|
|
1278
|
+
this.options = CronOptions(options);
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Encapsulate all internal states in an object.
|
|
1282
|
+
* Duplicate all options that can change to internal states, for example maxRuns and paused.
|
|
1283
|
+
* @private
|
|
1284
|
+
*/
|
|
1285
|
+
this._states = {
|
|
1286
|
+
/** @type {boolean} */
|
|
1287
|
+
kill: false,
|
|
1288
|
+
|
|
1289
|
+
/** @type {boolean} */
|
|
1290
|
+
blocking: false,
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Start time of previous trigger, updated after each trigger
|
|
1294
|
+
* @type {CronDate}
|
|
1295
|
+
*/
|
|
1296
|
+
previousRun: void 0,
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Start time of current trigger, this is updated just before triggering
|
|
1300
|
+
* @type {CronDate}
|
|
1301
|
+
*/
|
|
1302
|
+
currentRun: void 0,
|
|
1303
|
+
|
|
1304
|
+
/** @type {CronDate|undefined} */
|
|
1305
|
+
once: void 0,
|
|
1306
|
+
|
|
1307
|
+
/** @type {unknown|undefined} */
|
|
1308
|
+
currentTimeout: void 0,
|
|
1309
|
+
|
|
1310
|
+
/** @type {number} */
|
|
1311
|
+
maxRuns: options ? options.maxRuns : void 0,
|
|
1312
|
+
|
|
1313
|
+
/** @type {boolean} */
|
|
1314
|
+
paused: options ? options.paused : false,
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* @public
|
|
1319
|
+
* @type {CronPattern|undefined} */
|
|
1320
|
+
this.pattern = void 0;
|
|
1321
|
+
|
|
1322
|
+
// Check if we got a date, or a pattern supplied as first argument
|
|
1323
|
+
// Then set either this._states.once or this.pattern
|
|
1324
|
+
if (
|
|
1325
|
+
pattern &&
|
|
1326
|
+
(pattern instanceof Date || ((typeof pattern === "string") && pattern.indexOf(":") > 0))
|
|
1327
|
+
) {
|
|
1328
|
+
this._states.once = new CronDate(pattern, this.options.timezone || this.options.utcOffset);
|
|
1329
|
+
} else {
|
|
1330
|
+
this.pattern = new CronPattern(pattern, this.options.timezone);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Allow shorthand scheduling
|
|
1334
|
+
if (func !== void 0) {
|
|
1335
|
+
this.fn = func;
|
|
1336
|
+
this.schedule();
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Only store the job in scheduledJobs if a name is specified in the options.
|
|
1340
|
+
if (this.name) {
|
|
1341
|
+
const existing = scheduledJobs.find((j) => j.name === this.name);
|
|
1342
|
+
if (existing) {
|
|
1343
|
+
throw new Error(
|
|
1344
|
+
"Cron: Tried to initialize new named job '" + this.name + "', but name already taken.",
|
|
1345
|
+
);
|
|
1346
|
+
} else {
|
|
1347
|
+
scheduledJobs.push(this);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
return this;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Find next runtime, based on supplied date. Strips milliseconds.
|
|
1356
|
+
*
|
|
1357
|
+
* @param {CronDate|Date|string} [prev] - Date to start from
|
|
1358
|
+
* @returns {Date | null} - Next run time
|
|
1359
|
+
*/
|
|
1360
|
+
Cron.prototype.next = function (prev) {
|
|
1361
|
+
const next = this._next(prev);
|
|
1362
|
+
return next ? next.getDate() : null;
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Find next n runs, based on supplied date. Strips milliseconds.
|
|
1367
|
+
*
|
|
1368
|
+
* @param {number} n - Number of runs to enumerate
|
|
1369
|
+
* @param {Date|string} [previous] - Date to start from
|
|
1370
|
+
* @returns {Date[]} - Next n run times
|
|
1371
|
+
*/
|
|
1372
|
+
Cron.prototype.enumerate = function (n, previous) {
|
|
1373
|
+
if (n > this._states.maxRuns) {
|
|
1374
|
+
n = this._states.maxRuns;
|
|
1375
|
+
}
|
|
1376
|
+
const enumeration = [];
|
|
1377
|
+
let prev = previous || this._states.previousRun;
|
|
1378
|
+
while (n-- && (prev = this.next(prev))) {
|
|
1379
|
+
enumeration.push(prev);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
return enumeration;
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* Indicates wether or not the cron job is active, e.g. awaiting next trigger
|
|
1387
|
+
* @public
|
|
1388
|
+
*
|
|
1389
|
+
* @returns {boolean} - Running or not
|
|
1390
|
+
*/
|
|
1391
|
+
Cron.prototype.running = function () {
|
|
1392
|
+
const msLeft = this.msToNext(this._states.previousRun);
|
|
1393
|
+
const running = !this._states.paused && this.fn !== void 0;
|
|
1394
|
+
return msLeft !== null && running;
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
/**
|
|
1398
|
+
* Indicates wether or not the cron job is currently working
|
|
1399
|
+
* @public
|
|
1400
|
+
*
|
|
1401
|
+
* @returns {boolean} - Running or not
|
|
1402
|
+
*/
|
|
1403
|
+
Cron.prototype.busy = function () {
|
|
1404
|
+
return this._states.blocking;
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Return current/previous run start time
|
|
1409
|
+
* @public
|
|
1410
|
+
*
|
|
1411
|
+
* @returns {Date | null} - Previous run time
|
|
1412
|
+
*/
|
|
1413
|
+
Cron.prototype.started = function () {
|
|
1414
|
+
return this._states.currentRun ? this._states.currentRun.getDate() : null;
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Return previous run start time
|
|
1419
|
+
* @public
|
|
1420
|
+
*
|
|
1421
|
+
* @returns {Date | null} - Previous run time
|
|
1422
|
+
*/
|
|
1423
|
+
Cron.prototype.previous = function () {
|
|
1424
|
+
return this._states.previousRun ? this._states.previousRun.getDate() : null;
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Returns number of milliseconds to next run
|
|
1429
|
+
* @public
|
|
1430
|
+
*
|
|
1431
|
+
* @param {CronDate|Date|string} [prev] - Starting date, defaults to now - minimum interval
|
|
1432
|
+
* @returns {number | null}
|
|
1433
|
+
*/
|
|
1434
|
+
Cron.prototype.msToNext = function (prev) {
|
|
1435
|
+
// Get next run time
|
|
1436
|
+
const next = this._next(prev);
|
|
1437
|
+
|
|
1438
|
+
// Default previous for millisecond calculation
|
|
1439
|
+
prev = new CronDate(prev, this.options.timezone || this.options.utcOffset);
|
|
1440
|
+
|
|
1441
|
+
if (next) {
|
|
1442
|
+
return (next.getTime(true) - prev.getTime(true));
|
|
1443
|
+
} else {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Stop execution
|
|
1450
|
+
*
|
|
1451
|
+
* Running this will forcefully stop the job, and prevent furter exection. `.resume()` will not work after stopping.
|
|
1452
|
+
*
|
|
1453
|
+
* @public
|
|
1454
|
+
*/
|
|
1455
|
+
Cron.prototype.stop = function () {
|
|
1456
|
+
this._states.kill = true;
|
|
1457
|
+
// Stop any awaiting call
|
|
1458
|
+
if (this._states.currentTimeout) {
|
|
1459
|
+
clearTimeout(this._states.currentTimeout);
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Pause execution
|
|
1465
|
+
* @public
|
|
1466
|
+
*
|
|
1467
|
+
* @returns {boolean} - Wether pause was successful
|
|
1468
|
+
*/
|
|
1469
|
+
Cron.prototype.pause = function () {
|
|
1470
|
+
|
|
1471
|
+
this._states.paused = true;
|
|
1472
|
+
|
|
1473
|
+
return !this._states.kill;
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* Resume execution
|
|
1478
|
+
* @public
|
|
1479
|
+
*
|
|
1480
|
+
* @returns {boolean} - Wether resume was successful
|
|
1481
|
+
*/
|
|
1482
|
+
Cron.prototype.resume = function () {
|
|
1483
|
+
|
|
1484
|
+
this._states.paused = false;
|
|
1485
|
+
|
|
1486
|
+
return !this._states.kill;
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* Schedule a new job
|
|
1491
|
+
* @public
|
|
1492
|
+
*
|
|
1493
|
+
* @param {Function} func - Function to be run each iteration of pattern
|
|
1494
|
+
* @param {Date} [partial] - Internal function indicating a partial run
|
|
1495
|
+
* @returns {Cron}
|
|
1496
|
+
*/
|
|
1497
|
+
Cron.prototype.schedule = function (func, partial) {
|
|
1498
|
+
// If a function is already scheduled, bail out
|
|
1499
|
+
if (func && this.fn) {
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
"Cron: It is not allowed to schedule two functions using the same Croner instance.",
|
|
1502
|
+
);
|
|
1503
|
+
|
|
1504
|
+
// Update function if passed
|
|
1505
|
+
} else if (func) {
|
|
1506
|
+
this.fn = func;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Get ms to next run, bail out early if any of them is null (no next run)
|
|
1510
|
+
let waitMs = this.msToNext(partial ? partial : this._states.previousRun);
|
|
1511
|
+
const target = this.next(partial ? partial : this._states.previousRun);
|
|
1512
|
+
if (waitMs === null || target === null) return this;
|
|
1513
|
+
|
|
1514
|
+
// setTimeout cant handle more than Math.pow(2, 32 - 1) - 1 ms
|
|
1515
|
+
if (waitMs > maxDelay) {
|
|
1516
|
+
waitMs = maxDelay;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Start the timer loop
|
|
1520
|
+
// _checkTrigger will either call _trigger (if it's time, croner isn't paused and whatever),
|
|
1521
|
+
// or recurse back to this function to wait for next trigger
|
|
1522
|
+
this._states.currentTimeout = setTimeout(() => this._checkTrigger(target), waitMs);
|
|
1523
|
+
|
|
1524
|
+
// If unref option is set - unref the current timeout, which allows the process to exit even if there is a pending schedule
|
|
1525
|
+
if (this._states.currentTimeout && this.options.unref) {
|
|
1526
|
+
unrefTimer(this._states.currentTimeout);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
return this;
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* Internal function to trigger a run, used by both scheduled and manual trigger
|
|
1534
|
+
* @private
|
|
1535
|
+
*
|
|
1536
|
+
* @param {Date} [initiationDate]
|
|
1537
|
+
*/
|
|
1538
|
+
Cron.prototype._trigger = async function (initiationDate) {
|
|
1539
|
+
|
|
1540
|
+
this._states.blocking = true;
|
|
1541
|
+
|
|
1542
|
+
this._states.currentRun = new CronDate(
|
|
1543
|
+
initiationDate,
|
|
1544
|
+
this.options.timezone || this.options.utcOffset,
|
|
1545
|
+
);
|
|
1546
|
+
|
|
1547
|
+
if (this.options.catch) {
|
|
1548
|
+
try {
|
|
1549
|
+
await this.fn(this, this.options.context);
|
|
1550
|
+
} catch (_e) {
|
|
1551
|
+
if (isFunction(this.options.catch)) {
|
|
1552
|
+
// Do not await catch, even if it is synchronous
|
|
1553
|
+
setTimeout(() => this.options.catch(_e, this), 0);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
} else {
|
|
1557
|
+
// Trigger the function without catching
|
|
1558
|
+
await this.fn(this, this.options.context);
|
|
1559
|
+
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
this._states.previousRun = new CronDate(
|
|
1563
|
+
initiationDate,
|
|
1564
|
+
this.options.timezone || this.options.utcOffset,
|
|
1565
|
+
);
|
|
1566
|
+
|
|
1567
|
+
this._states.blocking = false;
|
|
1568
|
+
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* Trigger a run manually
|
|
1573
|
+
* @public
|
|
1574
|
+
*/
|
|
1575
|
+
Cron.prototype.trigger = async function () {
|
|
1576
|
+
await this._trigger();
|
|
1577
|
+
};
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* Called when it's time to trigger.
|
|
1581
|
+
* Checks if all conditions are currently met,
|
|
1582
|
+
* then instantly triggers the scheduled function.
|
|
1583
|
+
* @private
|
|
1584
|
+
*
|
|
1585
|
+
* @param {Date} target - Target Date
|
|
1586
|
+
*/
|
|
1587
|
+
Cron.prototype._checkTrigger = function (target) {
|
|
1588
|
+
const now = new Date(),
|
|
1589
|
+
shouldRun = !this._states.paused && now.getTime() >= target,
|
|
1590
|
+
isBlocked = this.blocking && this.options.protect;
|
|
1591
|
+
|
|
1592
|
+
if (shouldRun && !isBlocked) {
|
|
1593
|
+
this._states.maxRuns--;
|
|
1594
|
+
|
|
1595
|
+
// We do not await this
|
|
1596
|
+
this._trigger();
|
|
1597
|
+
|
|
1598
|
+
} else {
|
|
1599
|
+
// If this trigger were blocked, and protect is a function, trigger protect (without awaiting it, even if it's an synchronous function)
|
|
1600
|
+
if (shouldRun && isBlocked && isFunction(this.options.protect)) {
|
|
1601
|
+
setTimeout(() => this.options.protect(this), 0);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// Always reschedule
|
|
1606
|
+
this.schedule(undefined, now);
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Internal version of next. Cron needs millseconds internally, hence _next.
|
|
1611
|
+
* @private
|
|
1612
|
+
*
|
|
1613
|
+
* @param {CronDate|Date|string} prev - previousRun
|
|
1614
|
+
* @returns {CronDate | null} - Next run time
|
|
1615
|
+
*/
|
|
1616
|
+
Cron.prototype._next = function (prev) {
|
|
1617
|
+
const hasPreviousRun = (prev || this._states.previousRun) ? true : false;
|
|
1618
|
+
|
|
1619
|
+
// Ensure previous run is a CronDate
|
|
1620
|
+
prev = new CronDate(prev, this.options.timezone || this.options.utcOffset);
|
|
1621
|
+
|
|
1622
|
+
// Previous run should never be before startAt
|
|
1623
|
+
if (this.options.startAt && prev && prev.getTime() < this.options.startAt.getTime()) {
|
|
1624
|
+
prev = this.options.startAt;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Calculate next run according to pattern or one-off timestamp, pass actual previous run to increment
|
|
1628
|
+
const nextRun = this._states.once ||
|
|
1629
|
+
new CronDate(prev, this.options.timezone || this.options.utcOffset).increment(
|
|
1630
|
+
this.pattern,
|
|
1631
|
+
this.options,
|
|
1632
|
+
hasPreviousRun, // hasPreviousRun is used to allow
|
|
1633
|
+
);
|
|
1634
|
+
|
|
1635
|
+
if (this._states.once && this._states.once.getTime() <= prev.getTime()) {
|
|
1636
|
+
return null;
|
|
1637
|
+
} else if (
|
|
1638
|
+
(nextRun === null) ||
|
|
1639
|
+
(this._states.maxRuns <= 0) ||
|
|
1640
|
+
(this._states.kill) ||
|
|
1641
|
+
(this.options.stopAt && nextRun.getTime() >= this.options.stopAt.getTime())
|
|
1642
|
+
) {
|
|
1643
|
+
return null;
|
|
1644
|
+
} else {
|
|
1645
|
+
// All seem good, return next run
|
|
1646
|
+
return nextRun;
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
Cron.Cron = Cron;
|
|
1651
|
+
Cron.scheduledJobs = scheduledJobs;
|
|
1652
|
+
|
|
1653
|
+
module.exports = Cron;
|