croner 3.0.40 → 3.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/croner.js CHANGED
@@ -27,414 +27,49 @@
27
27
  THE SOFTWARE.
28
28
 
29
29
  ------------------------------------------------------------------------------------ */
30
-
31
- // ---- Type definitions ----------------------------------------------------------------
32
-
33
-
34
- /**
35
- * @typedef {"seconds" | "minutes" | "hours" | "days" | "months" | "daysOfWeek"} CronPatternPart
36
- */
37
-
38
- /**
39
- * @typedef {0 | -1} CronIndexOffset
40
- */
41
-
42
- /**
43
- * @typedef {Date | undefined} CronNextResult
44
- */
30
+ import { CronDate } from "./date.js";
31
+ import { CronPattern } from "./pattern.js";
45
32
 
46
33
  /**
34
+ * @typedef {CronDate | null} CronNextResult
35
+ *
47
36
  * @typedef {Object} CronOptions - Cron scheduler options
48
37
  * @property {boolean} [paused] - Job is paused
49
38
  * @property {boolean} [kill] - Job is about to be killed
50
- * @property {boolean} [rest] - Internal: Milliseconds left from previous run
39
+ * @property {number} [maxRuns] - Maximum nuber of executions
51
40
  * @property {number} [currentTimeout] - Internal: setTimeout "id"
52
41
  * @property {CronNextResult} [previous] - Previous run time
53
42
  * @property {string | Date} [startAt] - When to start running
54
43
  * @property {string | Date} [stopAt] - When to stop running
44
+ * @property {string} [timezone] - Time zone in Europe/Stockholm format
55
45
  */
56
46
 
57
47
  /**
58
48
  * @typedef {Function} CronJobStop - Stop current job
59
49
  * @returns {boolean} - If pause was successful
60
- */
61
-
62
- /**
50
+ *
63
51
  * @typedef {Function} CronJobResume - Resume current job
64
52
  * @returns {boolean} - If resume was successful
65
- */
66
-
67
- /**
53
+ *
68
54
  * @typedef {Object} CronJob - Cron job control functions
69
55
  * @property {CronJobStop} stop
70
56
  * @property {CronJobResume} pause
71
57
  * @property {Function} resume
72
58
  */
73
59
 
74
- // Many JS engines stores the delay as a 32-bit signed integer internally.
75
- // This causes an integer overflow when using delays larger than 2147483647,
76
- // resulting in the timeout being executed immediately.
77
- //
78
- // All JS engines implements an immediate execution of delays larger that a 32-bit
79
- // int to keep the behaviour concistent.
80
- const maxDelay = Math.pow(2, 32 - 1) - 1;
81
-
82
-
83
-
84
- //
85
- // ---- Helper functions ------------------------------------------------------------
86
- //
87
-
88
- function raise (err) {
89
- throw new TypeError("Cron parser: " + err);
90
- }
91
-
92
- function fill(arr, val) {
93
-
94
- // Simple "Polyfill" for Array.fill on pre ES6 environments
95
- for(let i = 0; i < arr.length; i++) {
96
- arr[i] = val;
97
- }
98
-
99
- return arr;
100
-
101
- }
102
-
103
-
104
-
105
- //
106
- // ---- CronDate ---------------------------------------------------------------------
107
- //
108
60
 
