├── .gitignore ├── .travis.yml ├── HISTORY.md ├── package.json ├── test.js ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | cache: 5 | directories: 6 | - node_modules 7 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## [v1.1.0] 2 | > Aug 10, 2017 3 | 4 | - [#3] - Allow very long timeouts. ([@rgcjonas]) 5 | 6 | [v1.1.0]: https://github.com/rstacruz/cron-scheduler/compare/v1.0.0...v1.1.0 7 | 8 | ## [v1.0.0] 9 | > Jan 9, 2016 10 | 11 | - Initial release. 12 | 13 | [v1.0.0]: https://github.com/rstacruz/cron-scheduler/tree/v1.0.0 14 | [#3]: https://github.com/rstacruz/cron-scheduler/issues/3 15 | [@rgcjonas]: https://github.com/rgcjonas 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cron-scheduler", 3 | "description": "Runs jobs in periodic intervals", 4 | "version": "1.2.0", 5 | "author": "Rico Sta. Cruz ", 6 | "bugs": { 7 | "url": "https://github.com/rstacruz/cron-scheduler/issues" 8 | }, 9 | "dependencies": { 10 | "any-promise": "0.1.0", 11 | "cron-converter": "0.0.8", 12 | "long-timeout": "^0.1.1", 13 | "moment-timezone": "0.5.0", 14 | "pretty-ms": "2.1.0" 15 | }, 16 | "devDependencies": { 17 | "sinon": "1.17.2", 18 | "sinon-in-sandbox": "1.0.0", 19 | "tape": "4.4.0" 20 | }, 21 | "homepage": "https://github.com/rstacruz/cron-scheduler#readme", 22 | "keywords": [ 23 | "cron", 24 | "cronjob", 25 | "crontab", 26 | "schedule" 27 | ], 28 | "license": "MIT", 29 | "main": "index.js", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/rstacruz/cron-scheduler.git" 33 | }, 34 | "scripts": { 35 | "test": "node test.js" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var sandbox = require('sinon-in-sandbox') 3 | 4 | test('cron', function (t) { 5 | sandbox(function (sinon, clock) { 6 | var clock = sinon.useFakeTimers() 7 | t.ok(+new Date() === 0, 'sinon timers is working') 8 | 9 | var cron = rerequire('./index') 10 | 11 | var job = cron({ on: '0 1 * * *', timezone: 'GMT' }, function () { 12 | t.pass('called') 13 | job.stop() 14 | t.end() 15 | }) 16 | 17 | clock.tick(3600 * 1000) 18 | }) 19 | }) 20 | 21 | test('cron with long delay', function (t) { 22 | sandbox(function (sinon, clock) { 23 | var clock = sinon.useFakeTimers() 24 | t.ok(+new Date() === 0, 'sinon timers is working') 25 | 26 | // HACK sinon timers do not implement the 2147483647ms limit, but we need it to make this test genuine 27 | var oldSetTimeout = setTimeout 28 | setTimeout = function(func, timeout) { 29 | if (timeout < 1 || timeout > 2147483647) timeout = 1 30 | 31 | return oldSetTimeout(func, timeout) 32 | } 33 | 34 | var cron = rerequire('./index') 35 | 36 | // we architect it in a way to have about three months until the next execution 37 | var called = false 38 | var job = cron({ on: '0 1 31 3 *', timezone: 'GMT' }, function () { 39 | t.fail('called') 40 | called = true 41 | }) 42 | 43 | clock.tick(3600 * 1000) 44 | if (!called) t.pass('not called') 45 | job.stop() 46 | t.end() 47 | }) 48 | }) 49 | 50 | test('cron timezones', function (t) { 51 | sandbox(function (sinon) { 52 | var clock = sinon.useFakeTimers() 53 | var cron = rerequire('./index') 54 | 55 | // Asia/Manila is +0800 GMT, so this is 1AM GMT 56 | var job = cron({ on: '0 9 * * *', timezone: 'Asia/Manila' }, function () { 57 | t.pass('called') 58 | job.stop() 59 | t.end() 60 | }) 61 | 62 | clock.tick(3600 * 1000) 63 | }) 64 | }) 65 | 66 | /* 67 | * substitute for require() that will not cache its result, so it make be 68 | * re-required again in the future. we need this so that cron/moment will 69 | * pick up the sandboxed Date object. 70 | */ 71 | 72 | function rerequire (mod) { 73 | var keys = Object.keys(require.cache) 74 | var result = require(mod) 75 | Object.keys(require.cache).forEach(function (key) { 76 | if (keys.indexOf(key) === -1) delete require.cache[key] 77 | }) 78 | return result 79 | } 80 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var CronConverter = require('cron-converter') 2 | var moment = require('moment-timezone') 3 | var ms = require('pretty-ms') 4 | var Promise = require('any-promise') 5 | var lt = require('long-timeout') 6 | var debug = function () {} 7 | 8 | /* 9 | * Starts a cronjob. 10 | */ 11 | 12 | function cron (options, fn) { 13 | var crontime, timezone, name, started, timer 14 | init() 15 | return { stop: stop, run: run, next: next } 16 | 17 | /* 18 | * Constructor. 19 | */ 20 | 21 | function init () { 22 | if (!options || !options.on) { 23 | throw new Error('cron-scheduler: expected an options object with `on`') 24 | } 25 | 26 | if (typeof fn !== 'function') { 27 | throw new Error('cron-scheduler: expected function') 28 | } 29 | 30 | crontime = new CronConverter() 31 | crontime.fromString(options.on) 32 | timezone = options.timezone 33 | name = options.name || fn.name || options.on 34 | started = true 35 | schedule() 36 | } 37 | 38 | /* 39 | * Sets a timer to run the next iteration. 40 | */ 41 | 42 | function schedule () { 43 | var future = next() 44 | var delta = Math.max(future.diff(moment()), 1000) 45 | 46 | debug(name + ': next run in ' + ms(delta) + 47 | ' at ' + future.format('llll Z')) 48 | 49 | if (timer) lt.clearTimeout(timer) 50 | timer = lt.setTimeout(run, delta) 51 | } 52 | 53 | /* 54 | * Returns the next scheduled iteration as a Moment date. 55 | */ 56 | 57 | function next () { 58 | // get the time to check for. cron-converter needs an 59 | // extra minute so it doesn't schedule it in the past 60 | var now = moment() 61 | if (timezone) now = now.tz(timezone) 62 | now = now.add(1, 'minute') 63 | 64 | // get the next date and cast it to the timezone. 65 | // return it as a Moment object. 66 | var next = crontime.next(now) 67 | var date = timezone ? moment.tz(next, timezone) : moment(next) 68 | return date 69 | } 70 | 71 | /* 72 | * Runs an iteration. 73 | */ 74 | 75 | function run () { 76 | debug(name + ': starting') 77 | var start = new Date() 78 | Promise.resolve(fn()) 79 | .then(function () { 80 | debug(name + ': OK in ' + ms(elapsed())) 81 | if (started) schedule() 82 | }) 83 | .catch(function (err) { 84 | debug(name + ': FAILED in ' + ms(elapsed())) 85 | throw err 86 | }) 87 | 88 | function elapsed () { return +new Date() - start } 89 | } 90 | 91 | /* 92 | * ...in the name of love. 93 | */ 94 | 95 | function stop () { 96 | if (timer) { 97 | clearTimeout(timer) 98 | timer = undefined 99 | } 100 | 101 | started = false 102 | } 103 | } 104 | 105 | /* 106 | * Sets the debug function. 107 | */ 108 | 109 | cron.debug = function (fn) { 110 | debug = fn 111 | } 112 | 113 | module.exports = cron 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cron-scheduler 2 | 3 | > Runs jobs in periodic intervals 4 | 5 | cron-scheduler is a way to run functions at specific times of the day. It runs 6 | in Node.js as well as the browser. 7 | 8 | It requires a Promise implementation to work. If you're on Node.js v4 or 9 | later, you should be fine. Otherwise, you'll also need to install 10 | [bluebird](https://github.com/petkaantonov/bluebird) (or [rsvp], [when], or [q.js]). 11 | 12 | [rsvp]: https://www.npmjs.com/package/rsvp 13 | [q.js]: https://github.com/kriskowal/q 14 | [when]: https://github.com/cujojs/when 15 | 16 | [![Status](https://travis-ci.org/rstacruz/cron-scheduler.svg?branch=master)](https://travis-ci.org/rstacruz/cron-scheduler "See test builds") 17 | 18 | ## cron() 19 | > `cron(options, function)` 20 | 21 | Starts a cronjob. 22 | 23 | ```js 24 | var cron = require('cron-scheduler') 25 | 26 | cron({ on: '0 9 * * *' }, function () { 27 | console.log('this will run every 9:00am') 28 | }) 29 | ``` 30 | 31 | #### Options 32 | The `options` parameter is an object with these things: 33 | 34 | - `on` *(String, required)* - the schedule in cron format (`min hour day 35 | month day-of-week`). 36 | - `timezone` *(String)* - the timezone to run it in. 37 | - `name` *(String)* - identifier to show in the debug logs. Means nothing 38 | if debugging is off. 39 | 40 | ```js 41 | cron({ 42 | timezone: 'Asia/Manila' 43 | on: '0 9 * * *', 44 | name: 'dostuff' 45 | } , function () { 46 | console.log('this will run every 9:00am') 47 | }) 48 | ``` 49 | 50 | #### Cron strings 51 | The `options.on` parameter is in cron standard format. Check the [cron 52 | cheatsheet](http://ricostacruz.com/cheatsheets/cron.html) for more details. 53 | Here are some examples: 54 | 55 | ``` 56 | 0 9 * * * - every 9:00AM 57 | 0 12 * * 1 - every 12:00PM on mondays 58 | 0 */2 * * * - every 2 hours 59 | ``` 60 | 61 | #### Errors 62 | Any errors will be thrown, and will stop the scheduler. If this is not 63 | what you want, you may wish to decorate the function being passed. 64 | 65 | ```js 66 | cron({ on: '0 9 * * *' }, trap(work)) 67 | 68 | function trap (fn) { 69 | return function () { 70 | return Promise.resolve(fn.apply(this, arguments)) 71 | .catch(function (err) { 72 | // do stuff. 73 | // this handler will work for both promise rejections 74 | // *and* regular errors. 75 | }) 76 | } 77 | } 78 | ``` 79 | 80 | #### Promises 81 | If `function` returns a Promise, it will wait for it to finish before 82 | scheduling the next job. If the promise is rejected, it will be an unhandled 83 | rejection (!). You may use the same `trap()` decorator trick above to get 84 | around this. 85 | 86 | #### Stopping 87 | To stop the cronjob, just run the `stop` method returned by `cron()`. 88 | 89 | ```js 90 | job = cron({ on: '0 12 * * *' }, work) 91 | job.stop() 92 | ``` 93 | 94 | #### Manually starting 95 | To manually invoke the cronjob, run the `run` method returned by `cron()`. 96 | This will not stop the next scheduled invocation. 97 | 98 | ```js 99 | job = cron({ on: '0 12 * * *' }, work) 100 | job.run() 101 | ``` 102 | 103 | ## cron.debug 104 | 105 | > `cron.debug(function)` 106 | 107 | Sets the debug function. 108 | 109 | ```js 110 | cron.debug(console.log.bind(console)) 111 | ``` 112 | 113 | You can pass your custom logger here. For instance, you can use the [debug][] 114 | module for prettier messages. 115 | 116 | ```js 117 | cron.debug(require('debug')('cron')) 118 | ``` 119 | 120 | [debug]: https://www.npmjs.com/package/debug 121 | 122 | ## Thanks 123 | 124 | **cron-scheduler** © 2016+, Rico Sta. Cruz. Released under the [MIT] License.
125 | Authored and maintained by Rico Sta. Cruz with help from contributors ([list][contributors]). 126 | 127 | > [ricostacruz.com](http://ricostacruz.com)  ·  128 | > GitHub [@rstacruz](https://github.com/rstacruz)  ·  129 | > Twitter [@rstacruz](https://twitter.com/rstacruz) 130 | 131 | [MIT]: http://mit-license.org/ 132 | [contributors]: http://github.com/rstacruz/cron-scheduler/contributors 133 | --------------------------------------------------------------------------------