schedule.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. 'use strict';
  2. /*
  3. node-schedule
  4. A cron-like and not-cron-like job scheduler for Node.
  5. */
  6. var events = require('events'),
  7. util = require('util'),
  8. cronParser = require('cron-parser'),
  9. CronDate = require('cron-parser/lib/date'),
  10. lt = require('long-timeout');
  11. /* Job object */
  12. var anonJobCounter = 0;
  13. function isValidDate(date) {
  14. // Taken from http://stackoverflow.com/a/12372720/1562178
  15. // If getTime() returns NaN it'll return false anyway
  16. return date.getTime() === date.getTime();
  17. }
  18. function Job(name, job, callback) {
  19. // setup a private pendingInvocations variable
  20. var pendingInvocations = [];
  21. //setup a private number of invocations variable
  22. var triggeredJobs = 0;
  23. // Set scope vars
  24. var jobName = name && typeof name === 'string' ? name : '<Anonymous Job ' + (++anonJobCounter) + '>';
  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. // Check for generator
  35. if (typeof this.job === 'function' &&
  36. this.job.prototype &&
  37. this.job.prototype.next) {
  38. this.job = function() {
  39. return this.next().value;
  40. }.bind(this.job.call(this));
  41. }
  42. // define properties
  43. Object.defineProperty(this, 'name', {
  44. value: jobName,
  45. writable: false,
  46. enumerable: true
  47. });
  48. // method that require private access
  49. this.trackInvocation = function(invocation) {
  50. // add to our invocation list
  51. pendingInvocations.push(invocation);
  52. // and sort
  53. pendingInvocations.sort(sorter);
  54. return true;
  55. };
  56. this.stopTrackingInvocation = function(invocation) {
  57. var invIdx = pendingInvocations.indexOf(invocation);
  58. if (invIdx > -1) {
  59. 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.cancel = function(reschedule) {
  71. reschedule = (typeof reschedule == 'boolean') ? reschedule : false;
  72. var inv, newInv;
  73. var newInvs = [];
  74. for (var j = 0; j < pendingInvocations.length; j++) {
  75. inv = pendingInvocations[j];
  76. cancelInvocation(inv);
  77. if (reschedule && inv.recurrenceRule.recurs) {
  78. newInv = scheduleNextRecurrence(inv.recurrenceRule, this, inv.fireDate, inv.endDate);
  79. if (newInv !== null) {
  80. newInvs.push(newInv);
  81. }
  82. }
  83. }
  84. pendingInvocations = [];
  85. for (var k = 0; k < newInvs.length; k++) {
  86. this.trackInvocation(newInvs[k]);
  87. }
  88. // remove from scheduledJobs if reschedule === false
  89. if (!reschedule) {
  90. if (this.name) {
  91. delete scheduledJobs[this.name];
  92. }
  93. }
  94. return true;
  95. };
  96. this.cancelNext = function(reschedule) {
  97. reschedule = (typeof reschedule == 'boolean') ? reschedule : true;
  98. if (!pendingInvocations.length) {
  99. return false;
  100. }
  101. var newInv;
  102. var nextInv = pendingInvocations.shift();
  103. cancelInvocation(nextInv);
  104. if (reschedule && nextInv.recurrenceRule.recurs) {
  105. newInv = scheduleNextRecurrence(nextInv.recurrenceRule, this, nextInv.fireDate, nextInv.endDate);
  106. if (newInv !== null) {
  107. this.trackInvocation(newInv);
  108. }
  109. }
  110. return true;
  111. };
  112. this.reschedule = function(spec) {
  113. var inv;
  114. var cInvs = pendingInvocations.slice();
  115. for (var j = 0; j < cInvs.length; j++) {
  116. inv = cInvs[j];
  117. cancelInvocation(inv);
  118. }
  119. pendingInvocations = [];
  120. if (this.schedule(spec)) {
  121. this.setTriggeredJobs(0);
  122. return true;
  123. } else {
  124. pendingInvocations = cInvs;
  125. return false;
  126. }
  127. };
  128. this.nextInvocation = function() {
  129. if (!pendingInvocations.length) {
  130. return null;
  131. }
  132. return pendingInvocations[0].fireDate;
  133. };
  134. this.pendingInvocations = function() {
  135. return pendingInvocations;
  136. };
  137. }
  138. util.inherits(Job, events.EventEmitter);
  139. Job.prototype.invoke = function() {
  140. if (typeof this.job == 'function') {
  141. this.setTriggeredJobs(this.triggeredJobs() + 1);
  142. this.job();
  143. } else {
  144. this.job.execute();
  145. }
  146. };
  147. Job.prototype.runOnDate = function(date) {
  148. return this.schedule(date);
  149. };
  150. Job.prototype.schedule = function(spec) {
  151. var self = this;
  152. var success = false;
  153. var inv;
  154. var start;
  155. var end;
  156. if (typeof spec === 'object' && spec.rule) {
  157. start = spec.start || null;
  158. end = spec.end || null;
  159. spec = spec.rule;
  160. if (start != null) {
  161. if (!(start instanceof Date)) {
  162. start = new Date(start);
  163. }
  164. if (!isValidDate(start) || start.getTime() < Date.now()) {
  165. start = null;
  166. }
  167. }
  168. if (end != null && !(end instanceof Date) && !isValidDate(end = new Date(end))) {
  169. end = null;
  170. }
  171. }
  172. try {
  173. var res = cronParser.parseExpression(spec, { currentDate: start });
  174. inv = scheduleNextRecurrence(res, self, start, end);
  175. if (inv !== null) {
  176. success = self.trackInvocation(inv);
  177. }
  178. } catch (err) {
  179. var type = typeof spec;
  180. if ((type === 'string') || (type === 'number')) {
  181. spec = new Date(spec);
  182. }
  183. if ((spec instanceof Date) && (isValidDate(spec))) {
  184. if (spec.getTime() >= Date.now()) {
  185. inv = new Invocation(self, spec);
  186. scheduleInvocation(inv);
  187. success = self.trackInvocation(inv);
  188. }
  189. } else if (type === 'object') {
  190. if (!(spec instanceof RecurrenceRule)) {
  191. var r = new RecurrenceRule();
  192. if ('year' in spec) {
  193. r.year = spec.year;
  194. }
  195. if ('month' in spec) {
  196. r.month = spec.month;
  197. }
  198. if ('date' in spec) {
  199. r.date = spec.date;
  200. }
  201. if ('dayOfWeek' in spec) {
  202. r.dayOfWeek = spec.dayOfWeek;
  203. }
  204. if ('hour' in spec) {
  205. r.hour = spec.hour;
  206. }
  207. if ('minute' in spec) {
  208. r.minute = spec.minute;
  209. }
  210. if ('second' in spec) {
  211. r.second = spec.second;
  212. }
  213. spec = r;
  214. }
  215. inv = scheduleNextRecurrence(spec, self, start, end);
  216. if (inv !== null) {
  217. success = self.trackInvocation(inv);
  218. }
  219. }
  220. }
  221. scheduledJobs[this.name] = this;
  222. return success;
  223. };
  224. /* API
  225. invoke()
  226. runOnDate(date)
  227. schedule(date || recurrenceRule || cronstring)
  228. cancel(reschedule = false)
  229. cancelNext(reschedule = true)
  230. Property constraints
  231. name: readonly
  232. job: readwrite
  233. */
  234. /* DoesntRecur rule */
  235. var DoesntRecur = new RecurrenceRule();
  236. DoesntRecur.recurs = false;
  237. /* Invocation object */
  238. function Invocation(job, fireDate, recurrenceRule, endDate) {
  239. this.job = job;
  240. this.fireDate = fireDate;
  241. this.endDate = endDate;
  242. this.recurrenceRule = recurrenceRule || DoesntRecur;
  243. this.timerID = null;
  244. }
  245. function sorter(a, b) {
  246. return (a.fireDate.getTime() - b.fireDate.getTime());
  247. }
  248. /* Range object */
  249. function Range(start, end, step) {
  250. this.start = start || 0;
  251. this.end = end || 60;
  252. this.step = step || 1;
  253. }
  254. Range.prototype.contains = function(val) {
  255. if (this.step === null || this.step === 1) {
  256. return (val >= this.start && val <= this.end);
  257. } else {
  258. for (var i = this.start; i < this.end; i += this.step) {
  259. if (i === val) {
  260. return true;
  261. }
  262. }
  263. return false;
  264. }
  265. };
  266. /* RecurrenceRule object */
  267. /*
  268. Interpreting each property:
  269. null - any value is valid
  270. number - fixed value
  271. Range - value must fall in range
  272. array - value must validate against any item in list
  273. NOTE: Cron months are 1-based, but RecurrenceRule months are 0-based.
  274. */
  275. function RecurrenceRule(year, month, date, dayOfWeek, hour, minute, second) {
  276. this.recurs = true;
  277. this.year = (year == null) ? null : year;
  278. this.month = (month == null) ? null : month;
  279. this.date = (date == null) ? null : date;
  280. this.dayOfWeek = (dayOfWeek == null) ? null : dayOfWeek;
  281. this.hour = (hour == null) ? null : hour;
  282. this.minute = (minute == null) ? null : minute;
  283. this.second = (second == null) ? 0 : second;
  284. }
  285. RecurrenceRule.prototype.nextInvocationDate = function(base) {
  286. base = (base instanceof Date) ? base : (new Date());
  287. if (!this.recurs) {
  288. return null;
  289. }
  290. var now = new Date();
  291. var fullYear = now.getFullYear();
  292. if ((this.year !== null) &&
  293. (typeof this.year == 'number') &&
  294. (this.year < fullYear)) {
  295. return null;
  296. }
  297. var next = new CronDate(base.getTime());
  298. next.addSecond();
  299. while (true) {
  300. if (this.year !== null) {
  301. fullYear = next.getFullYear();
  302. if ((typeof this.year == 'number') && (this.year < fullYear)) {
  303. next = null;
  304. break;
  305. }
  306. if (!recurMatch(fullYear, this.year)) {
  307. next.addYear();
  308. next.setMonth(0);
  309. next.setDate(1);
  310. next.setHours(0);
  311. next.setMinutes(0);
  312. next.setSeconds(0);
  313. continue;
  314. }
  315. }
  316. if (this.month != null && !recurMatch(next.getMonth(), this.month)) {
  317. next.addMonth();
  318. continue;
  319. }
  320. if (this.date != null && !recurMatch(next.getDate(), this.date)) {
  321. next.addDay();
  322. continue;
  323. }
  324. if (this.dayOfWeek != null && !recurMatch(next.getDay(), this.dayOfWeek)) {
  325. next.addDay();
  326. continue;
  327. }
  328. if (this.hour != null && !recurMatch(next.getHours(), this.hour)) {
  329. next.addHour();
  330. continue;
  331. }
  332. if (this.minute != null && !recurMatch(next.getMinutes(), this.minute)) {
  333. next.addMinute();
  334. continue;
  335. }
  336. if (this.second != null && !recurMatch(next.getSeconds(), this.second)) {
  337. next.addSecond();
  338. continue;
  339. }
  340. break;
  341. }
  342. return next;
  343. };
  344. function recurMatch(val, matcher) {
  345. if (matcher == null) {
  346. return true;
  347. }
  348. if (typeof matcher === 'number' || typeof matcher === 'string') {
  349. return (val === matcher);
  350. } else if (matcher instanceof Range) {
  351. return matcher.contains(val);
  352. } else if (Array.isArray(matcher) || (matcher instanceof Array)) {
  353. for (var i = 0; i < matcher.length; i++) {
  354. if (recurMatch(val, matcher[i])) {
  355. return true;
  356. }
  357. }
  358. }
  359. return false;
  360. }
  361. /* Date-based scheduler */
  362. function runOnDate(date, job) {
  363. var now = (new Date()).getTime();
  364. var then = date.getTime();
  365. if (then < now) {
  366. setImmediate(job);
  367. return null;
  368. }
  369. return lt.setTimeout(job, (then - now));
  370. }
  371. var invocations = [];
  372. var currentInvocation = null;
  373. function scheduleInvocation(invocation) {
  374. invocations.push(invocation);
  375. invocations.sort(sorter);
  376. prepareNextInvocation();
  377. invocation.job.emit('scheduled', invocation.fireDate);
  378. }
  379. function prepareNextInvocation() {
  380. if (invocations.length > 0 && currentInvocation !== invocations[0]) {
  381. if (currentInvocation !== null) {
  382. lt.clearTimeout(currentInvocation.timerID);
  383. currentInvocation.timerID = null;
  384. currentInvocation = null;
  385. }
  386. currentInvocation = invocations[0];
  387. var job = currentInvocation.job;
  388. var cinv = currentInvocation;
  389. currentInvocation.timerID = runOnDate(currentInvocation.fireDate, function() {
  390. currentInvocationFinished();
  391. if (job.callback) {
  392. job.callback();
  393. }
  394. if (cinv.recurrenceRule.recurs || cinv.recurrenceRule._endDate === null) {
  395. var inv = scheduleNextRecurrence(cinv.recurrenceRule, cinv.job, cinv.fireDate, cinv.endDate);
  396. if (inv !== null) {
  397. inv.job.trackInvocation(inv);
  398. }
  399. }
  400. job.stopTrackingInvocation(cinv);
  401. job.invoke();
  402. job.emit('run');
  403. });
  404. }
  405. }
  406. function currentInvocationFinished() {
  407. invocations.shift();
  408. currentInvocation = null;
  409. prepareNextInvocation();
  410. }
  411. function cancelInvocation(invocation) {
  412. var idx = invocations.indexOf(invocation);
  413. if (idx > -1) {
  414. invocations.splice(idx, 1);
  415. if (invocation.timerID !== null) {
  416. lt.clearTimeout(invocation.timerID);
  417. }
  418. if (currentInvocation === invocation) {
  419. currentInvocation = null;
  420. }
  421. invocation.job.emit('canceled', invocation.fireDate);
  422. prepareNextInvocation();
  423. }
  424. }
  425. /* Recurrence scheduler */
  426. function scheduleNextRecurrence(rule, job, prevDate, endDate) {
  427. prevDate = (prevDate instanceof Date) ? prevDate : (new Date());
  428. var date = (rule instanceof RecurrenceRule) ? rule.nextInvocationDate(prevDate) : rule.next();
  429. if (date === null) {
  430. return null;
  431. }
  432. if ((endDate instanceof Date) && date.getTime() > endDate.getTime()) {
  433. return null;
  434. }
  435. var inv = new Invocation(job, date, rule, endDate);
  436. scheduleInvocation(inv);
  437. return inv;
  438. }
  439. /* Convenience methods */
  440. var scheduledJobs = {};
  441. function scheduleJob() {
  442. if (arguments.length < 2) {
  443. return null;
  444. }
  445. var name = (arguments.length >= 3 && typeof arguments[0] === 'string') ? arguments[0] : null;
  446. var spec = name ? arguments[1] : arguments[0];
  447. var method = name ? arguments[2] : arguments[1];
  448. var callback = name ? arguments[3] : arguments[2];
  449. var job = new Job(name, method, callback);
  450. if (job.schedule(spec)) {
  451. return job;
  452. }
  453. return null;
  454. }
  455. function rescheduleJob(job, spec) {
  456. if (job instanceof Job) {
  457. if (job.reschedule(spec)) {
  458. return job;
  459. }
  460. } else if (typeof job == 'string' || job instanceof String) {
  461. if (job in scheduledJobs && scheduledJobs.hasOwnProperty(job)) {
  462. if (scheduledJobs[job].reschedule(spec)) {
  463. return scheduledJobs[job];
  464. }
  465. }
  466. }
  467. return null;
  468. }
  469. function cancelJob(job) {
  470. var success = false;
  471. if (job instanceof Job) {
  472. success = job.cancel();
  473. } else if (typeof job == 'string' || job instanceof String) {
  474. if (job in scheduledJobs && scheduledJobs.hasOwnProperty(job)) {
  475. success = scheduledJobs[job].cancel();
  476. }
  477. }
  478. return success;
  479. }
  480. /* Public API */
  481. module.exports.Job = Job;
  482. module.exports.Range = Range;
  483. module.exports.RecurrenceRule = RecurrenceRule;
  484. module.exports.Invocation = Invocation;
  485. module.exports.scheduleJob = scheduleJob;
  486. module.exports.rescheduleJob = rescheduleJob;
  487. module.exports.scheduledJobs = scheduledJobs;
  488. module.exports.cancelJob = cancelJob;