109
61
  /**
110
- * Converts date to CronDate
111
- * @constructor
112
- * @param {date} date - Input date
113
- */
114
- function CronDate (date) {
115
- this.milliseconds = date.getMilliseconds();
116
- this.seconds = date.getSeconds();
117
- this.minutes = date.getMinutes();
118
- this.hours = date.getHours();
119
- this.days = date.getDate();
120
- this.months = date.getMonth();
121
- this.years = date.getFullYear();
122
- }
123
-
124
- /**
125
- * Increment to next run time
62
+ * Many JS engines stores the delay as a 32-bit signed integer internally.
63
+ * This causes an integer overflow when using delays larger than 2147483647,
64
+ * resulting in the timeout being executed immediately.
126
65
  *
127
- * @param {string} pattern - The pattern used to increment current state
128
- */
129
- CronDate.prototype.increment = function (pattern) {
130
-
131
- this.seconds += 1;
132
- this.milliseconds = 0;
133
-
134
- let self = this,
135
-
136
-
137
- /**
138
- * Find next
139
- *
140
- * @param {string} target
141
- * @param {string} pattern
142
- * @param {string} offset
143
- * @param {string} override
144
- *
145
- * @returns {boolean}
146
- *
147
- */
148
- findNext = function (target, pattern, offset, override) {
149
-
150
- let startPos = (override === void 0) ? self[target] + offset : 0 + offset;
151
-
152
- for( let i = startPos; i < pattern[target].length; i++ ) {
153
- if( pattern[target][i] ) {
154
- self[target] = i-offset;
155
- return true;
156
- }
157
- }
158
-
159
- return false;
160
-
161
- };
162
-
163
- // Array of work to be done, consisting of subarrays described below:
164
- // [
165
- // First item is which member to process,
166
- // Second item is which member to increment if we didn't find a mathch in current item,
167
- // Third item is an offset. if months is handled 0-11 in js date object, and we get 1-12
168
- // from pattern. Offset should be -1
169
- // ]
170
- let toDo = [
171
- ["seconds", "minutes", 0],
172
- ["minutes", "hours", 0],
173
- ["hours", "days", 0],
174
- ["days", "months", -1],
175
- ["months", "years", 0]
176
- ],
177
- doing = 0;
178
-
179
- // Ok, we're working our way trough the toDo array, top to bottom
180
- // If we reach 5, work is done
181
- while(doing < 5) {
182
-
183
- // findNext sets the current member to next match in pattern
184
- // If time is 00:00:01 and pattern says *:*:05, seconds will
185
- // be set to 5
186
- if(!findNext(toDo[doing][0], pattern, toDo[doing][2])) {
187
-
188
- // If pattern didn't provide a match, increment next vanlue (e.g. minues)
189
- this[toDo[doing][1]]++;
190
-
191
- // Now when we have gone to next minute, we have to set seconds to the first match
192
- // Now we are at 00:01:05 following the same example.
193
- //
194
- // This goes all the way back to seconds, hence the reverse loop.
195
- while(doing >= 0) {
196
-
197
- // Ok, reset current member(e.g. seconds) to first match in pattern, using
198
- // the same method as aerlier
199
- //
200
- // Note the fourth parameter, stating that we should start matching the pattern
201
- // from zero, instead of current time.
202
- findNext(toDo[doing][0], pattern, toDo[doing][2], 0);
203
-
204
- // Go back up, days -> hours -> minutes -> seconds
205
- doing--;
206
- }
207
- }
208
-
209
- // Gp down, seconds -> minutes -> hours -> days -> months -> year
210
- doing++;
211
- }
212
-
213
- // This is a special case for weekday, as the user isn't able to combine date/month patterns
214
- // with weekday patterns, it's just to increment days until we get a match.
215
- while (!pattern.daysOfWeek[this.getDate().getDay()]) {
216
- this.days += 1;
217
- }
218
-
219
- };
220
-
221
- /**
222
- * Convert current state back to a javascript Date()
223
- *
224
- * @returns {date}
66
+ * All JS engines implements an immediate execution of delays larger that a 32-bit
67
+ * int to keep the behaviour concistent.
225
68
  *
69
+ * @type {number}
226
70
  */
