croner 5.7.0 → 6.0.0-dev.0

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