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