227
- CronDate.prototype.getDate = function () {
228
- return new Date(this.years, this.months, this.days, this.hours, this.minutes, this.seconds, this.milliseconds);
229
- };
230
-
231
-
232
-
233
- //
234
- // ---- CronPattern ---------------------------------------------------------------------
235
- //
236
-
237
- /**
238
- * Create a CronPattern instance from pattern string ('* * * * * *')
239
- * @constructor
240
- * @param {string} pattern - Input pattern
241
- */
242
- function CronPattern (pattern) {
243
-
244
- this.pattern = pattern;
245
-
246
- this.seconds = fill(Array(60),0); // 0-59
247
- this.minutes = fill(Array(60),0); // 0-59
248
- this.hours = fill(Array(24),0); // 0-23
249
- this.days = fill(Array(31),0); // 0-30 in array, 1-31 in config
250
- this.months = fill(Array(12),0); // 0-11 in array, 1-12 in config
251
- this.daysOfWeek = fill(Array(8),0); // 0-7 Where 0 = Sunday and 7=Sunday;
252
-
253
- this.parse();
254
-
255
- }
256
-
257
- /**
258
- * Parse current pattern, will raise an error on failure
259
- */
260
- CronPattern.prototype.parse = function () {
261
-
262
- // Sanity check
263
- if( !(typeof this.pattern === "string" || this.pattern.constructor === String) ) {
264
- raise("Pattern has to be of type string.");
265
- }
266
-
267
- // Split configuration on whitespace
268
- let parts = this.pattern.trim().replace(/\s+/g, " ").split(" "),
269
- part,
270
- i,
271
- reValidCron = /[^/*0-9,-]+/,
272
- hasMonths,
273
- hasDaysOfWeek,
274
- hasDates;
275
-
276
- // Validite number of configuration entries
277
- if( parts.length < 5 || parts.length > 6 ) {
278
- raise("invalid configuration format ('" + this.pattern + "'), exacly five or six space separated parts required.");
279
- }
280
-
281
- // If seconds is omitted, insert 0 for seconds
282
- if( parts.length == 5) {
283
- parts.unshift("0");
284
- }
285
-
286
- // Validate field content
287
- for( i = 0; i < parts.length; i++ ) {
288
- part = parts[i].trim();
289
-
290
- // Check that part only contain legal characters ^[0-9-,]+$
291
- if( reValidCron.test(part) ) {
292
- raise("configuration entry " + (i + 1) + " (" + part + ") contains illegal characters.");
293
- }
294
- }
295
-
296
- // Check that we dont have both months and daysofweek
297
- hasMonths = (parts[4] !== "*");
298
- hasDaysOfWeek = (parts[5] !== "*");
299
- hasDates = (parts[3] !== "*");
300
-
301
- // Month/Date and dayofweek is incompatible
302
- if( hasDaysOfWeek && (hasMonths || hasDates) ) {
303
- raise("configuration invalid, you can not combine month/date with day of week.");
304
- }
305
-
306
- // Parse parts into arrays, validates as we go
307
- this.partToArray("seconds", parts[0], 0);
308
- this.partToArray("minutes", parts[1], 0);
309
- this.partToArray("hours", parts[2], 0);
310
- this.partToArray("days", parts[3], -1);
311
- this.partToArray("months", parts[4], -1);
312
- this.partToArray("daysOfWeek", parts[5], 0);
313
-
314
- // 0 = Sunday, 7 = Sunday
315
- if( this.daysOfWeek[7] ) {
316
- this.daysOfWeek[0] = 1;
317
- }
318
-
319
- };
320
-
321
- /**
322
- * 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.
323
- *
324
- * @param {CronPatternPart} type - Seconds/minutes etc
325
- * @param {string} conf - Current pattern part - *, 0-1 etc
326
- * @param {CronIndexOffset} valueIndexOffset - 0 or -1. 0 for seconds,minutes, hours as they start on 1. -1 on days and months, as the start on 0
327
- */
328
- CronPattern.prototype.partToArray = function (type, conf, valueIndexOffset) {
329
-
330
- let i,
331
- split,
332
- lower,
333
- upper,
334
- steps,
335
- arr = this[type];
336
-
337
- // First off, handle wildcard
338
- if( conf === "*" ) {
339
- for( i = 0; i < arr.length; i++ ) {
340
- arr[i] = 1;
341
- }
342
- return;
343
- }
344
-
345
- // Recurse into comma separated entries
346
- split = conf.split(",");
347
- if( split.length > 1 ) {
348
- for( i = 0; i < split.length; i++ ) {
349
- this.partToArray(type, split[i], valueIndexOffset);
350
- }
351
-
352
- return;
353
- }
354
-
355
- // Didn't need to recurse, determine if this is a range, steps or a number
356
- // - Got a range
357
- if( conf.indexOf("-") !== -1 ) {
358
-
359
- split = conf.split("-");
360
-
361
- if( split.length !== 2 ) {
362
- raise("Syntax error, illegal range: '" + conf + "'");
363
- }
364
-
365
- lower = parseInt(split[0], 10) + valueIndexOffset;
366
- upper = parseInt(split[1], 10) + valueIndexOffset;
367
-
368
- if( isNaN(lower) ) {
369
- raise("Syntax error, illegal lower range (NaN)");
370
- } else if( isNaN(upper) ) {
371
- raise("Syntax error, illegal upper range (NaN)");
372
- }
373
-
374
- // Check that value is within range
375
- if( lower < 0 || upper >= arr.length ) {
376
- raise("Value out of range: '" + conf + "'");
377
- }
378
-
379
- //
380
- if( lower > upper ) {
381
- raise("From value is larger than to value: '" + conf + "'");
382
- }
383
-
384
- for( i = lower; i <= upper; i++ ) {
385
- arr[(i + valueIndexOffset)] = 1;
386
- }
387
-
388
- // - Got stepping
389
- } else if( conf.indexOf("/") !== -1 ) {
390
-
391
- split = conf.split("/");
392
-
393
- if( split.length !== 2 ) {
394
- raise("Syntax error, illegal stepping: '" + conf + "'");
395
- }
396
-
397
- if( split[0] !== "*" ) {
398
- raise("Syntax error, left part of / needs to be * : '" + conf + "'");
399
- }
400
-
401
- steps = parseInt(split[1], 10);
402
-
403
- if( isNaN(steps) ) {
404
- raise("Syntax error, illegal stepping: (NaN)");
405
- }
406
-
407
- if( steps === 0 ) {
408
- raise("Syntax error, illegal stepping: 0");
409
- }
410
-
411
- if( steps > arr.length ) {
412
- raise("Syntax error, steps cannot be greater than maximum value of part ("+arr.length+")");
413
- }
414
-
415
- for( i = 0; i < arr.length; i+= steps ) {
416
- arr[(i + valueIndexOffset)] = 1;
417
- }
418
-
419
- // - Got a number
420
- } else {
421
-
422
- i = (parseInt(conf, 10) + valueIndexOffset);
423
-
424
- if( i < 0 || i >= arr.length ) {
425
- raise(type + " value out of range: '" + conf + "'");
426
- }
427
-
428
- arr[i] = 1;
429
- }
430
-
431
- };
432
-
433
-
71
+ const maxDelay = Math.pow(2, 32 - 1) - 1;
434
72
 
435
- //
436
- // ---- Cron --------------------------------------------------------------------------
437
- //
438
73
  /**
439
74
  * Cron entrypoint
440
75
  *
@@ -457,7 +92,6 @@ function Cron (pattern, options, fn) {
457
92
 
458
93
  /** @type {CronOptions} */
459
94
  self.schedulerDefaults = {
460
- stopAt: Infinity,
461
95
  maxRuns: Infinity,
462
96
  kill: false
463
97
  };
@@ -468,8 +102,10 @@ function Cron (pattern, options, fn) {
468
102
  options = {};
469
103
  }
470
104
 
471
- // Store and validate options
472
- /** @type {CronOptions} */
105
+ /**
106
+ * Store and validate options
107
+ * @type {CronOptions}
108
+ */
473
109
  self.opts = self.validateOpts(options || {});
474
110
 
475
111
  // Determine what to return, default is self
@@ -489,56 +125,50 @@ function Cron (pattern, options, fn) {
489
125
  * Find next runtime, based on supplied date. Strips milliseconds.
490
126
  *
491
127
  * @param {Date} prev - Input pattern
492
- * @returns {CronNextResult} - Next run time
128
+ * @returns {Date | null} - Next run time
493
129
  */
494
130
  Cron.prototype.next = function (prev) {
495
- let dirtyDate = this._next(prev);
496
- if (dirtyDate) dirtyDate.setMilliseconds(0);
497
- return dirtyDate;
131
+ let next = this._next(prev);
132
+ return next ? next.getDate() : null;
498
133
  };
499
134
 
500
135
  /**
501
- * Return previos run time
136
+ * Return previous run time
502
137
  *
503
- * @returns {Date?} - Previous run time
138
+ * @returns {Date | null} - Previous run time
504
139
  */
505
140
  Cron.prototype.previous = function () {
506
- return this.opts.previous;
141
+ return this.opts.previous ? this.opts.previous.getDate() : null;
507
142
  };
508
143
 
509
144
  /**
510
145
  * Internal version of next. Cron needs millseconds internally, hence _next.
511
146
  *
512
147
  * @param {Date} prev - Input pattern
513
- * @returns {CronNextResult} - Next run time
148
+ * @returns {CronNextResult | null} - Next run time
514
149
  */
515
150
  Cron.prototype._next = function (prev) {
516
151
 
517
- prev = prev || new Date();
152
+ prev = new CronDate(prev, this.opts.timezone);
518
153
 
519
154
  // Previous run should never be before startAt
520
- if( this.opts.startAt && prev < this.opts.startAt ) {
521
- prev = this.opts.startAt;
155
+ if( this.opts.startAt && prev && prev.getTime() < this.opts.startAt.getTime() ) {
156
+ prev = new CronDate(this.opts.startAt, this.opts.timezone);
522
157
  }
523
158
 
159
+ // Calculate next run
160
+ let nextRun = new CronDate(prev, this.opts.timezone).increment(this.pattern);
161
+
524
162
  // Check for stop condition
525
163
  if ((this.opts.maxRuns <= 0) ||
526
- (this.opts.kill) ) {
527
- return void 0;
164
+ (this.opts.kill) ||
165
+ (this.opts.stopAt && nextRun.getTime() >= this.opts.stopAt.getTime() )) {
166
+ return null;
167
+ } else {
168
+ // All seem good, return next run
169
+ return nextRun;
528
170
  }
529
-
530
- let
531
- stopAt = this.opts.stopAt || this.schedulerDefaults.stopAt,
532
- cronDate = new CronDate(prev),
533
- nextRun;
534
-
535
- cronDate.increment(this.pattern);
536
-
537
- // Get next run
538
- nextRun = cronDate.getDate();
539
-
540
- // All seem good, return next run
541
- return !(stopAt && nextRun >= stopAt ) ? nextRun : void 0;
171
+
542
172
  };
543
173
 
544
174
  /**
@@ -550,26 +180,10 @@ Cron.prototype._next = function (prev) {
550
180
  Cron.prototype.validateOpts = function (opts) {
551
181
  // startAt is set, validate it
552
182
  if( opts.startAt ) {
553
- if( opts.startAt.constructor !== Date ) {
554
- opts.startAt = new Date(Date.parse(opts.startAt)-1);
555
- } else {
556
- opts.startAt = new Date(opts.startAt.getTime()-1);
557
- }
558
-
559
- // Raise if we did get an invalid date
560
- if( isNaN(opts.startAt) ) {
561
- raise("Provided value for startAt could not be parsed as date.");
562
- }
183
+ opts.startAt = new CronDate(opts.startAt, opts.timezone);
563
184
  }
564
185
  if( opts.stopAt ) {
565
- if( opts.stopAt.constructor !== Date ) {
566
- opts.stopAt = new Date(Date.parse(opts.stopAt));
567
- }
568
-
569
- // Raise if we did get an invalid date
570
- if( isNaN(opts.stopAt) ) {
571
- raise("Provided value for stopAt could not be parsed as date.");
572
- }
186
+ opts.stopAt = new CronDate(opts.stopAt, opts.timezone);
573
187
  }
574
188
  return opts;
575
189
  };
@@ -577,16 +191,16 @@ Cron.prototype.validateOpts = function (opts) {
577
191
  /**
578
192
  * Returns number of milliseconds to next run
579
193
  *
580
- * @param {CronNextResult} [prev=new Date()] - Starting date, defaults to now
581
- * @returns {number | CronNextResult}
194
+ * @param {CronNextResult} [prev=new CronDate()] - Starting date, defaults to now
195
+ * @returns {number | null}
582
196
  */
583
197
  Cron.prototype.msToNext = function (prev) {
584
- prev = prev || new Date();
198
+ prev = prev || new CronDate(void 0, this.opts.timezone);
585
199
  let next = this._next(prev);
586
200
  if( next ) {
587
- return (this._next(prev) - prev.getTime());
201
+ return (next.getTime() - prev.getTime());
588
202
  } else {
589
- return next;
203
+ return null;
590
204
  }
591
205
  };
592
206
 
@@ -599,36 +213,50 @@ Cron.prototype.msToNext = function (prev) {
599
213
  * @returns {CronJob}
600
214
  */
601
215
  Cron.prototype.schedule = function (opts, func) {
602
-
603
- let self = this,
604
- waitMs,
605
-
606
- // Prioritize context before closure,
607
- // to allow testing of maximum delay.
608
- _maxDelay = self.maxDelay || maxDelay;
609
-
216
+
610
217
  // Make opts optional
611
218
  if( !func ) {
612
219
  func = opts;
613
- opts = {};
220
+
221
+ // If options isn't passed to schedule, use stored options
222
+ opts = this.opts;
614
223
  }
615
224
 
616
225
  // Keep options, or set defaults
617
226
  opts.paused = (opts.paused === void 0) ? false : opts.paused;
618
227
  opts.kill = opts.kill || this.schedulerDefaults.kill;
619
- opts.rest = opts.rest || 0;
620
228
  if( !opts.maxRuns && opts.maxRuns !== 0 ) {
621
229
  opts.maxRuns = this.schedulerDefaults.maxRuns;
622
230
  }
623
231
 
624
232
  // Store options
625
- self.opts = self.validateOpts(opts || {});
233
+ this.opts = this.validateOpts(opts || {});
234
+
235
+ this._schedule(opts, func);
236
+ };
237
+
238
+ /**
239
+ * Schedule a new job
240
+ *
241
+ * @constructor
242
+ * @param {CronOptions | Function} [options] - Options
243
+ * @param {Function} [func] - Function to be run each iteration of pattern
244
+ * @returns {CronJob}
245
+ */
246
+ Cron.prototype._schedule = function (opts, func) {
247
+
248
+ let self = this,
249
+ waitMs,
250
+
251
+ // Prioritize context before closure,
252
+ // to allow testing of maximum delay.
253
+ _maxDelay = self.maxDelay || maxDelay;
626
254
 
627
255
  // Get ms to next run
628
- waitMs = this.msToNext(opts.previous);
256
+ waitMs = this.msToNext(self.opts.previous);
629
257
 
630
258
  // Check for stop conditions
631
- if ( waitMs === void 0 ) {
259
+ if ( waitMs === null ) {
632
260
  return;
633
261
  }
634
262
 
@@ -638,21 +266,22 @@ Cron.prototype.schedule = function (opts, func) {
638
266
  }
639
267
 
640
268
  // All ok, go go!
641
- opts.currentTimeout = setTimeout(function () {
269
+ self.opts.currentTimeout = setTimeout(function () {
642
270
 
643
271
  // Are we running? If waitMs is maxed out, this is a blank run
644
272
  if( waitMs !== _maxDelay ) {
645
273
 
646
- if ( !opts.paused ) {
647
- opts.maxRuns--;
274
+ if ( !self.opts.paused ) {
275
+ self.opts.maxRuns--;
648
276
  func();
649
277
  }
650
278
 
651
- opts.previous = new Date();
279
+ self.opts.previous = new CronDate(self.opts.previous, self.opts.timezone);
280
+
652
281
  }
653
282
 
654
283
  // Recurse
655
- self.schedule(opts, func);
284
+ self._schedule(self.opts, func);
656
285
 
657
286
  }, waitMs );
658
287
 
@@ -661,21 +290,21 @@ Cron.prototype.schedule = function (opts, func) {
661
290
 
662
291
  // Return undefined
663
292
  stop: function() {
664
- opts.kill = true;
293
+ self.opts.kill = true;
665
294
  // Stop any awaiting call
666
- if( opts.currentTimeout ) {
667
- clearTimeout( opts.currentTimeout );
295
+ if( self.opts.currentTimeout ) {
296
+ clearTimeout( self.opts.currentTimeout );
668
297
  }
669
298
  },
670
299
 
671
300
  // Return bool wether pause were successful
672
301
  pause: function() {
673
- return (opts.paused = true) && !opts.kill;
302
+ return (self.opts.paused = true) && !self.opts.kill;
674
303
  },
675
304
 
676
305
  // Return bool wether resume were successful
677
306
  resume: function () {
678
- return !(opts.paused = false) && !opts.kill;
307
+ return !(self.opts.paused = false) && !self.opts.kill;
679
308
  }
680
309
 
681
310
  };