123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742 |
- (function(root, factory) {
- if (typeof define === 'function' && define.amd) {
- define(['moment-timezone'], factory);
- } else if (typeof exports === 'object') {
- module.exports = factory(
- require('moment-timezone'),
- require('child_process')
- );
- } else {
- root.Cron = factory(root.moment);
- }
- })(this, function(moment, childProcess) {
- var exports = {};
- var timeUnits = [
- 'second',
- 'minute',
- 'hour',
- 'dayOfMonth',
- 'month',
- 'dayOfWeek'
- ];
- var spawn = childProcess && childProcess.spawn;
-
- function CronTime(source, zone, utcOffset) {
- this.source = source;
-
- if (zone) {
- if (moment.tz.names().indexOf(zone) === -1) {
- throw new Error('Invalid timezone.');
- }
-
- this.zone = zone;
- }
-
- if (typeof utcOffset !== 'undefined') this.utcOffset = utcOffset;
-
- var that = this;
- timeUnits.map(function(timeUnit) {
- that[timeUnit] = {};
- });
-
- if (this.source instanceof Date || this.source._isAMomentObject) {
- this.source = moment(this.source);
- this.realDate = true;
- } else {
- this._parse();
- this._verifyParse();
- }
- }
-
- CronTime.constraints = [[0, 59], [0, 59], [0, 23], [1, 31], [0, 11], [0, 6]];
- CronTime.monthConstraints = [
- 31,
- 29, // support leap year...not perfect
- 31,
- 30,
- 31,
- 30,
- 31,
- 31,
- 30,
- 31,
- 30,
- 31
- ];
- CronTime.parseDefaults = ['0', '*', '*', '*', '*', '*'];
- CronTime.aliases = {
- jan: 0,
- feb: 1,
- mar: 2,
- apr: 3,
- may: 4,
- jun: 5,
- jul: 6,
- aug: 7,
- sep: 8,
- oct: 9,
- nov: 10,
- dec: 11,
- sun: 0,
- mon: 1,
- tue: 2,
- wed: 3,
- thu: 4,
- fri: 5,
- sat: 6
- };
-
- CronTime.prototype = {
- _verifyParse: function() {
- var months = Object.keys(this.month);
- var ok = false;
-
- /* if a dayOfMonth is not found in all months, we only need to fix the last
- wrong month to prevent infinite loop */
- var lastWrongMonth = NaN;
- for (var i = 0; i < months.length; i++) {
- var m = months[i];
- var con = CronTime.monthConstraints[parseInt(m, 10)];
- var dsom = Object.keys(this.dayOfMonth);
-
- for (var j = 0; j < dsom.length; j++) {
- var dom = dsom[j];
- if (dom <= con) {
- ok = true;
- }
- }
-
- if (!ok) {
- // save the month in order to be fixed if all months fails (infinite loop)
- lastWrongMonth = m;
- console.warn("Month '" + m + "' is limited to '" + con + "' days.");
- }
- }
-
- // infinite loop detected (dayOfMonth is not found in all months)
- if (!ok) {
- var con = CronTime.monthConstraints[parseInt(lastWrongMonth, 10)];
- var dsom = Object.keys(this.dayOfMonth);
- for (var k = 0; k < dsom.length; k++) {
- var dom = dsom[k];
- if (dom > con) {
- delete this.dayOfMonth[dom];
- var fixedDay = Number(dom) % con;
- this.dayOfMonth[fixedDay] = true;
- }
- }
- }
- },
-
- /**
- * calculates the next send time
- */
- sendAt: function(i) {
- var date = this.realDate ? this.source : moment();
- // Set the timezone if given (http://momentjs.com/timezone/docs/#/using-timezones/parsing-in-zone/)
- if (this.zone) {
- date = date.tz(this.zone);
- }
-
- if (typeof this.utcOffset !== 'undefined') {
- date = date.utcOffset(this.utcOffset);
- }
-
- if (this.realDate) {
- const diff = moment().diff(date, 's');
- if (diff > 0) {
- throw new Error('WARNING: Date in past. Will never be fired.');
- }
-
- return date;
- }
-
- // If the i argument is not given, return the next send time
- if (isNaN(i) || i < 0) {
- date = this._getNextDateFrom(date);
-
- return date;
- } else {
- // Else return the next i send times
- var dates = [];
- for (; i > 0; i--) {
- date = this._getNextDateFrom(date);
- dates.push(moment(date));
- }
-
- return dates;
- }
- },
-
- /**
- * Get the number of milliseconds in the future at which to fire our callbacks.
- */
- getTimeout: function() {
- return Math.max(-1, this.sendAt() - moment());
- },
-
- /**
- * writes out a cron string
- */
- toString: function() {
- return this.toJSON().join(' ');
- },
-
- /**
- * Json representation of the parsed cron syntax.
- */
- toJSON: function() {
- var self = this;
- return timeUnits.map(function(timeName) {
- return self._wcOrAll(timeName);
- });
- },
-
- /**
- * get next date that matches parsed cron time
- */
- _getNextDateFrom: function(start, zone) {
- var date;
- var firstDate = moment(start).valueOf();
- if (zone) {
- date = moment(start).tz(zone);
- } else {
- date = moment(start);
- }
- if (!this.realDate) {
- const milliseconds =
- (start.milliseconds && start.milliseconds()) ||
- (start.getMilliseconds && start.getMilliseconds()) ||
- 0;
- if (milliseconds > 0) {
- date.milliseconds(0);
- date.seconds(date.seconds() + 1);
- }
- }
-
- if (date.toString() === 'Invalid date') {
- throw new Error('ERROR: You specified an invalid date.');
- }
-
- // it shouldn't take more than 5 seconds to find the next execution time
- // being very generous with this. Throw error if it takes too long to find the next time to protect from
- // infinite loop.
- var timeout = Date.now() + 5000;
- // determine next date
- while (true) {
- var diff = date - start;
- var prevMonth = date.month();
- var prevDay = date.days();
- var prevMinute = date.minutes();
- var prevSeconds = date.seconds();
- var origDate = new Date(date);
-
- if (Date.now() > timeout) {
- throw new Error(
- `Something went wrong. cron reached maximum iterations.
- Please open an issue (https://github.com/kelektiv/node-cron/issues/new) and provide the following string
- Time Zone: ${zone || '""'} - Cron String: ${this} - UTC offset: ${date.format(
- 'Z'
- )} - current Date: ${moment().toString()}`
- );
- }
- if (
- !(date.month() in this.month) &&
- Object.keys(this.month).length !== 12
- ) {
- date.add(1, 'M');
- if (date.month() === prevMonth) {
- date.add(1, 'M');
- }
- date.date(1);
- date.hours(0);
- date.minutes(0);
- date.seconds(0);
- continue;
- }
-
- if (
- !(date.date() in this.dayOfMonth) &&
- Object.keys(this.dayOfMonth).length !== 31 &&
- !(
- date.day() in this.dayOfWeek &&
- Object.keys(this.dayOfWeek).length !== 7
- )
- ) {
- date.add(1, 'd');
- if (date.days() === prevDay) {
- date.add(1, 'd');
- }
- date.hours(0);
- date.minutes(0);
- date.seconds(0);
- continue;
- }
-
- if (
- !(date.day() in this.dayOfWeek) &&
- Object.keys(this.dayOfWeek).length !== 7 &&
- !(
- date.date() in this.dayOfMonth &&
- Object.keys(this.dayOfMonth).length !== 31
- )
- ) {
- date.add(1, 'd');
- if (date.days() === prevDay) {
- date.add(1, 'd');
- }
- date.hours(0);
- date.minutes(0);
- date.seconds(0);
- if (date <= origDate) {
- date = this._findDST(origDate);
- }
- continue;
- }
-
- if (
- !(date.hours() in this.hour) &&
- Object.keys(this.hour).length !== 24
- ) {
- origDate = moment(date);
- var curHour = date.hours();
- date.hours(
- date.hours() === 23 && diff > 86400000 ? 0 : date.hours() + 1
- );
- /*
- * Moment Date will not allow you to set the time to 2 AM if there is no 2 AM (on the day we change the clock)
- * We will therefore jump to 3AM if time stayed at 1AM
- */
- if (curHour === date.hours()) {
- date.hours(date.hours() + 2);
- }
- date.minutes(0);
- date.seconds(0);
- if (date <= origDate) {
- date = this._findDST(origDate);
- }
- continue;
- }
-
- if (
- !(date.minutes() in this.minute) &&
- Object.keys(this.minute).length !== 60
- ) {
- origDate = moment(date);
- date.minutes(
- date.minutes() === 59 && diff > 60 * 60 * 1000
- ? 0
- : date.minutes() + 1
- );
- date.seconds(0);
- if (date <= origDate) {
- date = this._findDST(origDate);
- }
- continue;
- }
-
- if (
- !(date.seconds() in this.second) &&
- Object.keys(this.second).length !== 60
- ) {
- origDate = moment(date);
- date.seconds(
- date.seconds() === 59 && diff > 60 * 1000 ? 0 : date.seconds() + 1
- );
- if (date <= origDate) {
- date = this._findDST(origDate);
- }
- continue;
- }
-
- if (date.valueOf() === firstDate) {
- date.seconds(date.seconds() + 1);
- continue;
- }
-
- break;
- }
-
- return date;
- },
-
- /**
- * get next date that is a valid DST date
- */
- _findDST: function(date) {
- var newDate = moment(date);
- while (newDate <= date) {
- // eslint seems to trigger here, it is wrong
- newDate.add(1, 's');
- }
-
- return newDate;
- },
-
- /**
- * wildcard, or all params in array (for to string)
- */
- _wcOrAll: function(type) {
- if (this._hasAll(type)) return '*';
-
- var all = [];
- for (var time in this[type]) {
- all.push(time);
- }
-
- return all.join(',');
- },
-
- _hasAll: function(type) {
- var constrain = CronTime.constraints[timeUnits.indexOf(type)];
-
- for (var i = constrain[0], n = constrain[1]; i < n; i++) {
- if (!(i in this[type])) return false;
- }
-
- return true;
- },
-
- _parse: function() {
- var aliases = CronTime.aliases;
- var source = this.source.replace(/[a-z]{1,3}/gi, function(alias) {
- alias = alias.toLowerCase();
-
- if (alias in aliases) {
- return aliases[alias];
- }
-
- throw new Error('Unknown alias: ' + alias);
- });
- var split = source.replace(/^\s\s*|\s\s*$/g, '').split(/\s+/);
- var cur;
- var i = 0;
- var len = timeUnits.length;
-
- // seconds are optional
- if (split.length < timeUnits.length - 1) {
- throw new Error('Too few fields');
- }
- if (split.length > timeUnits.length) {
- throw new Error('Too many fields');
- }
-
- for (; i < timeUnits.length; i++) {
- // If the split source string doesn't contain all digits,
- // assume defaults for first n missing digits.
- // This adds support for 5-digit standard cron syntax
- cur = split[i - (len - split.length)] || CronTime.parseDefaults[i];
- this._parseField(cur, timeUnits[i], CronTime.constraints[i]);
- }
- },
-
- _parseField: function(field, type, constraints) {
- var rangePattern = /^(\d+)(?:-(\d+))?(?:\/(\d+))?$/g;
- var typeObj = this[type];
- var pointer;
- var low = constraints[0];
- var high = constraints[1];
-
- var fields = field.split(',');
- fields.forEach(function(field) {
- var wildcardIndex = field.indexOf('*');
- if (wildcardIndex !== -1 && wildcardIndex !== 0) {
- throw new Error('Field (' + field + ') has an invalid wildcard expression');
- }
- });
-
- // * is a shortcut to [lower-upper] range
- field = field.replace(/\*/g, low + '-' + high);
-
- // commas separate information, so split based on those
- var allRanges = field.split(',');
-
- for (var i = 0; i < allRanges.length; i++) {
- if (allRanges[i].match(rangePattern)) {
- allRanges[i].replace(rangePattern, function($0, lower, upper, step) {
- lower = parseInt(lower, 10);
- upper = parseInt(upper, 10) || undefined;
-
- const wasStepDefined = !isNaN(parseInt(step, 10));
- if (step === '0') {
- throw new Error('Field (' + field + ') has a step of zero');
- }
- step = parseInt(step, 10) || 1;
-
- if (upper && lower > upper) {
- throw new Error('Field (' + field + ') has an invalid range');
- }
-
- const outOfRangeError =
- lower < low ||
- (upper && upper > high) ||
- (!upper && lower > high);
-
- if (outOfRangeError) {
- throw new Error('Field (' + field + ') value is out of range');
- }
-
- // Positive integer higher than constraints[0]
- lower = Math.min(Math.max(low, ~~Math.abs(lower)), high);
-
- // Positive integer lower than constraints[1]
- if (upper) {
- upper = Math.min(high, ~~Math.abs(upper));
- } else {
- // If step is provided, the default upper range is the highest value
- upper = wasStepDefined ? high : lower;
- }
-
- // Count from the lower barrier to the upper
- pointer = lower;
-
- do {
- typeObj[pointer] = true;
- pointer += step;
- } while (pointer <= upper);
- });
- } else {
- throw new Error('Field (' + field + ') cannot be parsed');
- }
- }
- }
- };
-
- function command2function(cmd) {
- var command;
- var args;
-
- switch (typeof cmd) {
- case 'string':
- args = cmd.split(' ');
- command = args.shift();
-
- cmd = spawn.bind(undefined, command, args);
- break;
-
- case 'object':
- command = cmd && cmd.command;
- if (command) {
- args = cmd.args;
- var options = cmd.options;
-
- cmd = spawn.bind(undefined, command, args, options);
- }
- break;
- }
-
- return cmd;
- }
-
- function CronJob(
- cronTime,
- onTick,
- onComplete,
- startNow,
- timeZone,
- context,
- runOnInit,
- utcOffset,
- unrefTimeout
- ) {
- var _cronTime = cronTime;
- var argCount = 0;
- for (var i = 0; i < arguments.length; i++) {
- if (arguments[i] !== undefined) {
- argCount++;
- }
- }
- if (typeof cronTime !== 'string' && argCount === 1) {
- // crontime is an object...
- onTick = cronTime.onTick;
- onComplete = cronTime.onComplete;
- context = cronTime.context;
- startNow = cronTime.start || cronTime.startNow || cronTime.startJob;
- timeZone = cronTime.timeZone;
- runOnInit = cronTime.runOnInit;
- _cronTime = cronTime.cronTime;
- utcOffset = cronTime.utcOffset;
- unrefTimeout = cronTime.unrefTimeout;
- }
-
- this.context = context || this;
- this._callbacks = [];
- this.onComplete = command2function(onComplete);
- this.cronTime = new CronTime(_cronTime, timeZone, utcOffset);
- this.unrefTimeout = unrefTimeout;
-
- addCallback.call(this, command2function(onTick));
-
- if (runOnInit) {
- this.lastExecution = new Date();
- fireOnTick.call(this);
- }
-
- if (startNow) {
- start.call(this);
- }
-
- return this;
- }
-
- var addCallback = function(callback) {
- if (typeof callback === 'function') this._callbacks.push(callback);
- };
- CronJob.prototype.addCallback = addCallback;
-
- CronJob.prototype.setTime = function(time) {
- if (!(time instanceof CronTime))
- throw new Error('time must be an instance of CronTime.');
- this.stop();
- this.cronTime = time;
- };
-
- CronJob.prototype.nextDate = function() {
- return this.cronTime.sendAt();
- };
-
- var fireOnTick = function() {
- for (var i = this._callbacks.length - 1; i >= 0; i--)
- this._callbacks[i].call(this.context, this.onComplete);
- };
- CronJob.prototype.fireOnTick = fireOnTick;
-
- CronJob.prototype.nextDates = function(i) {
- return this.cronTime.sendAt(i);
- };
-
- var start = function() {
- if (this.running) return;
-
- var MAXDELAY = 2147483647; // The maximum number of milliseconds setTimeout will wait.
- var self = this;
- var timeout = this.cronTime.getTimeout();
- var remaining = 0;
- var startTime;
-
- if (this.cronTime.realDate) this.runOnce = true;
-
- function _setTimeout(timeout) {
- startTime = Date.now();
- self._timeout = setTimeout(callbackWrapper, timeout);
- if (self.unrefTimeout && typeof self._timeout.unref === 'function') {
- self._timeout.unref();
- }
- }
-
- // The callback wrapper checks if it needs to sleep another period or not
- // and does the real callback logic when it's time.
-
- function callbackWrapper() {
- var diff = startTime + timeout - Date.now();
-
- if (diff > 0) {
- var newTimeout = self.cronTime.getTimeout();
-
- if (newTimeout > diff) {
- newTimeout = diff;
- }
-
- remaining += newTimeout;
- }
-
- // If there is sleep time remaining, calculate how long and go to sleep
- // again. This processing might make us miss the deadline by a few ms
- // times the number of sleep sessions. Given a MAXDELAY of almost a
- // month, this should be no issue.
-
- self.lastExecution = new Date();
- if (remaining) {
- if (remaining > MAXDELAY) {
- remaining -= MAXDELAY;
- timeout = MAXDELAY;
- } else {
- timeout = remaining;
- remaining = 0;
- }
-
- _setTimeout(timeout);
- } else {
- // We have arrived at the correct point in time.
-
- self.running = false;
-
- // start before calling back so the callbacks have the ability to stop the cron job
- if (!self.runOnce) self.start();
-
- self.fireOnTick();
- }
- }
-
- if (timeout >= 0) {
- this.running = true;
-
- // Don't try to sleep more than MAXDELAY ms at a time.
-
- if (timeout > MAXDELAY) {
- remaining = timeout - MAXDELAY;
- timeout = MAXDELAY;
- }
-
- _setTimeout(timeout);
- } else {
- this.stop();
- }
- };
-
- CronJob.prototype.start = start;
-
- CronJob.prototype.lastDate = function() {
- return this.lastExecution;
- };
-
- /**
- * Stop the cronjob.
- */
- CronJob.prototype.stop = function() {
- if (this._timeout) clearTimeout(this._timeout);
- this.running = false;
- if (typeof this.onComplete === 'function') this.onComplete();
- };
-
- exports.job = function(
- cronTime,
- onTick,
- onComplete,
- startNow,
- timeZone,
- context,
- runOnInit,
- utcOffset,
- unrefTimeout
- ) {
- return new CronJob(
- cronTime,
- onTick,
- onComplete,
- startNow,
- timeZone,
- context,
- runOnInit,
- utcOffset,
- unrefTimeout
- );
- };
-
- exports.time = function(cronTime, timeZone) {
- return new CronTime(cronTime, timeZone);
- };
-
- exports.sendAt = function(cronTime) {
- return exports.time(cronTime).sendAt();
- };
-
- exports.timeout = function(cronTime) {
- return exports.time(cronTime).getTimeout();
- };
-
- exports.CronJob = CronJob;
- exports.CronTime = CronTime;
-
- return exports;
- });
|