cron.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. var CronDate = Date;
  2. try {
  3. CronDate = require("time").Date;
  4. } catch(e) {
  5. //no time module...leave CronDate alone. :)
  6. }
  7. function CronTime(source, zone) {
  8. this.source = source;
  9. this.zone = zone;
  10. this.second = {};
  11. this.minute = {};
  12. this.hour = {};
  13. this.dayOfWeek = {};
  14. this.dayOfMonth = {};
  15. this.month = {};
  16. if ((this.source instanceof Date) || (this.source instanceof CronDate)) {
  17. this.source = new CronDate(this.source);
  18. this.realDate = true;
  19. } else {
  20. this._parse();
  21. }
  22. };
  23. CronTime.map = ['second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'];
  24. CronTime.constraints = [ [0, 59], [0, 59], [0, 23], [1, 31], [0, 11], [0, 6] ];
  25. CronTime.parseDefaults = [ '0', '*', '*', '*', '*', '*' ];
  26. CronTime.aliases = {
  27. jan:0, feb:1, mar:2, apr:3, may:4, jun:5, jul:6, aug:7, sep:8, oct:9, nov:10, dec:11,
  28. sun:0, mon:1, tue:2, wed:3, thu:4, fri:5, sat:6
  29. };
  30. CronTime.prototype = {
  31. /**
  32. * calculates the next send time
  33. */
  34. sendAt: function() {
  35. var date = (this.source instanceof CronDate) ? this.source : new CronDate();
  36. if (this.zone && date.setTimezone)
  37. date.setTimezone(this.zone);
  38. //add 1 second so next time isn't now (can cause timeout to be 0)
  39. if (!(this.realDate)) date.setSeconds(date.getSeconds() + 1);
  40. if (this.realDate) {
  41. return date;
  42. }
  43. return this._getNextDateFrom(date);
  44. },
  45. /**
  46. * Get the number of seconds in the future at which to fire our callbacks.
  47. */
  48. getTimeout: function() {
  49. return Math.max(-1, this.sendAt().getTime() - CronDate.now());
  50. },
  51. /**
  52. * writes out a cron string
  53. */
  54. toString: function() {
  55. return this.toJSON().join(' ');
  56. },
  57. /**
  58. * Json representation of the parsed cron syntax.
  59. */
  60. toJSON: function() {
  61. return [
  62. this._wcOrAll('second'),
  63. this._wcOrAll('minute'),
  64. this._wcOrAll('hour'),
  65. this._wcOrAll('dayOfMonth'),
  66. this._wcOrAll('month'),
  67. this._wcOrAll('dayOfWeek')
  68. ];
  69. },
  70. /**
  71. * get next date that matches parsed cron time
  72. */
  73. _getNextDateFrom: function(start) {
  74. var date = new CronDate(start);
  75. if (this.zone && date.setTimezone)
  76. date.setTimezone(start.getTimezone());
  77. //sanity check
  78. var i = 1000;
  79. while(--i) {
  80. var diff = date - start;
  81. if (!(date.getMonth() in this.month)) {
  82. date.setMonth(date.getMonth()+1);
  83. date.setDate(1);
  84. date.setHours(0);
  85. date.setMinutes(0);
  86. continue;
  87. }
  88. if (!(date.getDate() in this.dayOfMonth)) {
  89. date.setDate(date.getDate()+1);
  90. date.setHours(0);
  91. date.setMinutes(0);
  92. continue;
  93. }
  94. if (!(date.getDay() in this.dayOfWeek)) {
  95. date.setDate(date.getDate()+1);
  96. date.setHours(0);
  97. date.setMinutes(0);
  98. continue;
  99. }
  100. if (!(date.getHours() in this.hour)) {
  101. date.setHours(date.getHours() == 23 && diff > 24*60*60*1000 ? 0 : date.getHours()+1);
  102. date.setMinutes(0);
  103. continue;
  104. }
  105. if (!(date.getMinutes() in this.minute)) {
  106. date.setMinutes(date.getMinutes() == 59 && diff > 60*60*1000 ? 0 : date.getMinutes()+1);
  107. date.setSeconds(0);
  108. continue;
  109. }
  110. if (!(date.getSeconds() in this.second)) {
  111. date.setSeconds(date.getSeconds() == 59 && diff > 60*1000 ? 0 : date.getSeconds()+1);
  112. continue;
  113. }
  114. break;
  115. }
  116. return date;
  117. },
  118. /**
  119. * wildcard, or all params in array (for to string)
  120. */
  121. _wcOrAll: function(type) {
  122. if(this._hasAll(type)) return '*';
  123. var all = [];
  124. for(var time in this[type]) {
  125. all.push(time);
  126. }
  127. return all.join(',');
  128. },
  129. /**
  130. */
  131. _hasAll: function(type) {
  132. var constrain = CronTime.constraints[CronTime.map.indexOf(type)];
  133. for(var i = constrain[0], n = constrain[1]; i < n; i++) {
  134. if(!(i in this[type])) return false;
  135. }
  136. return true;
  137. },
  138. /**
  139. * Parse the cron syntax.
  140. */
  141. _parse: function() {
  142. var aliases = CronTime.aliases,
  143. source = this.source.replace(/[a-z]{1,3}/ig, function(alias){
  144. alias = alias.toLowerCase();
  145. if (alias in aliases) {
  146. return aliases[alias];
  147. }
  148. throw new Error('Unknown alias: ' + alias);
  149. }),
  150. split = source.replace(/^\s\s*|\s\s*$/g, '').split(/\s+/),
  151. cur, i = 0, len = CronTime.map.length;
  152. for (; i < CronTime.map.length; i++) {
  153. // If the split source string doesn't contain all digits,
  154. // assume defaults for first n missing digits.
  155. // This adds support for 5-digit standard cron syntax
  156. cur = split[i - (len - split.length)] || CronTime.parseDefaults[i];
  157. this._parseField(cur, CronTime.map[i], CronTime.constraints[i]);
  158. }
  159. },
  160. /**
  161. * Parse a field from the cron syntax.
  162. */
  163. _parseField: function(field, type, constraints) {
  164. var rangePattern = /(\d+?)(?:-(\d+?))?(?:\/(\d+?))?(?:,|$)/g,
  165. typeObj = this[type],
  166. diff, pointer,
  167. low = constraints[0],
  168. high = constraints[1];
  169. // * is a shortcut to [lower-upper] range
  170. field = field.replace(/\*/g, low + '-' + high);
  171. if (field.match(rangePattern)) {
  172. field.replace(rangePattern, function($0, lower, upper, step) {
  173. step = parseInt(step) || 1;
  174. // Positive integer higher than constraints[0]
  175. lower = Math.max(low, ~~Math.abs(lower));
  176. // Positive integer lower than constraints[1]
  177. upper = upper ? Math.min(high, ~~Math.abs(upper)) : lower;
  178. // Count from the lower barrier to the upper
  179. pointer = lower;
  180. do {
  181. typeObj[pointer] = true
  182. pointer += step;
  183. } while(pointer <= upper);
  184. });
  185. } else {
  186. throw new Error('Field (' + field + ') cannot be parsed');
  187. }
  188. }
  189. };
  190. function CronJob(cronTime, onTick, onComplete, start, timeZone, context) {
  191. if (typeof cronTime != "string" && arguments.length == 1) {
  192. //crontime is an object...
  193. onTick = cronTime.onTick;
  194. onComplete = cronTime.onComplete;
  195. context = cronTime.context;
  196. start = cronTime.start;
  197. timeZone = cronTime.timeZone;
  198. cronTime = cronTime.cronTime;
  199. }
  200. if (timeZone && !(CronDate.prototype.setTimezone)) console.log('You specified a Timezone but have not included the `time` module. Timezone functionality is disabled. Please install the `time` module to use Timezones in your application.');
  201. this.context = (context || this);
  202. this._callbacks = [];
  203. this.onComplete = onComplete;
  204. this.cronTime = new CronTime(cronTime, timeZone);
  205. this.addCallback(onTick);
  206. if (start) this.start();
  207. return this;
  208. }
  209. CronJob.prototype = {
  210. /**
  211. * Add a method to fire onTick
  212. */
  213. addCallback: function(callback) {
  214. //only functions
  215. if(typeof callback == 'function') this._callbacks.push(callback);
  216. },
  217. /**
  218. * Fire all callbacks registered.
  219. */
  220. _callback: function() {
  221. for (var i = (this._callbacks.length - 1); i >= 0; i--) {
  222. //send this so the callback can call this.stop();
  223. this._callbacks[i].call(this.context, this.onComplete);
  224. }
  225. },
  226. /**
  227. * Manually set the time of a job
  228. */
  229. setTime: function(time) {
  230. if (!(time instanceof CronTime)) throw '\'time\' must be an instance of CronTime.';
  231. this.stop();
  232. this.cronTime = time;
  233. },
  234. /**
  235. * Start the cronjob.
  236. */
  237. start: function() {
  238. if(this.running) return;
  239. var MAXDELAY = 2147483647; // The maximum number of milliseconds setTimeout will wait.
  240. var self = this;
  241. var timeout = this.cronTime.getTimeout();
  242. var remaining = 0;
  243. if (this.cronTime.realDate) this.runOnce = true;
  244. // The callback wrapper checks if it needs to sleep another period or not
  245. // and does the real callback logic when it's time.
  246. function callbackWrapper() {
  247. // If there is sleep time remaining, calculate how long and go to sleep
  248. // again. This processing might make us miss the deadline by a few ms
  249. // times the number of sleep sessions. Given a MAXDELAY of almost a
  250. // month, this should be no issue.
  251. if (remaining) {
  252. if (remaining > MAXDELAY) {
  253. remaining -= MAXDELAY;
  254. timeout = MAXDELAY;
  255. } else {
  256. timeout = remaining;
  257. remaining = 0;
  258. }
  259. self._timeout = setTimeout(callbackWrapper, timeout);
  260. } else {
  261. // We have arrived at the correct point in time.
  262. self.running = false;
  263. //start before calling back so the callbacks have the ability to stop the cron job
  264. if (!(self.runOnce)) self.start();
  265. self._callback();
  266. }
  267. }
  268. if (timeout >= 0) {
  269. this.running = true;
  270. // Don't try to sleep more than MAXDELAY ms at a time.
  271. if (timeout > MAXDELAY) {
  272. remaining = timeout - MAXDELAY;
  273. timeout = MAXDELAY;
  274. }
  275. this._timeout = setTimeout(callbackWrapper, timeout);
  276. } else {
  277. this.stop();
  278. }
  279. },
  280. /**
  281. * Stop the cronjob.
  282. */
  283. stop: function()
  284. {
  285. clearTimeout(this._timeout);
  286. this.running = false;
  287. if (this.onComplete) this.onComplete();
  288. }
  289. };
  290. exports.job = function(cronTime, onTick, onComplete) {
  291. return new CronJob(cronTime, onTick, onComplete);
  292. }
  293. exports.time = function(cronTime, timeZone) {
  294. return new CronTime(cronTime, timeZone);
  295. }
  296. exports.sendAt = function(cronTime) {
  297. return exports.time(cronTime).sendAt();
  298. }
  299. exports.timeout = function(cronTime) {
  300. return exports.time(cronTime).getTimeout();
  301. }
  302. exports.CronJob = CronJob;
  303. exports.CronTime = CronTime;