Job.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. 'use strict';
  2. const events = require('events')
  3. const cronParser = require('cron-parser')
  4. const CronDate = require('cron-parser/lib/date')
  5. const sorted = require('sorted-array-functions')
  6. const { scheduleNextRecurrence, scheduleInvocation, cancelInvocation, RecurrenceRule, sorter, Invocation } = require('./Invocation')
  7. const { isValidDate } = require('./utils/dateUtils')
  8. const scheduledJobs = {};
  9. let anonJobCounter = 0;
  10. function resolveAnonJobName() {
  11. const now = new Date()
  12. if (anonJobCounter === Number.MAX_SAFE_INTEGER) {
  13. anonJobCounter = 0
  14. }
  15. anonJobCounter++
  16. return `<Anonymous Job ${anonJobCounter} ${now.toISOString()}>`
  17. }
  18. function Job(name, job, callback) {
  19. // setup a private pendingInvocations variable
  20. this.pendingInvocations = [];
  21. //setup a private number of invocations variable
  22. let triggeredJobs = 0;
  23. // Set scope vars
  24. const jobName = name && typeof name === 'string' ? name : resolveAnonJobName();
  25. this.job = name && typeof name === 'function' ? name : job;
  26. // Make sure callback is actually a callback
  27. if (this.job === name) {
  28. // Name wasn't provided and maybe a callback is there
  29. this.callback = typeof job === 'function' ? job : false;
  30. } else {
  31. // Name was provided, and maybe a callback is there
  32. this.callback = typeof callback === 'function' ? callback : false;
  33. }
  34. // task count
  35. this.running = 0;
  36. // Check for generator
  37. if (typeof this.job === 'function' &&
  38. this.job.prototype &&
  39. this.job.prototype.next) {
  40. this.job = function() {
  41. return this.next().value;
  42. }.bind(this.job.call(this));
  43. }
  44. // define properties
  45. Object.defineProperty(this, 'name', {
  46. value: jobName,
  47. writable: false,
  48. enumerable: true
  49. });
  50. // method that require private access
  51. this.trackInvocation = function(invocation) {
  52. // add to our invocation list
  53. sorted.add(this.pendingInvocations, invocation, sorter);
  54. return true;
  55. };
  56. this.stopTrackingInvocation = function(invocation) {
  57. const invIdx = this.pendingInvocations.indexOf(invocation);
  58. if (invIdx > -1) {
  59. this.pendingInvocations.splice(invIdx, 1);
  60. return true;
  61. }
  62. return false;
  63. };
  64. this.triggeredJobs = function() {
  65. return triggeredJobs;
  66. };
  67. this.setTriggeredJobs = function(triggeredJob) {
  68. triggeredJobs = triggeredJob;
  69. };
  70. this.deleteFromSchedule = function() {
  71. deleteScheduledJob(this.name)
  72. };
  73. this.cancel = function(reschedule) {
  74. reschedule = (typeof reschedule == 'boolean') ? reschedule : false;
  75. let inv, newInv;
  76. const newInvs = [];
  77. for (let j = 0; j < this.pendingInvocations.length; j++) {
  78. inv = this.pendingInvocations[j];
  79. cancelInvocation(inv);
  80. if (reschedule && (inv.recurrenceRule.recurs || inv.recurrenceRule.next)) {
  81. newInv = scheduleNextRecurrence(inv.recurrenceRule, this, inv.fireDate, inv.endDate);
  82. if (newInv !== null) {
  83. newInvs.push(newInv);
  84. }
  85. }
  86. }
  87. this.pendingInvocations = [];
  88. for (let k = 0; k < newInvs.length; k++) {
  89. this.trackInvocation(newInvs[k]);
  90. }
  91. // remove from scheduledJobs if reschedule === false
  92. if (!reschedule) {
  93. this.deleteFromSchedule()
  94. }
  95. return true;
  96. };
  97. this.cancelNext = function(reschedule) {
  98. reschedule = (typeof reschedule == 'boolean') ? reschedule : true;
  99. if (!this.pendingInvocations.length) {
  100. return false;
  101. }
  102. let newInv;
  103. const nextInv = this.pendingInvocations.shift();
  104. cancelInvocation(nextInv);
  105. if (reschedule && (nextInv.recurrenceRule.recurs || nextInv.recurrenceRule.next)) {
  106. newInv = scheduleNextRecurrence(nextInv.recurrenceRule, this, nextInv.fireDate, nextInv.endDate);
  107. if (newInv !== null) {
  108. this.trackInvocation(newInv);
  109. }
  110. }
  111. return true;
  112. };
  113. this.reschedule = function(spec) {
  114. let inv;
  115. const invocationsToCancel = this.pendingInvocations.slice();
  116. for (let j = 0; j < invocationsToCancel.length; j++) {
  117. inv = invocationsToCancel[j];
  118. cancelInvocation(inv);
  119. }
  120. this.pendingInvocations = [];
  121. if (this.schedule(spec)) {
  122. this.setTriggeredJobs(0);
  123. return true;
  124. } else {
  125. this.pendingInvocations = invocationsToCancel;
  126. return false;
  127. }
  128. };
  129. this.nextInvocation = function() {
  130. if (!this.pendingInvocations.length) {
  131. return null;
  132. }
  133. return this.pendingInvocations[0].fireDate;
  134. };
  135. }
  136. Object.setPrototypeOf(Job.prototype, events.EventEmitter.prototype);
  137. Job.prototype.invoke = function(fireDate) {
  138. this.setTriggeredJobs(this.triggeredJobs() + 1);
  139. return this.job(fireDate);
  140. };
  141. Job.prototype.runOnDate = function(date) {
  142. return this.schedule(date);
  143. };
  144. Job.prototype.schedule = function(spec) {
  145. const self = this;
  146. let success = false;
  147. let inv;
  148. let start;
  149. let end;
  150. let tz;
  151. // save passed-in value before 'spec' is replaced
  152. if (typeof spec === 'object' && 'tz' in spec) {
  153. tz = spec.tz;
  154. }
  155. if (typeof spec === 'object' && spec.rule) {
  156. start = spec.start || undefined;
  157. end = spec.end || undefined;
  158. spec = spec.rule;
  159. if (start) {
  160. if (!(start instanceof Date)) {
  161. start = new Date(start);
  162. }
  163. start = new CronDate(start, tz);
  164. if (!isValidDate(start) || start.getTime() < Date.now()) {
  165. start = undefined;
  166. }
  167. }
  168. if (end && !(end instanceof Date) && !isValidDate(end = new Date(end))) {
  169. end = undefined;
  170. }
  171. if (end) {
  172. end = new CronDate(end, tz);
  173. }
  174. }
  175. try {
  176. const res = cronParser.parseExpression(spec, {currentDate: start, tz: tz});
  177. inv = scheduleNextRecurrence(res, self, start, end);
  178. if (inv !== null) {
  179. success = self.trackInvocation(inv);
  180. }
  181. } catch (err) {
  182. const type = typeof spec;
  183. if ((type === 'string') || (type === 'number')) {
  184. spec = new Date(spec);
  185. }
  186. if ((spec instanceof Date) && (isValidDate(spec))) {
  187. spec = new CronDate(spec);
  188. self.isOneTimeJob = true;
  189. if (spec.getTime() >= Date.now()) {
  190. inv = new Invocation(self, spec);
  191. scheduleInvocation(inv);
  192. success = self.trackInvocation(inv);
  193. }
  194. } else if (type === 'object') {
  195. self.isOneTimeJob = false;
  196. if (!(spec instanceof RecurrenceRule)) {
  197. const r = new RecurrenceRule();
  198. if ('year' in spec) {
  199. r.year = spec.year;
  200. }
  201. if ('month' in spec) {
  202. r.month = spec.month;
  203. }
  204. if ('date' in spec) {
  205. r.date = spec.date;
  206. }
  207. if ('dayOfWeek' in spec) {
  208. r.dayOfWeek = spec.dayOfWeek;
  209. }
  210. if ('hour' in spec) {
  211. r.hour = spec.hour;
  212. }
  213. if ('minute' in spec) {
  214. r.minute = spec.minute;
  215. }
  216. if ('second' in spec) {
  217. r.second = spec.second;
  218. }
  219. spec = r;
  220. }
  221. spec.tz = tz;
  222. inv = scheduleNextRecurrence(spec, self, start, end);
  223. if (inv !== null) {
  224. success = self.trackInvocation(inv);
  225. }
  226. }
  227. }
  228. scheduledJobs[this.name] = this;
  229. return success;
  230. };
  231. function deleteScheduledJob(name) {
  232. if (name) {
  233. delete scheduledJobs[name];
  234. }
  235. }
  236. module.exports = {
  237. Job,
  238. deleteScheduledJob,
  239. scheduledJobs
  240. }