Invocation.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. 'use strict';
  2. const lt = require('long-timeout')
  3. const CronDate = require('cron-parser/lib/date')
  4. const sorted = require('sorted-array-functions')
  5. const invocations = [];
  6. let currentInvocation = null;
  7. /* DoesntRecur rule */
  8. const DoesntRecur = new RecurrenceRule();
  9. DoesntRecur.recurs = false;
  10. /* Invocation object */
  11. function Invocation(job, fireDate, recurrenceRule, endDate) {
  12. this.job = job;
  13. this.fireDate = fireDate;
  14. this.endDate = endDate;
  15. this.recurrenceRule = recurrenceRule || DoesntRecur;
  16. this.timerID = null;
  17. }
  18. function sorter(a, b) {
  19. return (a.fireDate.getTime() - b.fireDate.getTime());
  20. }
  21. /* Range object */
  22. function Range(start, end, step) {
  23. this.start = start || 0;
  24. this.end = end || 60;
  25. this.step = step || 1;
  26. }
  27. Range.prototype.contains = function(val) {
  28. if (this.step === null || this.step === 1) {
  29. return (val >= this.start && val <= this.end);
  30. } else {
  31. for (let i = this.start; i < this.end; i += this.step) {
  32. if (i === val) {
  33. return true;
  34. }
  35. }
  36. return false;
  37. }
  38. };
  39. /* RecurrenceRule object */
  40. /*
  41. Interpreting each property:
  42. null - any value is valid
  43. number - fixed value
  44. Range - value must fall in range
  45. array - value must validate against any item in list
  46. NOTE: Cron months are 1-based, but RecurrenceRule months are 0-based.
  47. */
  48. function RecurrenceRule(year, month, date, dayOfWeek, hour, minute, second) {
  49. this.recurs = true;
  50. this.year = (year == null) ? null : year;
  51. this.month = (month == null) ? null : month;
  52. this.date = (date == null) ? null : date;
  53. this.dayOfWeek = (dayOfWeek == null) ? null : dayOfWeek;
  54. this.hour = (hour == null) ? null : hour;
  55. this.minute = (minute == null) ? null : minute;
  56. this.second = (second == null) ? 0 : second;
  57. }
  58. RecurrenceRule.prototype.isValid = function() {
  59. function isValidType(num) {
  60. if (Array.isArray(num) || (num instanceof Array)) {
  61. return num.every(function(e) {
  62. return isValidType(e);
  63. });
  64. }
  65. return !(Number.isNaN(Number(num)) && !(num instanceof Range));
  66. }
  67. if (this.month !== null && (this.month < 0 || this.month > 11 || !isValidType(this.month))) {
  68. return false;
  69. }
  70. if (this.dayOfWeek !== null && (this.dayOfWeek < 0 || this.dayOfWeek > 6 || !isValidType(this.dayOfWeek))) {
  71. return false;
  72. }
  73. if (this.hour !== null && (this.hour < 0 || this.hour > 23 || !isValidType(this.hour))) {
  74. return false;
  75. }
  76. if (this.minute !== null && (this.minute < 0 || this.minute > 59 || !isValidType(this.minute))) {
  77. return false;
  78. }
  79. if (this.second !== null && (this.second < 0 || this.second > 59 || !isValidType(this.second))) {
  80. return false;
  81. }
  82. if (this.date !== null) {
  83. if(!isValidType(this.date)) {
  84. return false;
  85. }
  86. switch (this.month) {
  87. case 3:
  88. case 5:
  89. case 8:
  90. case 10:
  91. if (this.date < 1 || this. date > 30) {
  92. return false;
  93. }
  94. break;
  95. case 1:
  96. if (this.date < 1 || this. date > 29) {
  97. return false;
  98. }
  99. break;
  100. default:
  101. if (this.date < 1 || this. date > 31) {
  102. return false;
  103. }
  104. }
  105. }
  106. return true;
  107. };
  108. RecurrenceRule.prototype.nextInvocationDate = function(base) {
  109. const next = this._nextInvocationDate(base);
  110. return next ? next.toDate() : null;
  111. };
  112. RecurrenceRule.prototype._nextInvocationDate = function(base) {
  113. base = ((base instanceof CronDate) || (base instanceof Date)) ? base : (new Date());
  114. if (!this.recurs) {
  115. return null;
  116. }
  117. if(!this.isValid()) {
  118. return null;
  119. }
  120. const now = new CronDate(Date.now(), this.tz);
  121. let fullYear = now.getFullYear();
  122. if ((this.year !== null) &&
  123. (typeof this.year == 'number') &&
  124. (this.year < fullYear)) {
  125. return null;
  126. }
  127. let next = new CronDate(base.getTime(), this.tz);
  128. next.addSecond();
  129. while (true) {
  130. if (this.year !== null) {
  131. fullYear = next.getFullYear();
  132. if ((typeof this.year == 'number') && (this.year < fullYear)) {
  133. next = null;
  134. break;
  135. }
  136. if (!recurMatch(fullYear, this.year)) {
  137. next.addYear();
  138. next.setMonth(0);
  139. next.setDate(1);
  140. next.setHours(0);
  141. next.setMinutes(0);
  142. next.setSeconds(0);
  143. continue;
  144. }
  145. }
  146. if (this.month != null && !recurMatch(next.getMonth(), this.month)) {
  147. next.addMonth();
  148. continue;
  149. }
  150. if (this.date != null && !recurMatch(next.getDate(), this.date)) {
  151. next.addDay();
  152. continue;
  153. }
  154. if (this.dayOfWeek != null && !recurMatch(next.getDay(), this.dayOfWeek)) {
  155. next.addDay();
  156. continue;
  157. }
  158. if (this.hour != null && !recurMatch(next.getHours(), this.hour)) {
  159. next.addHour();
  160. continue;
  161. }
  162. if (this.minute != null && !recurMatch(next.getMinutes(), this.minute)) {
  163. next.addMinute();
  164. continue;
  165. }
  166. if (this.second != null && !recurMatch(next.getSeconds(), this.second)) {
  167. next.addSecond();
  168. continue;
  169. }
  170. break;
  171. }
  172. return next;
  173. };
  174. function recurMatch(val, matcher) {
  175. if (matcher == null) {
  176. return true;
  177. }
  178. if (typeof matcher === 'number') {
  179. return (val === matcher);
  180. } else if(typeof matcher === 'string') {
  181. return (val === Number(matcher));
  182. } else if (matcher instanceof Range) {
  183. return matcher.contains(val);
  184. } else if (Array.isArray(matcher) || (matcher instanceof Array)) {
  185. for (let i = 0; i < matcher.length; i++) {
  186. if (recurMatch(val, matcher[i])) {
  187. return true;
  188. }
  189. }
  190. }
  191. return false;
  192. }
  193. /* Date-based scheduler */
  194. function runOnDate(date, job) {
  195. const now = Date.now();
  196. const then = date.getTime();
  197. return lt.setTimeout(function() {
  198. if (then > Date.now())
  199. runOnDate(date, job);
  200. else
  201. job();
  202. }, (then < now ? 0 : then - now));
  203. }
  204. function scheduleInvocation(invocation) {
  205. sorted.add(invocations, invocation, sorter);
  206. prepareNextInvocation();
  207. const date = invocation.fireDate instanceof CronDate ? invocation.fireDate.toDate() : invocation.fireDate;
  208. invocation.job.emit('scheduled', date);
  209. }
  210. function prepareNextInvocation() {
  211. if (invocations.length > 0 && currentInvocation !== invocations[0]) {
  212. if (currentInvocation !== null) {
  213. lt.clearTimeout(currentInvocation.timerID);
  214. currentInvocation.timerID = null;
  215. currentInvocation = null;
  216. }
  217. currentInvocation = invocations[0];
  218. const job = currentInvocation.job;
  219. const cinv = currentInvocation;
  220. currentInvocation.timerID = runOnDate(currentInvocation.fireDate, function() {
  221. currentInvocationFinished();
  222. if (job.callback) {
  223. job.callback();
  224. }
  225. if (cinv.recurrenceRule.recurs || cinv.recurrenceRule._endDate === null) {
  226. const inv = scheduleNextRecurrence(cinv.recurrenceRule, cinv.job, cinv.fireDate, cinv.endDate);
  227. if (inv !== null) {
  228. inv.job.trackInvocation(inv);
  229. }
  230. }
  231. job.stopTrackingInvocation(cinv);
  232. try {
  233. const result = job.invoke(cinv.fireDate instanceof CronDate ? cinv.fireDate.toDate() : cinv.fireDate);
  234. job.emit('run');
  235. job.running += 1;
  236. if (result instanceof Promise) {
  237. result.then(function (value) {
  238. job.emit('success', value);
  239. job.running -= 1;
  240. }).catch(function (err) {
  241. job.emit('error', err);
  242. job.running -= 1;
  243. });
  244. } else {
  245. job.emit('success', result);
  246. job.running -= 1;
  247. }
  248. } catch (err) {
  249. job.emit('error', err);
  250. job.running -= 1;
  251. }
  252. if (job.isOneTimeJob) {
  253. job.deleteFromSchedule();
  254. }
  255. });
  256. }
  257. }
  258. function currentInvocationFinished() {
  259. invocations.shift();
  260. currentInvocation = null;
  261. prepareNextInvocation();
  262. }
  263. function cancelInvocation(invocation) {
  264. const idx = invocations.indexOf(invocation);
  265. if (idx > -1) {
  266. invocations.splice(idx, 1);
  267. if (invocation.timerID !== null) {
  268. lt.clearTimeout(invocation.timerID);
  269. }
  270. if (currentInvocation === invocation) {
  271. currentInvocation = null;
  272. }
  273. invocation.job.emit('canceled', invocation.fireDate);
  274. prepareNextInvocation();
  275. }
  276. }
  277. /* Recurrence scheduler */
  278. function scheduleNextRecurrence(rule, job, prevDate, endDate) {
  279. prevDate = (prevDate instanceof CronDate) ? prevDate : new CronDate();
  280. const date = (rule instanceof RecurrenceRule) ? rule._nextInvocationDate(prevDate) : rule.next();
  281. if (date === null) {
  282. return null;
  283. }
  284. if ((endDate instanceof CronDate) && date.getTime() > endDate.getTime()) {
  285. return null;
  286. }
  287. const inv = new Invocation(job, date, rule, endDate);
  288. scheduleInvocation(inv);
  289. return inv;
  290. }
  291. module.exports = {
  292. Range,
  293. RecurrenceRule,
  294. Invocation,
  295. cancelInvocation,
  296. scheduleInvocation,
  297. scheduleNextRecurrence,
  298. sorter,
  299. _invocations: invocations
  300. }