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

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