(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; });