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