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 | ||
|  | } |