1033 lines
31 KiB
JavaScript
1033 lines
31 KiB
JavaScript
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Cron = factory());
|
|
})(this, (function () { 'use strict';
|
|
|
|
/**
|
|
* "Converts" a date to a specific time zone
|
|
*
|
|
* Note: This is only for specific and controlled usage,
|
|
* as the internal UTC time of the resulting object will be off.
|
|
*
|
|
* Example:
|
|
* let normalDate = new Date(); // d is a normal Date instance, with local timezone and correct utc representation
|
|
* tzDate = convertTZ(d, 'America/New_York') // d is a tainted Date instance, where getHours()
|
|
* (for example) will return local time in new york, but getUTCHours()
|
|
* will return something irrelevant.
|
|
*
|
|
* @param {Date} date - Input date
|
|
* @param {string} tzString - Timezone string in Europe/Stockholm format
|
|
* @returns {Date}
|
|
*/
|
|
function convertTZ(date, tzString) {
|
|
return new Date(date.toLocaleString("en-US", {timeZone: tzString}));
|
|
}
|
|
|
|
/**
|
|
* Converts date to CronDate
|
|
* @constructor
|
|
*
|
|
* @param {CronDate|date|string} [date] - Input date, if using string representation ISO 8001 (2015-11-24T19:40:00) local timezone is expected
|
|
* @param {string} [timezone] - String representation of target timezone in Europe/Stockholm format.
|
|
*/
|
|
function CronDate (date, timezone) {
|
|
|
|
this.timezone = timezone;
|
|
|
|
if (date && date instanceof Date) {
|
|
this.fromDate(date);
|
|
} else if (date === void 0) {
|
|
this.fromDate(new Date());
|
|
} else if (date && typeof date === "string") {
|
|
this.fromString(date);
|
|
} else if (date instanceof CronDate) {
|
|
this.fromCronDate(date);
|
|
} else {
|
|
throw new TypeError("CronDate: Invalid type (" + typeof date + ") passed as parameter to CronDate constructor");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets internals using a Date
|
|
* @private
|
|
*
|
|
* @param {Date} date - Input date
|
|
*/
|
|
CronDate.prototype.fromDate = function (date) {
|
|
|
|
if (this.timezone) {
|
|
date = convertTZ(date, this.timezone);
|
|
}
|
|
|
|
this.milliseconds = date.getMilliseconds();
|
|
this.seconds = date.getSeconds();
|
|
this.minutes = date.getMinutes();
|
|
this.hours = date.getHours();
|
|
this.days = date.getDate();
|
|
this.months = date.getMonth();
|
|
this.years = date.getFullYear();
|
|
|
|
};
|
|
|
|
/**
|
|
* Sets internals by deep copying another CronDate
|
|
* @private
|
|
*
|
|
* @param {CronDate} date - Input date
|
|
*/
|
|
CronDate.prototype.fromCronDate = function (date) {
|
|
this.timezone = date.timezone;
|
|
this.milliseconds = date.milliseconds;
|
|
this.seconds = date.seconds;
|
|
this.minutes = date.minutes;
|
|
this.hours = date.hours;
|
|
this.days = date.days;
|
|
this.months = date.months;
|
|
this.years = date.years;
|
|
};
|
|
|
|
/**
|
|
* Reset internal parameters (seconds, minutes, hours) that may have exceeded their ranges
|
|
* @private
|
|
*
|
|
* @param {Date} date - Input date
|
|
*/
|
|
CronDate.prototype.apply = function () {
|
|
const newDate = new Date(this.years, this.months, this.days, this.hours, this.minutes, this.seconds, this.milliseconds);
|
|
|
|
this.milliseconds = newDate.getMilliseconds();
|
|
this.seconds = newDate.getSeconds();
|
|
this.minutes = newDate.getMinutes();
|
|
this.hours = newDate.getHours();
|
|
this.days = newDate.getDate();
|
|
this.months = newDate.getMonth();
|
|
this.years = newDate.getFullYear();
|
|
};
|
|
|
|
/**
|
|
* Sets internals by parsing a string
|
|
* @private
|
|
*
|
|
* @param {Date} date - Input date
|
|
*/
|
|
CronDate.prototype.fromString = function (str) {
|
|
|
|
const parsedDate = this.parseISOLocal(str);
|
|
|
|
// Throw if we did get an invalid date
|
|
if( isNaN(parsedDate) ) {
|
|
throw new TypeError("CronDate: Provided string value for CronDate could not be parsed as date.");
|
|
}
|
|
|
|
this.fromDate(parsedDate);
|
|
};
|
|
|
|
/**
|
|
* Increment to next run time
|
|
* @public
|
|
*
|
|
* @param {string} pattern - The pattern used to increment current state
|
|
* @param {boolean} [rerun=false] - If this is an internal incremental run
|
|
* @return {CronDate|null} - Returns itself for chaining, or null if increment wasnt possible
|
|
*/
|
|
CronDate.prototype.increment = function (pattern, rerun) {
|
|
|
|
if (!rerun) {
|
|
this.seconds += 1;
|
|
}
|
|
|
|
this.milliseconds = 0;
|
|
|
|
const
|
|
origTime = this.getTime(),
|
|
|
|
/**
|
|
* Find next
|
|
*
|
|
* @param {string} target
|
|
* @param {string} pattern
|
|
* @param {string} offset
|
|
* @param {string} override
|
|
*
|
|
* @returns {boolean}
|
|
*
|
|
*/
|
|
findNext = (target, pattern, offset, override) => {
|
|
|
|
const startPos = (override === void 0) ? this[target] + offset : 0 + offset;
|
|
|
|
for( let i = startPos; i < pattern[target].length; i++ ) {
|
|
|
|
// If pattern matches and, in case of days, weekday matches, go on
|
|
if( pattern[target][i] ) {
|
|
|
|
// Special handling for L (last day of month), when we are searching for days
|
|
if (target === "days" && pattern.lastDayOfMonth) {
|
|
let baseDate = this.getDate(true);
|
|
|
|
// Set days to one day after today, if month changes, then we are at the last day of the month
|
|
baseDate.setDate(i-offset+1);
|
|
if (baseDate.getMonth() !== this["months"]) {
|
|
this[target] = i-offset;
|
|
return true;
|
|
}
|
|
|
|
// Normal handling
|
|
} else {
|
|
this[target] = i-offset;
|
|
return true;
|
|
}
|
|
|
|
}
|
|
}
|
|
return false;
|
|
|
|
},
|
|
|
|
resetPrevious = (offset) => {
|
|
// Now when we have gone to next minute, we have to set seconds to the first match
|
|
// Now we are at 00:01:05 following the same example.
|
|
//
|
|
// This goes all the way back to seconds, hence the reverse loop.
|
|
while(doing + offset >= 0) {
|
|
|
|
// Ok, reset current member(e.g. seconds) to first match in pattern, using
|
|
// the same method as aerlier
|
|
//
|
|
// Note the fourth parameter, stating that we should start matching the pattern
|
|
// from zero, instead of current time.
|
|
findNext(toDo[doing + offset][0], pattern, toDo[doing + offset][2], 0);
|
|
|
|
// Go back up, days -> hours -> minutes -> seconds
|
|
doing--;
|
|
}
|
|
};
|
|
|
|
// Array of work to be done, consisting of subarrays described below:
|
|
// [
|
|
// First item is which member to process,
|
|
// Second item is which member to increment if we didn't find a mathch in current item,
|
|
// Third item is an offset. if months is handled 0-11 in js date object, and we get 1-12
|
|
// from pattern. Offset should be -1
|
|
// ]
|
|
const toDo = [
|
|
["seconds", "minutes", 0],
|
|
["minutes", "hours", 0],
|
|
["hours", "days", 0],
|
|
["days", "months", -1],
|
|
["months", "years", 0]
|
|
];
|
|
|
|
// Ok, we're working our way trough the toDo array, top to bottom
|
|
// If we reach 5, work is done
|
|
let doing = 0;
|
|
while(doing < 5) {
|
|
|
|
// findNext sets the current member to next match in pattern
|
|
// If time is 00:00:01 and pattern says *:*:05, seconds will
|
|
// be set to 5
|
|
|
|
// Store current value at current level
|
|
let currentValue = this[toDo[doing][0]];
|
|
|
|
// If pattern didn't provide a match, increment next value (e.g. minues)
|
|
if(!findNext(toDo[doing][0], pattern, toDo[doing][2])) {
|
|
this[toDo[doing][1]]++;
|
|
|
|
// Reset current level and previous levels
|
|
resetPrevious(0);
|
|
|
|
// If pattern provided a match, but changed current value ...
|
|
} else if (currentValue !== this[toDo[doing][0]]) {
|
|
// Reset previous levels
|
|
resetPrevious(-1);
|
|
|
|
}
|
|
|
|
// Bail out if an impossible pattern is used
|
|
if (this.years >= 4000) {
|
|
return null;
|
|
}
|
|
|
|
// Gp down, seconds -> minutes -> hours -> days -> months -> year
|
|
doing++;
|
|
}
|
|
|
|
// This is a special case for weekday, as the user isn't able to combine date/month patterns
|
|
// with weekday patterns, it's just to increment days until we get a match.
|
|
while (!pattern.daysOfWeek[this.getDate(true).getDay()]) {
|
|
this.days += 1;
|
|
|
|
// Reset everything before days
|
|
doing = 2;
|
|
resetPrevious();
|
|
}
|
|
|
|
// If anything changed, recreate this CronDate and run again without incrementing
|
|
if (origTime != this.getTime()) {
|
|
this.apply();
|
|
return this.increment(pattern, true);
|
|
} else {
|
|
return this;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Convert current state back to a javascript Date()
|
|
* @public
|
|
*
|
|
* @param {boolean} internal - If this is an internal call
|
|
* @returns {Date}
|
|
*/
|
|
CronDate.prototype.getDate = function (internal) {
|
|
const targetDate = new Date(this.years, this.months, this.days, this.hours, this.minutes, this.seconds, this.milliseconds);
|
|
if (internal || !this.timezone) {
|
|
return targetDate;
|
|
} else {
|
|
const offset = convertTZ(targetDate, this.timezone).getTime()-targetDate.getTime();
|
|
return new Date(targetDate.getTime()-offset);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Convert current state back to a javascript Date() and return UTC milliseconds
|
|
* @public
|
|
*
|
|
* @param {boolean} internal - If this is an internal call
|
|
* @returns {Date}
|
|
*/
|
|
CronDate.prototype.getTime = function (internal) {
|
|
return this.getDate(internal).getTime();
|
|
};
|
|
|
|
/**
|
|
* Takes a iso 8001 local date time string and creates a Date object
|
|
* @private
|
|
*
|
|
* @param {string} dateTimeString - an ISO 8001 format date and time string
|
|
* with all components, e.g. 2015-11-24T19:40:00
|
|
* @returns {Date|number} - Date instance from parsing the string. May be NaN.
|
|
*/
|
|
CronDate.prototype.parseISOLocal = function (dateTimeString) {
|
|
const dateTimeStringSplit = dateTimeString.split(/\D/);
|
|
|
|
// Check for completeness
|
|
if (dateTimeStringSplit.length < 6) {
|
|
return NaN;
|
|
}
|
|
|
|
const
|
|
year = parseInt(dateTimeStringSplit[0], 10),
|
|
month = parseInt(dateTimeStringSplit[1], 10),
|
|
day = parseInt(dateTimeStringSplit[2], 10),
|
|
hour = parseInt(dateTimeStringSplit[3], 10),
|
|
minute = parseInt(dateTimeStringSplit[4], 10),
|
|
second = parseInt(dateTimeStringSplit[5], 10);
|
|
|
|
// Check parts for numeric
|
|
if( isNaN(year) || isNaN(month) || isNaN(day) || isNaN(hour) || isNaN(minute) || isNaN(second) ) {
|
|
return NaN;
|
|
} else {
|
|
let generatedDate;
|
|
|
|
// Check for UTC flag
|
|
if ((dateTimeString.indexOf("Z") > 0)) {
|
|
|
|
// Handle date as UTC
|
|
generatedDate = new Date(Date.UTC(year, month-1, day, hour, minute, second));
|
|
|
|
// Check generated date
|
|
if (year == generatedDate.getUTCFullYear()
|
|
&& month == generatedDate.getUTCMonth()+1
|
|
&& day == generatedDate.getUTCDate()
|
|
&& hour == generatedDate.getUTCHours()
|
|
&& minute == generatedDate.getUTCMinutes()
|
|
&& second == generatedDate.getUTCSeconds()) {
|
|
return generatedDate;
|
|
} else {
|
|
return NaN;
|
|
}
|
|
} else {
|
|
|
|
// Handle date as local time
|
|
generatedDate = new Date(year, month-1, day, hour, minute, second);
|
|
|
|
// Check generated date
|
|
if (year == generatedDate.getFullYear()
|
|
&& month == generatedDate.getMonth()+1
|
|
&& day == generatedDate.getDate()
|
|
&& hour == generatedDate.getHours()
|
|
&& minute == generatedDate.getMinutes()
|
|
&& second == generatedDate.getSeconds()) {
|
|
return generatedDate;
|
|
} else {
|
|
return NaN;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Name for each part of the cron pattern
|
|
* @typedef {("seconds" | "minutes" | "hours" | "days" | "months" | "daysOfWeek")} CronPatternPart
|
|
*/
|
|
|
|
/**
|
|
* Offset, 0 or -1.
|
|
*
|
|
* 0 for seconds,minutes and hours as they start on 1.
|
|
* -1 on days and months, as the start on 0
|
|
*
|
|
* @typedef {Number} CronIndexOffset
|
|
*/
|
|
|
|
/**
|
|
* Create a CronPattern instance from pattern string ('* * * * * *')
|
|
* @constructor
|
|
* @param {string} pattern - Input pattern
|
|
* @param {string} timezone - Input timezone, used for '?'-substitution
|
|
*/
|
|
function CronPattern (pattern, timezone) {
|
|
|
|
this.pattern = pattern;
|
|
this.timezone = timezone;
|
|
|
|
this.seconds = Array(60).fill(0); // 0-59
|
|
this.minutes = Array(60).fill(0); // 0-59
|
|
this.hours = Array(24).fill(0); // 0-23
|
|
this.days = Array(31).fill(0); // 0-30 in array, 1-31 in config
|
|
this.months = Array(12).fill(0); // 0-11 in array, 1-12 in config
|
|
this.daysOfWeek = Array(8).fill(0); // 0-7 Where 0 = Sunday and 7=Sunday;
|
|
|
|
this.lastDayOfMonth = false;
|
|
|
|
this.parse();
|
|
|
|
}
|
|
|
|
/**
|
|
* Parse current pattern, will throw on any type of failure
|
|
* @private
|
|
*/
|
|
CronPattern.prototype.parse = function () {
|
|
|
|
// Sanity check
|
|
if( !(typeof this.pattern === "string" || this.pattern.constructor === String) ) {
|
|
throw new TypeError("CronPattern: Pattern has to be of type string.");
|
|
}
|
|
|
|
// Split configuration on whitespace
|
|
const parts = this.pattern.trim().replace(/\s+/g, " ").split(" ");
|
|
|
|
// Validite number of configuration entries
|
|
if( parts.length < 5 || parts.length > 6 ) {
|
|
throw new TypeError("CronPattern: invalid configuration format ('" + this.pattern + "'), exacly five or six space separated parts required.");
|
|
}
|
|
|
|
// If seconds is omitted, insert 0 for seconds
|
|
if( parts.length === 5) {
|
|
parts.unshift("0");
|
|
}
|
|
|
|
// Convert 'L' to '*' and add lastDayOfMonth flag,
|
|
// and set days to 28,29,30,31 as those are the only days that can be the last day of month
|
|
if(parts[3].toUpperCase() == "L") {
|
|
parts[3] = "28,29,30,31";
|
|
this.lastDayOfMonth = true;
|
|
}
|
|
|
|
// Replace alpha representations
|
|
parts[4] = this.replaceAlphaMonths(parts[4]);
|
|
parts[5] = this.replaceAlphaDays(parts[5]);
|
|
|
|
// Implement '?' in the simplest possible way - replace ? with current value, before further processing
|
|
let initDate = new CronDate(new Date(),this.timezone).getDate(true);
|
|
|
|
parts[0] = parts[0].replace("?", initDate.getSeconds());
|
|
parts[1] = parts[1].replace("?", initDate.getMinutes());
|
|
parts[2] = parts[2].replace("?", initDate.getHours());
|
|
parts[3] = parts[3].replace("?", initDate.getDate());
|
|
parts[4] = parts[4].replace("?", initDate.getMonth()+1); // getMonth is zero indexed while pattern starts from 1
|
|
parts[5] = parts[5].replace("?", initDate.getDay());
|
|
|
|
// Check part content
|
|
this.throwAtIllegalCharacters(parts);
|
|
|
|
// Parse parts into arrays, validates as we go
|
|
this.partToArray("seconds", parts[0], 0);
|
|
this.partToArray("minutes", parts[1], 0);
|
|
this.partToArray("hours", parts[2], 0);
|
|
this.partToArray("days", parts[3], -1);
|
|
this.partToArray("months", parts[4], -1);
|
|
this.partToArray("daysOfWeek", parts[5], 0);
|
|
|
|
// 0 = Sunday, 7 = Sunday
|
|
if( this.daysOfWeek[7] ) {
|
|
this.daysOfWeek[0] = 1;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
* @private
|
|
*
|
|
* @param {CronPatternPart} type - Seconds/minutes etc
|
|
* @param {string} conf - Current pattern part - *, 0-1 etc
|
|
* @param {CronIndexOffset} valueIndexOffset
|
|
* @param {boolean} [recursed] - Is this a recursed call
|
|
*/
|
|
CronPattern.prototype.partToArray = function (type, conf, valueIndexOffset, recursed) {
|
|
|
|
const arr = this[type];
|
|
|
|
// First off, handle wildcard
|
|
if( conf === "*" ) {
|
|
for( let i = 0; i < arr.length; i++ ) {
|
|
arr[i] = 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle separated entries (,) by recursion
|
|
const split = conf.split(",");
|
|
if( split.length > 1 ) {
|
|
for( let i = 0; i < split.length; i++ ) {
|
|
this.partToArray(type, split[i], valueIndexOffset, true);
|
|
}
|
|
|
|
// Handle range with stepping (x-y/z)
|
|
} else if( conf.indexOf("-") !== -1 && conf.indexOf("/") !== -1 ) {
|
|
if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
|
|
|
|
this.handleRangeWithStepping(conf, type, valueIndexOffset);
|
|
|
|
// Handle range
|
|
} else if( conf.indexOf("-") !== -1 ) {
|
|
if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
|
|
|
|
this.handleRange(conf, type, valueIndexOffset);
|
|
|
|
// Handle stepping
|
|
} else if( conf.indexOf("/") !== -1 ) {
|
|
if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
|
|
|
|
this.handleStepping(conf, type, valueIndexOffset);
|
|
|
|
} else {
|
|
this.handleNumber(conf, type, valueIndexOffset);
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* After converting JAN-DEC, SUN-SAT only 0-9 * , / - are allowed, throw if anything else pops up
|
|
* @private
|
|
*
|
|
* @param {string[]} parts - Each part split as strings
|
|
*/
|
|
CronPattern.prototype.throwAtIllegalCharacters = function (parts) {
|
|
const reValidCron = /[^/*0-9,-]+/;
|
|
for(let i = 0; i < parts.length; i++) {
|
|
if( reValidCron.test(parts[i]) ) {
|
|
throw new TypeError("CronPattern: configuration entry " + i + " (" + parts[i] + ") contains illegal characters.");
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Nothing but a number left, handle that
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a number, as a string
|
|
* @param {string} type - One of "seconds", "minutes" etc
|
|
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
*/
|
|
CronPattern.prototype.handleNumber = function (conf, type, valueIndexOffset) {
|
|
const i = (parseInt(conf, 10) + valueIndexOffset);
|
|
|
|
if( i < 0 || i >= this[type].length ) {
|
|
throw new TypeError("CronPattern: " + type + " value out of range: '" + conf + "'");
|
|
}
|
|
|
|
this[type][i] = 1;
|
|
};
|
|
|
|
/**
|
|
* Take care of ranges with stepping (e.g. 3-23/5)
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string like 3-23/5
|
|
* @param {string} type - One of "seconds", "minutes" etc
|
|
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
*/
|
|
CronPattern.prototype.handleRangeWithStepping = function (conf, type, valueIndexOffset) {
|
|
const matches = conf.match(/^(\d+)-(\d+)\/(\d+)$/);
|
|
|
|
if( matches === null ) throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '" + conf + "'");
|
|
|
|
let [, lower, upper, steps] = matches;
|
|
lower = parseInt(lower, 10) + valueIndexOffset;
|
|
upper = parseInt(upper, 10) + valueIndexOffset;
|
|
steps = parseInt(steps, 10);
|
|
|
|
if( isNaN(lower) ) throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");
|
|
if( isNaN(upper) ) throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
|
|
if( isNaN(steps) ) throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");
|
|
|
|
if( steps === 0 ) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
|
|
if( steps > this[type].length ) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part ("+this[type].length+")");
|
|
|
|
if( lower < 0 || upper >= this[type].length ) throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
|
|
if( lower > upper ) throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
|
|
|
|
for (let i = lower; i <= upper; i += steps) {
|
|
this[type][i] = 1;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Take care of ranges (e.g. 1-20)
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string like 1-20
|
|
* @param {string} type - One of "seconds", "minutes" etc
|
|
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
*/
|
|
CronPattern.prototype.handleRange = function (conf, type, valueIndexOffset) {
|
|
const split = conf.split("-");
|
|
|
|
if( split.length !== 2 ) {
|
|
throw new TypeError("CronPattern: Syntax error, illegal range: '" + conf + "'");
|
|
}
|
|
|
|
const lower = parseInt(split[0], 10) + valueIndexOffset,
|
|
upper = parseInt(split[1], 10) + valueIndexOffset;
|
|
|
|
if( isNaN(lower) ) {
|
|
throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");
|
|
} else if( isNaN(upper) ) {
|
|
throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
|
|
}
|
|
|
|
// Check that value is within range
|
|
if( lower < 0 || upper >= this[type].length ) {
|
|
throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
|
|
}
|
|
|
|
//
|
|
if( lower > upper ) {
|
|
throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
|
|
}
|
|
|
|
for( let i = lower; i <= upper; i++ ) {
|
|
this[type][i] = 1;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle stepping (e.g. * / 14)
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string like * /20 (without the space)
|
|
* @param {string} type - One of "seconds", "minutes" etc
|
|
*/
|
|
CronPattern.prototype.handleStepping = function (conf, type) {
|
|
|
|
const split = conf.split("/");
|
|
|
|
if( split.length !== 2 ) {
|
|
throw new TypeError("CronPattern: Syntax error, illegal stepping: '" + conf + "'");
|
|
}
|
|
|
|
let start = 0;
|
|
if( split[0] !== "*" ) {
|
|
start = parseInt(split[0], 10);
|
|
}
|
|
|
|
const steps = parseInt(split[1], 10);
|
|
|
|
if( isNaN(steps) ) throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");
|
|
if( steps === 0 ) throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
|
|
if( steps > this[type].length ) throw new TypeError("CronPattern: Syntax error, steps cannot be greater than maximum value of part ("+this[type].length+")");
|
|
|
|
for( let i = start; i < this[type].length; i+= steps ) {
|
|
this[type][i] = 1;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Replace day name with day numbers
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string that might contain sun,mon etc.
|
|
*
|
|
* @returns {string} - conf with 0 instead of sun etc.
|
|
*/
|
|
CronPattern.prototype.replaceAlphaDays = function (conf) {
|
|
return conf
|
|
.replace(/sun/gi, "0")
|
|
.replace(/mon/gi, "1")
|
|
.replace(/tue/gi, "2")
|
|
.replace(/wed/gi, "3")
|
|
.replace(/thu/gi, "4")
|
|
.replace(/fri/gi, "5")
|
|
.replace(/sat/gi, "6");
|
|
};
|
|
|
|
/**
|
|
* Replace month name with month numbers
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string that might contain jan,feb etc.
|
|
*
|
|
* @returns {string} - conf with 0 instead of sun etc.
|
|
*/
|
|
CronPattern.prototype.replaceAlphaMonths = function (conf) {
|
|
return conf
|
|
.replace(/jan/gi, "1")
|
|
.replace(/feb/gi, "2")
|
|
.replace(/mar/gi, "3")
|
|
.replace(/apr/gi, "4")
|
|
.replace(/may/gi, "5")
|
|
.replace(/jun/gi, "6")
|
|
.replace(/jul/gi, "7")
|
|
.replace(/aug/gi, "8")
|
|
.replace(/sep/gi, "9")
|
|
.replace(/oct/gi, "10")
|
|
.replace(/nov/gi, "11")
|
|
.replace(/dec/gi, "12");
|
|
};
|
|
|
|
/* ------------------------------------------------------------------------------------
|
|
|
|
Croner - MIT License - Hexagon <github.com/Hexagon>
|
|
|
|
Pure JavaScript Isomorphic cron parser and scheduler without dependencies.
|
|
|
|
------------------------------------------------------------------------------------
|
|
|
|
License:
|
|
|
|
Copyright (c) 2015-2021 Hexagon <github.com/Hexagon>
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
|
|
------------------------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* @typedef {Object} CronOptions - Cron scheduler options
|
|
* @property {boolean} [paused] - Job is paused
|
|
* @property {boolean} [kill] - Job is about to be killed or killed
|
|
* @property {boolean} [catch] - Continue exection even if a unhandled error is thrown by triggered function
|
|
* @property {number} [maxRuns] - Maximum nuber of executions
|
|
* @property {string | Date} [startAt] - When to start running
|
|
* @property {string | Date} [stopAt] - When to stop running
|
|
* @property {string} [timezone] - Time zone in Europe/Stockholm format
|
|
* @property {?} [context] - Used to pass any object to scheduled function
|
|
*/
|
|
|
|
/**
|
|
* Many JS engines stores the delay as a 32-bit signed integer internally.
|
|
* This causes an integer overflow when using delays larger than 2147483647,
|
|
* resulting in the timeout being executed immediately.
|
|
*
|
|
* All JS engines implements an immediate execution of delays larger that a 32-bit
|
|
* int to keep the behaviour concistent.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
const maxDelay = Math.pow(2, 32 - 1) - 1;
|
|
|
|
/**
|
|
* Cron entrypoint
|
|
*
|
|
* @constructor
|
|
* @param {string|Date} pattern - Input pattern, input date, or input ISO 8601 time string
|
|
* @param {CronOptions|Function} [options] - Options
|
|
* @param {Function} [func] - Function to be run each iteration of pattern
|
|
* @returns {Cron}
|
|
*/
|
|
function Cron (pattern, options, func) {
|
|
|
|
// Optional "new" keyword
|
|
if( !(this instanceof Cron) ) {
|
|
return new Cron(pattern, options, func);
|
|
}
|
|
|
|
// Make options optional
|
|
if( typeof options === "function" ) {
|
|
func = options;
|
|
options = void 0;
|
|
}
|
|
|
|
/** @type {CronOptions} */
|
|
this.options = this.processOptions(options);
|
|
|
|
// Check if we got a date, or a pattern supplied as first argument
|
|
if (pattern && (pattern instanceof Date)) {
|
|
this.once = new CronDate(pattern, this.options.timezone);
|
|
} else if (pattern && (typeof pattern === "string") && pattern.indexOf(":") > 0) {
|
|
/** @type {CronDate} */
|
|
this.once = new CronDate(pattern, this.options.timezone);
|
|
} else {
|
|
/** @type {CronPattern} */
|
|
this.pattern = new CronPattern(pattern, this.options.timezone);
|
|
}
|
|
|
|
/**
|
|
* Allow shorthand scheduling
|
|
*/
|
|
if( func !== void 0 ) {
|
|
this.fn = func;
|
|
this.schedule();
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
/**
|
|
* Internal function that validates options, and sets defaults
|
|
* @private
|
|
*
|
|
* @param {CronOptions} options
|
|
* @returns {CronOptions}
|
|
*/
|
|
Cron.prototype.processOptions = function (options) {
|
|
|
|
// If no options are passed, create empty object
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
|
|
// Keep options, or set defaults
|
|
options.paused = (options.paused === void 0) ? false : options.paused;
|
|
options.maxRuns = (options.maxRuns === void 0) ? Infinity : options.maxRuns;
|
|
options.catch = (options.catch === void 0) ? false : options.catch;
|
|
options.kill = false;
|
|
|
|
// startAt is set, validate it
|
|
if( options.startAt ) {
|
|
options.startAt = new CronDate(options.startAt, options.timezone);
|
|
}
|
|
if( options.stopAt ) {
|
|
options.stopAt = new CronDate(options.stopAt, options.timezone);
|
|
}
|
|
|
|
return options;
|
|
};
|
|
|
|
/**
|
|
* Find next runtime, based on supplied date. Strips milliseconds.
|
|
*
|
|
* @param {Date|string} [prev] - Date to start from
|
|
* @returns {Date | null} - Next run time
|
|
*/
|
|
Cron.prototype.next = function (prev) {
|
|
prev = new CronDate(prev, this.options.timezone);
|
|
const next = this._next(prev);
|
|
return next ? next.getDate() : null;
|
|
};
|
|
|
|
/**
|
|
* Find next n runs, based on supplied date. Strips milliseconds.
|
|
*
|
|
* @param {number} n - Number of runs to enumerate
|
|
* @param {Date|string} [previous] - Date to start from
|
|
* @returns {Date[]} - Next n run times
|
|
*/
|
|
Cron.prototype.enumerate = function (n, previous) {
|
|
let enumeration = [];
|
|
|
|
while(n-- && (previous = this.next(previous))) {
|
|
enumeration.push(previous);
|
|
}
|
|
|
|
return enumeration;
|
|
};
|
|
|
|
/**
|
|
* Is running?
|
|
* @public
|
|
*
|
|
* @returns {boolean} - Running or not
|
|
*/
|
|
Cron.prototype.running = function () {
|
|
const msLeft = this.msToNext(this.previousrun);
|
|
const running = !this.options.paused && this.fn !== void 0;
|
|
return msLeft !== null && running;
|
|
};
|
|
|
|
/**
|
|
* Return previous run time
|
|
* @public
|
|
*
|
|
* @returns {Date | null} - Previous run time
|
|
*/
|
|
Cron.prototype.previous = function () {
|
|
return this.previousrun ? this.previousrun.getDate() : null;
|
|
};
|
|
|
|
/**
|
|
* Internal version of next. Cron needs millseconds internally, hence _next.
|
|
* @private
|
|
*
|
|
* @param {CronDate} prev - Input pattern
|
|
* @returns {CronDate | null} - Next run time
|
|
*/
|
|
Cron.prototype._next = function (prev) {
|
|
|
|
// Previous run should never be before startAt
|
|
if( this.options.startAt && prev && prev.getTime(true) < this.options.startAt.getTime(true) ) {
|
|
prev = this.options.startAt;
|
|
}
|
|
|
|
// Calculate next run according to pattern or one-off timestamp
|
|
const nextRun = this.once || new CronDate(prev, this.options.timezone).increment(this.pattern);
|
|
|
|
if (this.once && this.once.getTime(true) <= prev.getTime(true)) {
|
|
return null;
|
|
|
|
} else if ((nextRun === null) ||
|
|
(this.options.maxRuns <= 0) ||
|
|
(this.options.kill) ||
|
|
(this.options.stopAt && nextRun.getTime(true) >= this.options.stopAt.getTime(true) )) {
|
|
return null;
|
|
|
|
} else {
|
|
// All seem good, return next run
|
|
return nextRun;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Returns number of milliseconds to next run
|
|
* @public
|
|
*
|
|
* @param {Date} [prev] - Starting date, defaults to now
|
|
* @returns {number | null}
|
|
*/
|
|
Cron.prototype.msToNext = function (prev) {
|
|
prev = new CronDate(prev, this.options.timezone);
|
|
const next = this._next(prev);
|
|
if( next ) {
|
|
return (next.getTime(true) - prev.getTime(true));
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Stop execution
|
|
* @public
|
|
*/
|
|
Cron.prototype.stop = function () {
|
|
this.options.kill = true;
|
|
// Stop any awaiting call
|
|
if( this.currentTimeout ) {
|
|
clearTimeout( this.currentTimeout );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Pause executionR
|
|
* @public
|
|
*
|
|
* @returns {boolean} - Wether pause was successful
|
|
*/
|
|
Cron.prototype.pause = function () {
|
|
return (this.options.paused = true) && !this.options.kill;
|
|
};
|
|
|
|
/**
|
|
* Pause execution
|
|
* @public
|
|
*
|
|
* @returns {boolean} - Wether resume was successful
|
|
*/
|
|
Cron.prototype.resume = function () {
|
|
return !(this.options.paused = false) && !this.options.kill;
|
|
};
|
|
|
|
/**
|
|
* Schedule a new job
|
|
* @public
|
|
*
|
|
* @param {Function} func - Function to be run each iteration of pattern
|
|
* @returns {Cron}
|
|
*/
|
|
Cron.prototype.schedule = function (func) {
|
|
|
|
// If a function is already scheduled, bail out
|
|
if (func && this.fn) {
|
|
throw new Error("Cron: It is not allowed to schedule two functions using the same Croner instance.");
|
|
|
|
// Update function if passed
|
|
} else if (func) {
|
|
this.fn = func;
|
|
}
|
|
|
|
// Get ms to next run, bail out early if waitMs is null (no next run)
|
|
let waitMs = this.msToNext(this.previousrun);
|
|
if ( waitMs === null ) return this;
|
|
|
|
// setTimeout cant handle more than Math.pow(2, 32 - 1) - 1 ms
|
|
if( waitMs > maxDelay ) {
|
|
waitMs = maxDelay;
|
|
}
|
|
|
|
// Ok, go!
|
|
this.currentTimeout = setTimeout(() => {
|
|
|
|
if( waitMs !== maxDelay && !this.options.paused ) {
|
|
|
|
this.options.maxRuns--;
|
|
|
|
// Always catch errors, but only re-throw if options.catch is not set
|
|
if (this.options.catch) {
|
|
try {
|
|
this.fn(this, this.options.context);
|
|
} catch (_e) {
|
|
// Ignore
|
|
}
|
|
} else {
|
|
this.fn(this, this.options.context);
|
|
}
|
|
|
|
this.previousrun = new CronDate(void 0, this.options.timezone);
|
|
|
|
}
|
|
|
|
// Recurse
|
|
this.schedule();
|
|
|
|
}, waitMs );
|
|
|
|
return this;
|
|
|
|
};
|
|
|
|
return Cron;
|
|
|
|
}));
|