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.
Files changed (2) hide show
  1. package/dist/croner.cjs +1653 -0
  2. package/package.json +2 -3
@@ -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;