You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					288 lines
				
				7.1 KiB
			
		
		
			
		
	
	
					288 lines
				
				7.1 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								'use strict';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const events = require('events')
							 | 
						||
| 
								 | 
							
								const cronParser = require('cron-parser')
							 | 
						||
| 
								 | 
							
								const CronDate = require('cron-parser/lib/date')
							 | 
						||
| 
								 | 
							
								const sorted = require('sorted-array-functions')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const { scheduleNextRecurrence, scheduleInvocation, cancelInvocation, RecurrenceRule, sorter, Invocation } = require('./Invocation')
							 | 
						||
| 
								 | 
							
								const { isValidDate } = require('./utils/dateUtils')
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								const scheduledJobs = {};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								let anonJobCounter = 0;
							 | 
						||
| 
								 | 
							
								function resolveAnonJobName() {
							 | 
						||
| 
								 | 
							
								  const now = new Date()
							 | 
						||
| 
								 | 
							
								  if (anonJobCounter === Number.MAX_SAFE_INTEGER) {
							 | 
						||
| 
								 | 
							
								    anonJobCounter = 0
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								  anonJobCounter++
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  return `<Anonymous Job ${anonJobCounter} ${now.toISOString()}>`
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function Job(name, job, callback) {
							 | 
						||
| 
								 | 
							
								  // setup a private pendingInvocations variable
							 | 
						||
| 
								 | 
							
								  this.pendingInvocations = [];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  //setup a private number of invocations variable
							 | 
						||
| 
								 | 
							
								  let triggeredJobs = 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Set scope vars
							 | 
						||
| 
								 | 
							
								  const jobName = name && typeof name === 'string' ? name : resolveAnonJobName();
							 | 
						||
| 
								 | 
							
								  this.job = name && typeof name === 'function' ? name : job;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Make sure callback is actually a callback
							 | 
						||
| 
								 | 
							
								  if (this.job === name) {
							 | 
						||
| 
								 | 
							
								    // Name wasn't provided and maybe a callback is there
							 | 
						||
| 
								 | 
							
								    this.callback = typeof job === 'function' ? job : false;
							 | 
						||
| 
								 | 
							
								  } else {
							 | 
						||
| 
								 | 
							
								    // Name was provided, and maybe a callback is there
							 | 
						||
| 
								 | 
							
								    this.callback = typeof callback === 'function' ? callback : false;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // task count
							 | 
						||
| 
								 | 
							
								  this.running = 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // Check for generator
							 | 
						||
| 
								 | 
							
								  if (typeof this.job === 'function' &&
							 | 
						||
| 
								 | 
							
								    this.job.prototype &&
							 | 
						||
| 
								 | 
							
								    this.job.prototype.next) {
							 | 
						||
| 
								 | 
							
								    this.job = function() {
							 | 
						||
| 
								 | 
							
								      return this.next().value;
							 | 
						||
| 
								 | 
							
								    }.bind(this.job.call(this));
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // define properties
							 | 
						||
| 
								 | 
							
								  Object.defineProperty(this, 'name', {
							 | 
						||
| 
								 | 
							
								    value: jobName,
							 | 
						||
| 
								 | 
							
								    writable: false,
							 | 
						||
| 
								 | 
							
								    enumerable: true
							 | 
						||
| 
								 | 
							
								  });
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // method that require private access
							 | 
						||
| 
								 | 
							
								  this.trackInvocation = function(invocation) {
							 | 
						||
| 
								 | 
							
								    // add to our invocation list
							 | 
						||
| 
								 | 
							
								    sorted.add(this.pendingInvocations, invocation, sorter);
							 | 
						||
| 
								 | 
							
								    return true;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  this.stopTrackingInvocation = function(invocation) {
							 | 
						||
| 
								 | 
							
								    const invIdx = this.pendingInvocations.indexOf(invocation);
							 | 
						||
| 
								 | 
							
								    if (invIdx > -1) {
							 | 
						||
| 
								 | 
							
								      this.pendingInvocations.splice(invIdx, 1);
							 | 
						||
| 
								 | 
							
								      return true;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return false;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  this.triggeredJobs = function() {
							 | 
						||
| 
								 | 
							
								    return triggeredJobs;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  this.setTriggeredJobs = function(triggeredJob) {
							 | 
						||
| 
								 | 
							
								    triggeredJobs = triggeredJob;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  this.deleteFromSchedule = function() {
							 | 
						||
| 
								 | 
							
								    deleteScheduledJob(this.name)
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  this.cancel = function(reschedule) {
							 | 
						||
| 
								 | 
							
								    reschedule = (typeof reschedule == 'boolean') ? reschedule : false;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    let inv, newInv;
							 | 
						||
| 
								 | 
							
								    const newInvs = [];
							 | 
						||
| 
								 | 
							
								    for (let j = 0; j < this.pendingInvocations.length; j++) {
							 | 
						||
| 
								 | 
							
								      inv = this.pendingInvocations[j];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      cancelInvocation(inv);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      if (reschedule && (inv.recurrenceRule.recurs || inv.recurrenceRule.next)) {
							 | 
						||
| 
								 | 
							
								        newInv = scheduleNextRecurrence(inv.recurrenceRule, this, inv.fireDate, inv.endDate);
							 | 
						||
| 
								 | 
							
								        if (newInv !== null) {
							 | 
						||
| 
								 | 
							
								          newInvs.push(newInv);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.pendingInvocations = [];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for (let k = 0; k < newInvs.length; k++) {
							 | 
						||
| 
								 | 
							
								      this.trackInvocation(newInvs[k]);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    // remove from scheduledJobs if reschedule === false
							 | 
						||
| 
								 | 
							
								    if (!reschedule) {
							 | 
						||
| 
								 | 
							
								      this.deleteFromSchedule()
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return true;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  this.cancelNext = function(reschedule) {
							 | 
						||
| 
								 | 
							
								    reschedule = (typeof reschedule == 'boolean') ? reschedule : true;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (!this.pendingInvocations.length) {
							 | 
						||
| 
								 | 
							
								      return false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    let newInv;
							 | 
						||
| 
								 | 
							
								    const nextInv = this.pendingInvocations.shift();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    cancelInvocation(nextInv);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (reschedule && (nextInv.recurrenceRule.recurs || nextInv.recurrenceRule.next)) {
							 | 
						||
| 
								 | 
							
								      newInv = scheduleNextRecurrence(nextInv.recurrenceRule, this, nextInv.fireDate, nextInv.endDate);
							 | 
						||
| 
								 | 
							
								      if (newInv !== null) {
							 | 
						||
| 
								 | 
							
								        this.trackInvocation(newInv);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return true;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  this.reschedule = function(spec) {
							 | 
						||
| 
								 | 
							
								    let inv;
							 | 
						||
| 
								 | 
							
								    const invocationsToCancel = this.pendingInvocations.slice();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    for (let j = 0; j < invocationsToCancel.length; j++) {
							 | 
						||
| 
								 | 
							
								      inv = invocationsToCancel[j];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      cancelInvocation(inv);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    this.pendingInvocations = [];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (this.schedule(spec)) {
							 | 
						||
| 
								 | 
							
								      this.setTriggeredJobs(0);
							 | 
						||
| 
								 | 
							
								      return true;
							 | 
						||
| 
								 | 
							
								    } else {
							 | 
						||
| 
								 | 
							
								      this.pendingInvocations = invocationsToCancel;
							 | 
						||
| 
								 | 
							
								      return false;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  this.nextInvocation = function() {
							 | 
						||
| 
								 | 
							
								    if (!this.pendingInvocations.length) {
							 | 
						||
| 
								 | 
							
								      return null;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    return this.pendingInvocations[0].fireDate;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Object.setPrototypeOf(Job.prototype, events.EventEmitter.prototype);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Job.prototype.invoke = function(fireDate) {
							 | 
						||
| 
								 | 
							
								  this.setTriggeredJobs(this.triggeredJobs() + 1);
							 | 
						||
| 
								 | 
							
								  return this.job(fireDate);
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Job.prototype.runOnDate = function(date) {
							 | 
						||
| 
								 | 
							
								  return this.schedule(date);
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Job.prototype.schedule = function(spec) {
							 | 
						||
| 
								 | 
							
								  const self = this;
							 | 
						||
| 
								 | 
							
								  let success = false;
							 | 
						||
| 
								 | 
							
								  let inv;
							 | 
						||
| 
								 | 
							
								  let start;
							 | 
						||
| 
								 | 
							
								  let end;
							 | 
						||
| 
								 | 
							
								  let tz;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // save passed-in value before 'spec' is replaced
							 | 
						||
| 
								 | 
							
								  if (typeof spec === 'object' && 'tz' in spec) {
							 | 
						||
| 
								 | 
							
								    tz = spec.tz;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  if (typeof spec === 'object' && spec.rule) {
							 | 
						||
| 
								 | 
							
								    start = spec.start || undefined;
							 | 
						||
| 
								 | 
							
								    end = spec.end || undefined;
							 | 
						||
| 
								 | 
							
								    spec = spec.rule;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (start) {
							 | 
						||
| 
								 | 
							
								      if (!(start instanceof Date)) {
							 | 
						||
| 
								 | 
							
								        start = new Date(start);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      start = new CronDate(start, tz);
							 | 
						||
| 
								 | 
							
								      if (!isValidDate(start) || start.getTime() < Date.now()) {
							 | 
						||
| 
								 | 
							
								        start = undefined;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (end && !(end instanceof Date) && !isValidDate(end = new Date(end))) {
							 | 
						||
| 
								 | 
							
								      end = undefined;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if (end) {
							 | 
						||
| 
								 | 
							
								      end = new CronDate(end, tz);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  try {
							 | 
						||
| 
								 | 
							
								    const res = cronParser.parseExpression(spec, {currentDate: start, tz: tz});
							 | 
						||
| 
								 | 
							
								    inv = scheduleNextRecurrence(res, self, start, end);
							 | 
						||
| 
								 | 
							
								    if (inv !== null) {
							 | 
						||
| 
								 | 
							
								      success = self.trackInvocation(inv);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  } catch (err) {
							 | 
						||
| 
								 | 
							
								    const type = typeof spec;
							 | 
						||
| 
								 | 
							
								    if ((type === 'string') || (type === 'number')) {
							 | 
						||
| 
								 | 
							
								      spec = new Date(spec);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ((spec instanceof Date) && (isValidDate(spec))) {
							 | 
						||
| 
								 | 
							
								      spec = new CronDate(spec);
							 | 
						||
| 
								 | 
							
								      self.isOneTimeJob = true;
							 | 
						||
| 
								 | 
							
								      if (spec.getTime() >= Date.now()) {
							 | 
						||
| 
								 | 
							
								        inv = new Invocation(self, spec);
							 | 
						||
| 
								 | 
							
								        scheduleInvocation(inv);
							 | 
						||
| 
								 | 
							
								        success = self.trackInvocation(inv);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    } else if (type === 'object') {
							 | 
						||
| 
								 | 
							
								      self.isOneTimeJob = false;
							 | 
						||
| 
								 | 
							
								      if (!(spec instanceof RecurrenceRule)) {
							 | 
						||
| 
								 | 
							
								        const r = new RecurrenceRule();
							 | 
						||
| 
								 | 
							
								        if ('year' in spec) {
							 | 
						||
| 
								 | 
							
								          r.year = spec.year;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        if ('month' in spec) {
							 | 
						||
| 
								 | 
							
								          r.month = spec.month;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        if ('date' in spec) {
							 | 
						||
| 
								 | 
							
								          r.date = spec.date;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        if ('dayOfWeek' in spec) {
							 | 
						||
| 
								 | 
							
								          r.dayOfWeek = spec.dayOfWeek;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        if ('hour' in spec) {
							 | 
						||
| 
								 | 
							
								          r.hour = spec.hour;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        if ('minute' in spec) {
							 | 
						||
| 
								 | 
							
								          r.minute = spec.minute;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        if ('second' in spec) {
							 | 
						||
| 
								 | 
							
								          r.second = spec.second;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        spec = r;
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								      spec.tz = tz;
							 | 
						||
| 
								 | 
							
								      inv = scheduleNextRecurrence(spec, self, start, end);
							 | 
						||
| 
								 | 
							
								      if (inv !== null) {
							 | 
						||
| 
								 | 
							
								        success = self.trackInvocation(inv);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  scheduledJobs[this.name] = this;
							 | 
						||
| 
								 | 
							
								  return success;
							 | 
						||
| 
								 | 
							
								};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function deleteScheduledJob(name) {
							 | 
						||
| 
								 | 
							
								  if (name) {
							 | 
						||
| 
								 | 
							
								    delete scheduledJobs[name];
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								module.exports = {
							 | 
						||
| 
								 | 
							
								  Job,
							 | 
						||
| 
								 | 
							
								  deleteScheduledJob,
							 | 
						||
| 
								 | 
							
								  scheduledJobs
							 | 
						||
| 
								 | 
							
								}
							 |