├── .gitignore ├── .travis.yml ├── index.js ├── package.json ├── lib ├── Job.js ├── RepeatingJob.js ├── SeriallyRepeatingJob.js ├── AbstractJob.js ├── scheduling.js └── temporalUtil.js ├── test ├── Job.test.js ├── SeriallyRepeatingJob.test.js ├── RepeatingJob.test.js ├── scheduling.test.js └── temporalUtil.test.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports.temporalUtil = module.exports.tu = require('./lib/temporalUtil.js'); 2 | 3 | module.exports.schedule = require('./lib/scheduling.js'); 4 | 5 | module.exports.AbstractJob = require('./lib/AbstractJob.js'); 6 | module.exports.Job = require('./lib/Job'); 7 | module.exports.RepeatingJob = require('./lib/RepeatingJob'); 8 | module.exports.SeriallyRepeatingJob = require('./lib/SeriallyRepeatingJob'); 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tempus-fugit", 3 | "version": "2.3.1", 4 | "description": "A scheduling and time utilities module that doesn't waste your time", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/kessler/tempus-fugit" 12 | }, 13 | "keywords": [ 14 | "schedule", 15 | "scheduling" 16 | ], 17 | "author": "Yaniv Kessler", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/kessler/tempus-fugit/issues" 21 | }, 22 | "devDependencies": { 23 | "mocha": "~1.16.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/Job.js: -------------------------------------------------------------------------------- 1 | var AbstractJob = require('./AbstractJob.js'); 2 | var $u = require('util'); 3 | 4 | module.exports = Job; 5 | 6 | /* 7 | @param task - optional, a function that will be executed 8 | @param options - { delay: [number], unref: [boolean] } 9 | */ 10 | $u.inherits(Job, AbstractJob); 11 | function Job(task, options) { 12 | AbstractJob.call(this, task, options); 13 | 14 | var self = this; 15 | this._taskFunctor = function functor() { 16 | self._task(self); 17 | }; 18 | } 19 | 20 | Job.prototype._executeImpl = function () { 21 | return setTimeout(this._taskFunctor, this._options.delay); 22 | }; 23 | 24 | Job.prototype.cancel = function () { 25 | clearTimeout(this._ref); 26 | delete this._ref; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/RepeatingJob.js: -------------------------------------------------------------------------------- 1 | var $u = require('util'); 2 | var AbstractJob = require('./AbstractJob.js'); 3 | 4 | module.exports = RepeatingJob; 5 | 6 | /* 7 | a repeating job runs repeatedly with no regards to the state of previous iterations 8 | */ 9 | $u.inherits(RepeatingJob, AbstractJob); 10 | function RepeatingJob(task, options) { 11 | AbstractJob.call(this, task, options); 12 | 13 | if (!this._options.interval) 14 | throw new Error('interval in options is missing or set to 0'); 15 | 16 | var self = this; 17 | this.executions = 0 18 | this._millis = this._options.delay; 19 | this._timer = setTimeout; 20 | 21 | this._repeatingTaskFunctor = function repeating() { 22 | self._task(self); 23 | }; 24 | 25 | this._taskFunctor = function functor() { 26 | self._taskFunctor = self._repeatingTaskFunctor; 27 | self._timer = setInterval; 28 | self._millis = self._options.interval; 29 | self._task(self); 30 | self.execute(); 31 | }; 32 | } 33 | 34 | RepeatingJob.prototype._executeImpl = function() { 35 | return this._timer.call(null, this._taskFunctor, this._millis); 36 | }; 37 | 38 | RepeatingJob.prototype.cancel = function () { 39 | // clear if its in first or repeating stage 40 | clearTimeout(this._ref); 41 | clearInterval(this._ref); 42 | }; 43 | -------------------------------------------------------------------------------- /lib/SeriallyRepeatingJob.js: -------------------------------------------------------------------------------- 1 | var RepeatingJob = require('./RepeatingJob.js'); 2 | var $u = require('util'); 3 | var MAX = Math.pow(2, 53); 4 | 5 | module.exports = SeriallyRepeatingJob; 6 | 7 | /* 8 | @param options - { delay: [number], interval:[number], continueOnError: [boolean], unref: [boolean] } 9 | */ 10 | $u.inherits(SeriallyRepeatingJob, RepeatingJob); 11 | function SeriallyRepeatingJob(task, options) { 12 | RepeatingJob.call(this, task, options); 13 | 14 | var self = this; 15 | 16 | this._timer = setTimeout; 17 | 18 | this._taskFunctor = function functor() { 19 | self._taskFunctor = self._repeatingTaskFunctor; 20 | self._millis = self._options.interval; 21 | self._task(self); 22 | }; 23 | } 24 | 25 | SeriallyRepeatingJob.prototype.done = function(err) { 26 | if (!err) { 27 | if (this.executions === MAX) { 28 | console.log('resetting executions counter cuz I reached max integer'); 29 | this.executions = 0; 30 | } 31 | 32 | this.executions++; 33 | } 34 | 35 | if (this._cancelled) return; 36 | 37 | if (err && this._options.continueOnError) return; 38 | 39 | this.execute(); 40 | }; 41 | 42 | SeriallyRepeatingJob.prototype.cancel = function () { 43 | this._cancelled = true; 44 | clearTimeout(this._ref); 45 | }; 46 | 47 | SeriallyRepeatingJob.prototype.callback = function () { 48 | var self = this; 49 | return function (err) { 50 | self.done(err); 51 | } 52 | }; -------------------------------------------------------------------------------- /test/Job.test.js: -------------------------------------------------------------------------------- 1 | var Job = require('../lib/Job.js'); 2 | var assert = require('assert'); 3 | 4 | describe('Job', function () { 5 | 6 | it('executes the task exactly once using a given delay', function (done) { 7 | var taskCalled = 0; 8 | 9 | function task() { 10 | console.log(taskCalled++); 11 | 12 | assert.strictEqual(job, this); 13 | } 14 | 15 | var job = new Job(task, { delay: 10, unref: true }); 16 | 17 | job.execute(); 18 | 19 | setTimeout(function () { 20 | assert.strictEqual(taskCalled, 1); 21 | done(); 22 | }, 30); 23 | }); 24 | 25 | it ('can be cancelled', function (done) { 26 | var taskCalled = 0; 27 | 28 | function task() { 29 | console.log(taskCalled++); 30 | 31 | assert.strictEqual(job, this); 32 | } 33 | 34 | var job = new Job(task, { delay: 100, unref: true }); 35 | 36 | job.execute(); 37 | 38 | setTimeout(function () { 39 | job.cancel(); 40 | 41 | setTimeout(function () { 42 | assert.strictEqual(taskCalled, 0); 43 | done(); 44 | }, 100); 45 | }, 30); 46 | }); 47 | 48 | it('unrefs the time if specified in the options', function () { 49 | var job = new Job(function () {}, { delay: 10, unref: true }); 50 | 51 | var unrefCalled = false; 52 | job._executeImpl = function () { 53 | return { 54 | unref: function () { 55 | unrefCalled = true; 56 | } 57 | } 58 | }; 59 | 60 | job.execute(); 61 | 62 | assert.ok(unrefCalled); 63 | }); 64 | }); 65 | 66 | -------------------------------------------------------------------------------- /lib/AbstractJob.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | var inherits = require('util').inherits 3 | 4 | module.exports = AbstractJob; 5 | 6 | /* 7 | @param task - optional, a function that will be executed 8 | @param options - { unref: [boolean] } 9 | */ 10 | 11 | function AbstractJob(task, options) { 12 | // EventEmitter.call(this) 13 | 14 | if (task) 15 | this._task = task; 16 | else 17 | options = task; 18 | 19 | if (!options) 20 | throw new Error('missing options'); 21 | 22 | this._options = options; 23 | } 24 | 25 | AbstractJob.prototype.execute = function () { 26 | 27 | this._ref = this._executeImpl(); 28 | 29 | if (this._ref && this._options.unref) 30 | this._ref.unref(); 31 | }; 32 | 33 | AbstractJob.prototype._executeImpl = function () { 34 | throw new Error('must implement'); 35 | }; 36 | 37 | AbstractJob.prototype._cancelImpl = function(token) { 38 | throw new Error('must implement'); 39 | }; 40 | 41 | /* 42 | must be overridden or assigned 43 | */ 44 | AbstractJob.prototype._task = function() { 45 | throw new Error('assign as task to this job or subclass it and "override" this method'); 46 | }; 47 | 48 | AbstractJob.prototype.cancel = function () { 49 | return this._cancelImpl(this._ref); 50 | }; 51 | 52 | // maintain api compatibility with all implementations 53 | var noop = function () {}; 54 | 55 | AbstractJob.prototype.done = noop; 56 | 57 | AbstractJob.prototype.callback = function () { 58 | return noop 59 | }; -------------------------------------------------------------------------------- /test/SeriallyRepeatingJob.test.js: -------------------------------------------------------------------------------- 1 | var SeriallyRepeatingJob = require('../lib/SeriallyRepeatingJob.js'); 2 | var assert = require('assert'); 3 | 4 | describe('SeriallyRepeatingJob', function () { 5 | 6 | it('executes the task repeatedly, first after a certain delay and then in constant intervals, job execution will not overlap - meaning delays can occur', function (done) { 7 | var taskCalled = 0; 8 | 9 | function task(job) { 10 | taskCalled++; 11 | 12 | assert.strictEqual(job, this); 13 | 14 | if (taskCalled == 1) { 15 | setTimeout(function () { 16 | job.done(); 17 | }, 500); 18 | } else { 19 | job.done(); 20 | } 21 | } 22 | 23 | var job = new SeriallyRepeatingJob(task, { delay: 200, interval: 200, unref: true }); 24 | 25 | job.execute(); 26 | 27 | setTimeout(function () { 28 | assert.strictEqual(taskCalled, 1); 29 | 30 | setTimeout(function () { 31 | assert.strictEqual(taskCalled, 2); 32 | setTimeout(function () { 33 | assert.strictEqual(taskCalled, 3); 34 | done(); 35 | }, 205); 36 | }, 750); 37 | 38 | }, 250); 39 | }); 40 | 41 | it('will not continue if task implementation doesnt call done', function (done) { 42 | var taskCalled = 0; 43 | 44 | function task(job) { 45 | taskCalled++; 46 | assert.strictEqual(job, this); 47 | } 48 | 49 | var job = new SeriallyRepeatingJob(task, { delay: 20, interval: 100, unref: true }); 50 | 51 | job.execute(); 52 | 53 | setTimeout(function () { 54 | assert.strictEqual(taskCalled, 1); 55 | done(); 56 | }, 200); 57 | }); 58 | 59 | it('can be cancelled', function (done) { 60 | var taskCalled = 0; 61 | 62 | function task(job) { 63 | taskCalled++; 64 | assert.strictEqual(job, this); 65 | job.done(); 66 | } 67 | 68 | var job = new SeriallyRepeatingJob(task, { delay: 100, interval: 100, unref: true }); 69 | 70 | job.execute(); 71 | 72 | setTimeout(function () { 73 | assert.strictEqual(taskCalled, 1); 74 | job.cancel(); 75 | 76 | setTimeout(function () { 77 | assert.strictEqual(taskCalled, 1); 78 | done(); 79 | }, 200); 80 | }, 110); 81 | }); 82 | }); 83 | 84 | -------------------------------------------------------------------------------- /test/RepeatingJob.test.js: -------------------------------------------------------------------------------- 1 | var RepeatingJob = require('../lib/RepeatingJob.js'); 2 | var assert = require('assert'); 3 | 4 | describe('RepeatingJob', function (done) { 5 | 6 | it('executes the task repeatedly, first after a certain delay and then in constant intervals with no regard to overlapping executions', function () { 7 | var taskCalled = 0; 8 | 9 | function task() { 10 | taskCalled++; 11 | assert.strictEqual(job, this); 12 | } 13 | 14 | var job = new RepeatingJob(task, { delay: 1000, interval: 1000, unref: true }); 15 | 16 | job.execute(); 17 | 18 | setTimeout(function () { 19 | assert.strictEqual(taskCalled, 1); 20 | 21 | setTimeout(function () { 22 | assert.strictEqual(taskCalled, 4); 23 | }, 3300); 24 | }, 1100); 25 | }); 26 | 27 | it('can be cancelled', function (done) { 28 | var taskCalled = 0; 29 | 30 | function task() { 31 | console.log(taskCalled++); 32 | assert.strictEqual(job, this); 33 | } 34 | 35 | var job = new RepeatingJob(task, { delay: 100, interval: 100, unref: true }); 36 | 37 | job.execute(); 38 | 39 | setTimeout(function () { 40 | job.cancel(); 41 | 42 | setTimeout(function () { 43 | assert.strictEqual(taskCalled, 1); 44 | done(); 45 | }, 100); 46 | }, 110); 47 | }); 48 | 49 | it('unrefs the time if specified in the options', function () { 50 | var job = new RepeatingJob(function () {}, { delay: 10, interval: 100, unref: true }); 51 | 52 | var unrefCalled = false; 53 | job._executeImpl = function () { 54 | return { 55 | unref: function () { 56 | unrefCalled = true; 57 | } 58 | } 59 | }; 60 | 61 | job.execute(); 62 | 63 | assert.ok(unrefCalled); 64 | }); 65 | 66 | // it('bench', function () { 67 | 68 | // function task() { 69 | // } 70 | 71 | // var size = 10000; 72 | // var time = process.hrtime(); 73 | 74 | // for (var i = 0; i < size; i++) { 75 | // var job = new RepeatingJob(task, { delay: 1, interval: 100, unref: true }); 76 | 77 | // } 78 | // var diff = process.hrtime(time); 79 | 80 | // console.log('benchmark took %d nanoseconds', (diff[0] * 1e9 + diff[1]) / size); 81 | 82 | // }); 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /lib/scheduling.js: -------------------------------------------------------------------------------- 1 | var $u = require('util'); 2 | var Job = require('./Job.js'); 3 | var RepeatingJob = require('./RepeatingJob.js'); 4 | var SeriallyRepeatingJob = require('./SeriallyRepeatingJob.js'); 5 | var tu = require('./temporalUtil.js'); 6 | 7 | var EMPTY = {}; 8 | 9 | /* 10 | @param intervalOrDate - an interval object or date 11 | @param task - a function to be executed 12 | @param options - { 13 | unref: [boolean] (default false) setting this to true will issue automatic unref() on timers, 14 | overlappingExecutions: [boolean] (default false) setting this to true will cause tasks to overlap if 15 | they dont finish before interval time elapses, 16 | createOnly: [boolean] (default false) if set to true execute() will not be called 17 | } 18 | */ 19 | module.exports = function schedule(intervalOrDate, task, options) { 20 | 21 | // warning: DO NOT pass this directly to a job, I suspect it will prevent it from being garbage collected 22 | options = options || {}; 23 | 24 | var Implementation, delay, job, start, now; 25 | 26 | var opts = { unref: options.unref }; 27 | 28 | if ($u.isDate(intervalOrDate)) { 29 | 30 | now = Date.now(); 31 | start = intervalOrDate.getTime(); 32 | Implementation = Job; 33 | 34 | } else if (typeof(intervalOrDate) === 'number') { 35 | 36 | now = Date.now(); 37 | start = now + intervalOrDate; 38 | Implementation = Job; 39 | 40 | } else if (typeof(intervalOrDate) === 'object'){ 41 | 42 | now = intervalOrDate.now || Date.now(); 43 | 44 | opts.interval = tu.intervalObjectToMillis(intervalOrDate); 45 | 46 | // 0 is an ok value for delay, it means the interval starts from exactly now 47 | // no delay means we want to start from the next "round" interval 48 | if (intervalOrDate.start) { 49 | start = intervalOrDate.start; 50 | } else if ($u.isDate(intervalOrDate.start)) { 51 | start = intervalOrDate.start.getTime(); 52 | } else { 53 | start = tu.nextIntervalEvent(opts.interval, now); 54 | } 55 | 56 | if (options.overlappingExecutions) { 57 | Implementation = RepeatingJob; 58 | } else { 59 | Implementation = SeriallyRepeatingJob; 60 | } 61 | } else { 62 | throw new TypeError('invalid interval arguments, must be Date or Interval Object'); 63 | } 64 | 65 | // TODO: not sure if its so good to add 50ms here just to prevent { start: Date.now() } to throw an error since it will be smaller than internal 66 | // now in a few ms 67 | if (start + 50 < now) { 68 | throw new Error('job start is in the past'); 69 | } 70 | 71 | opts.delay = start - now; 72 | 73 | job = new Implementation(task, opts); 74 | 75 | if(!options.createOnly) { 76 | job.execute(); 77 | } 78 | 79 | return job; 80 | }; -------------------------------------------------------------------------------- /test/scheduling.test.js: -------------------------------------------------------------------------------- 1 | var schedule = require('../lib/scheduling.js'); 2 | var assert = require('assert'); 3 | var Constants = require('../lib/temporalUtil.js').millisConstants; 4 | 5 | describe('scheduling', function () { 6 | 7 | 8 | it('schedules a one time job for a date in the future', function (done) { 9 | var now = new Date(); 10 | 11 | var jobTime = new Date(now.getTime() + 100); 12 | 13 | var taskFired = 0; 14 | 15 | function task(job) { 16 | taskFired++; 17 | } 18 | 19 | var job = schedule(jobTime, task); 20 | 21 | setTimeout(function () { 22 | assert.strictEqual(taskFired, 1); 23 | 24 | setTimeout(function () { 25 | assert.strictEqual(taskFired, 1); 26 | done(); 27 | }, 100) 28 | }, 120) 29 | }); 30 | 31 | 32 | describe('schedules a repeating job', function () { 33 | 34 | it('starts in the next interval event', function () { 35 | 36 | var taskFired = 0; 37 | 38 | function task(job) { 39 | taskFired++; 40 | job.done(); 41 | } 42 | 43 | var sampleDate = new Date(2013, 11, 25, 0, 30); 44 | 45 | var interval = { 46 | hour: 1, 47 | minute: 60, 48 | now: sampleDate.getTime() 49 | }; 50 | 51 | var job = schedule(interval, task, { createOnly: true }); 52 | 53 | assert.strictEqual(job._options.delay, Constants.HOUR + (Constants.MINUTE * 30)); 54 | 55 | assert.strictEqual(job._options.interval, Constants.HOUR * 2); 56 | }); 57 | 58 | it('starts whenever we tell it to start', function () { 59 | 60 | var taskFired = 0; 61 | 62 | function task(job) { 63 | taskFired++; 64 | job.done(); 65 | } 66 | 67 | var sampleNow = new Date(2013, 11, 25, 0, 10); 68 | var sampleDate = new Date(2013, 11, 25, 0, 30); 69 | 70 | var interval = { 71 | hour: 1, 72 | minute: 60, 73 | now: sampleNow.getTime(), 74 | start: sampleDate 75 | }; 76 | 77 | var job = schedule(interval, task, { createOnly: true }); 78 | 79 | assert.strictEqual(job._options.delay, Constants.MINUTE * 20); 80 | 81 | assert.strictEqual(job._options.interval, Constants.HOUR * 2); 82 | }); 83 | 84 | it('executes on time (second resolution)', function (done) { 85 | 86 | this.timeout(7500); 87 | 88 | var now = Date.now(); 89 | 90 | var taskFired = 0; 91 | var fireTime = 0; 92 | function task(job) { 93 | fireTime = Date.now(); 94 | taskFired++; 95 | job.done(); 96 | } 97 | 98 | var interval = { 99 | second: 5, 100 | start: now + 1000 101 | }; 102 | 103 | var job = schedule(interval, task); 104 | 105 | setTimeout(function () { 106 | assert.strictEqual(taskFired, 2); 107 | assert.strictEqual(Math.round(fireTime / 1000), Math.round( (now + 6000) / 1000)); 108 | done(); 109 | }, 7000); 110 | }); 111 | 112 | it('execute the job immediately and repeat by interval from the first execution', function (done) { 113 | this.timeout(7500); 114 | 115 | var now = Date.now(); 116 | 117 | var fireTime = 0; 118 | 119 | function task(job) { 120 | fireTime = Date.now(); 121 | job.done(); 122 | } 123 | 124 | var interval = { 125 | second: 5, 126 | start: Date.now() 127 | }; 128 | 129 | var job = schedule(interval, task); 130 | 131 | setTimeout(function () { 132 | assert.strictEqual(Math.round(fireTime / 1000), Math.round( (now + 5000) / 1000)); 133 | done(); 134 | }, 7000); 135 | }); 136 | }); 137 | 138 | }); 139 | -------------------------------------------------------------------------------- /lib/temporalUtil.js: -------------------------------------------------------------------------------- 1 | var constants = { 2 | SECOND: 1000, 3 | MINUTE: 1000 * 60, 4 | HOUR: 1000 * 60 * 60, 5 | DAY: 1000 * 60 * 60 * 24 6 | }; 7 | 8 | constants.TROPICAL_YEAR = constants.DAY * 365 + (constants.HOUR * 5) + (constants.MINUTE * 48) + (constants.SECOND * 46); 9 | 10 | module.exports.millisConstants = constants; 11 | 12 | /* 13 | interval object fields 14 | 15 | millisecond 16 | second 17 | minute 18 | hour 19 | day 20 | 21 | (currently does not support month and year intervals) 22 | */ 23 | module.exports.intervalObjectToMillis = function(intervalObject) { 24 | 25 | var total = 0; 26 | 27 | if (intervalObject.millisecond) 28 | total += intervalObject.millisecond; 29 | 30 | if (intervalObject.second) 31 | total += 1000 * intervalObject.second; 32 | 33 | if (intervalObject.minute) 34 | total += 60000 * intervalObject.minute; 35 | 36 | if (intervalObject.hour) 37 | total += 3600000 * intervalObject.hour; 38 | 39 | if (intervalObject.day) 40 | total += 86400000 * intervalObject.day; 41 | 42 | return total; 43 | }; 44 | 45 | module.exports.intervalCountSinceEpoch = function (interval, millisSinceEpoch) { 46 | return (millisSinceEpoch || Date.now()) / interval; 47 | }; 48 | 49 | module.exports.nextIntervalEvent = function(interval, millisSinceEpoch) { 50 | 51 | var count = Math.floor(exports.intervalCountSinceEpoch(interval, millisSinceEpoch)); 52 | 53 | return (count + 1) * interval; 54 | }; 55 | 56 | /* 57 | this is not precise when trying to find big intervals like years. 58 | */ 59 | module.exports.recentIntervalEvent = function(interval, millisSinceEpoch) { 60 | 61 | var count = Math.floor(exports.intervalCountSinceEpoch(interval, millisSinceEpoch)); 62 | 63 | return count * interval; 64 | }; 65 | 66 | 67 | /* 68 | returns a Date object rounded to the next second 69 | */ 70 | module.exports.nextSecond = function(now) { 71 | 72 | return new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes(), now.getSeconds() + 1, 0); 73 | }; 74 | 75 | /* 76 | returns a Date object rounded to the next minute 77 | */ 78 | module.exports.nextMinute = function(now) { 79 | 80 | return new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes() + 1, 0, 0); 81 | }; 82 | 83 | /* 84 | returns a Date object rounded to the next hour 85 | */ 86 | module.exports.nextHour = function(now) { 87 | 88 | return new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours() + 1, 0, 0, 0); 89 | }; 90 | 91 | /* 92 | returns a Date object rounded to the next day 93 | */ 94 | module.exports.nextDate = function(now) { 95 | 96 | return new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0, 0); 97 | }; 98 | 99 | 100 | /* 101 | returns a Date object rounded to the next month 102 | */ 103 | module.exports.nextMonth = function(now) { 104 | 105 | return new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0); 106 | }; 107 | 108 | 109 | /* 110 | returns a Date object rounded to the next year 111 | */ 112 | module.exports.nextYear = function(now) { 113 | 114 | return new Date(now.getFullYear() + 1, 0, 1, 0, 0, 0, 0); 115 | }; 116 | 117 | /* 118 | 119 | */ 120 | module.exports.normalizeIntervalObject = function(intervalObject) { 121 | if (intervalObject.millisecond >= 1000) { 122 | var extraSeconds = Math.floor(intervalObject.millisecond / 1000); 123 | intervalObject.millisecond = intervalObject.millisecond % 1000; 124 | 125 | if (!intervalObject.second) 126 | intervalObject.second = 0; 127 | 128 | intervalObject.second += extraSeconds; 129 | } 130 | 131 | if (intervalObject.second >= 60) { 132 | 133 | var extraMinutes = Math.floor( intervalObject.second / 60 ); 134 | intervalObject.second = intervalObject.second % 60; 135 | 136 | if (!intervalObject.minute) 137 | intervalObject.minute = 0; 138 | 139 | intervalObject.minute += extraMinutes; 140 | } 141 | 142 | if (intervalObject.minute >= 60) { 143 | 144 | var extraHours = Math.floor( intervalObject.minute / 60 ); 145 | intervalObject.minute = intervalObject.minute % 60; 146 | 147 | if (!intervalObject.hour) 148 | intervalObject.hour = 0; 149 | 150 | intervalObject.hour += extraHours; 151 | } 152 | 153 | if (intervalObject.hour >= 24) { 154 | 155 | var extraDays = Math.floor( intervalObject.hour / 24 ); 156 | intervalObject.hour = intervalObject.hour % 24; 157 | 158 | if (!intervalObject.day) 159 | intervalObject.day = 0; 160 | 161 | intervalObject.day += extraDays; 162 | } 163 | 164 | return intervalObject; 165 | }; 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tempus Fugit [![Build Status](https://secure.travis-ci.org/kessler/tempus-fugit.png?branch=master)](http://travis-ci.org/kessler/tempus-fugit) [![Join the chat at https://gitter.im/kessler/tempus-fugit](https://badges.gitter.im/kessler/tempus-fugit.svg)](https://gitter.im/kessler/tempus-fugit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | 4 | > Tempus fugit is a Latin expression meaning "time flees", more commonly translated as "time flies". It is frequently used as an inscription on clocks. 5 | 6 | This module contains high level api for scheduling jobs and also exposes utilities and classes to help build other more custom / complex scheduling code. 7 | 8 | Install 9 | ------- 10 | ``` 11 | npm install tempus-fugit 12 | ``` 13 | 14 | Usage 15 | ----- 16 | ### Scheduling api 17 | 18 | The scheduling api can be used to schedule single time or repeating jobs. Repeating jobs schedule is defined using the interval object (see below). 19 | 20 | ##### schedule a one time job in the future: 21 | ```js 22 | var schedule = require('tempus-fugit').schedule; 23 | 24 | var futureDate = new Date(....); 25 | function task() {} 26 | 27 | var job = schedule(futureDate, task); 28 | 29 | // can cancel 30 | job.cancel(); 31 | 32 | job = schedule(1000, task); // schedule in 1 second from now 33 | ``` 34 | 35 | ##### schedule a repeating / recurring job: 36 | ```js 37 | var schedule = require('tempus-fugit').schedule; 38 | 39 | var interval = { hour: 1, minute: 5 }; // every hour and 5 minutes 40 | 41 | // job.done() is not required when overlappingExecutions is true 42 | function task(job) { 43 | // this.done() also works 44 | // also job.callback() can be used to create a callback function instead, e.g fs.readFile('foo', job.callback()) 45 | job.done(); 46 | } 47 | 48 | var job = schedule(interval, task /*, {.. options ..} */); 49 | 50 | // can cancel 51 | job.cancel(); 52 | ``` 53 | ##### scheduling options: 54 | 55 | unref: \[boolean\] (default false) setting this to true will issue automatic unref() on timers, which will allow the node process to exit when a task is run. 56 | 57 | overlappingExecutions: \[boolean\] (default false) setting this to true will cause tasks to overlap if 58 | they dont finish before interval time elapses. 59 | 60 | createOnly: \[boolean\] (default false) if set to true execute() will not be called, this means you will have to call job.execute() after shceduling.schedule(...) 61 | 62 | ##### the interval object: 63 | ```js 64 | var interval = { 65 | millisecond: 1, 66 | second: 2, 67 | minute: 3, 68 | hour: 4, 69 | day: 5, 70 | start: Date.now() + 10000 || new Date('some date in the future') //optional 71 | } 72 | ``` 73 | The interval object supports all the time units displayed above, those can also be used in combination to create more complex intervals (e.g day + hour + second). When scheduling a task using an interval object, tempus-fugit will sync the execution cycle to the next round occurance of the interval. 74 | 75 | For example, look at the following code: 76 | ```js 77 | schedule({ hour: 1 }, function task (job) { job.done() }) 78 | ``` 79 | If initially run at 20:31, will execute task at 21:00, then 22:00, then 23:00 etc... 80 | 81 | If we want to start the cycle right away we can use the optional **start** property: 82 | ```js 83 | schedule({ hour: 1, start: Date.now() }, function task (job) { job.done() }) 84 | ``` 85 | If initially run at 20:31, will execute task at 20:31, then 21:31, 22:31 etc... 86 | 87 | ##### Creating new job "classes" 88 | ```js 89 | var AbstractJob = require('tempus-fugit').AbstractJob; 90 | var $u = require('util'); 91 | 92 | $u.inherits(MyJob, AbstractJob); 93 | function MyJob(task, options) { 94 | AbstractJob.call(this, task, options) 95 | } 96 | 97 | // must implement 98 | MyJob.prototype._executeImpl = function () { 99 | return setInterval(this._task, 500); 100 | }; 101 | 102 | // must implement 103 | MyJob.prototype._cancelImpl = function(token) { 104 | return clearInterval(token); 105 | }; 106 | 107 | // optionally implement, if so, do no pass task argument in constructor 108 | MyJob.prototype._task = function () { 109 | console.log('foo!'); 110 | }; 111 | 112 | ``` 113 | - - - 114 | ### Interval util 115 | 116 | ##### tu.intervalObjectToMillis(): 117 | ```js 118 | var tu = require('tempus-fugit').temporalUtil; 119 | 120 | var interval = { millisecond: 500, second: 2 }; 121 | 122 | console.log(tu.intervalObjectToMillis(interval)); 123 | ``` 124 | will print: 125 | 126 | > 2500 127 | 128 | ##### tu.normalizeIntervalObject: 129 | ```js 130 | var tu = require('tempus-fugit').tu; 131 | 132 | var interval = { millisecond: 1502, second: 2 }; 133 | 134 | console.log(tu.normalizeIntervalObject(interval)); 135 | ``` 136 | will print: 137 | 138 | > { millisecond: 502, second: 3 } 139 | 140 | _note: this will modify the original interval object_ 141 | 142 | ##### tu.intervalCountSinceEpoch: 143 | ```js 144 | var tu = require('tempus-fugit').tu; 145 | 146 | var interval = { day: 1 }; 147 | 148 | var n = Date.UTC(2000, 0); 149 | 150 | var millis = tu.intervalObjectToMillis(interval); 151 | 152 | console.log(tu.intervalCountSinceEpoch(millis, n)); 153 | 154 | ``` 155 | will print: 156 | 157 | > 10957 158 | 159 | which is 30 years * 365 day + 7(.5) days from leap years 160 | 161 | _note: the n argument is optional, if omitted the function will use Date.now() internally_ 162 | 163 | ##### tu.nextIntervalEvent: 164 | ```js 165 | var tu = require('tempus-fugit').tu; 166 | 167 | var interval = { day: 1 }; 168 | 169 | var n = Date.UTC(2000, 0, 1, 0, 30); // Sat Jan 01 2000 00:30:00 GMT 170 | 171 | var millis = tu.intervalObjectToMillis(interval); 172 | 173 | var nextInterval = tu.nextIntervalEvent(millis, n); 174 | 175 | console.log(new Date(nextInterval).toUTCString()); 176 | 177 | ``` 178 | will print: 179 | 180 | > Sun, 02 Jan 2000 00:00:00 GMT 181 | 182 | _note: the n argument is optional, if omitted the function will use Date.now() internally_ 183 | 184 | 185 | ### Date related util 186 | 187 | tu.nextSecond(date); 188 | 189 | tu.nextMinute(date); 190 | 191 | tu.nextHour(date); 192 | 193 | tu.nextDate(date); 194 | 195 | tu.nextMonth(date); 196 | 197 | tu.nextYear(date); 198 | 199 | ##### example 200 | ```js 201 | var tf = require('tempus-fugit'); 202 | 203 | var now = new Date(2013, 11, 25, 23, 23, 59, 123); 204 | var actual = tf.tu.nextSecond(now); // tf.tu === tf.temporalUtil 205 | 206 | console.log('closest second:'); 207 | console.log(now); 208 | console.log(actual); 209 | 210 | ``` 211 | will print: 212 | 213 | > Wed Dec 25 2013 23:23:59 GMT+0200 (Jerusalem Standard Time) 214 | 215 | > Wed Dec 25 2013 23:24:00 GMT+0200 (Jerusalem Standard Time) 216 | 217 | ### A Pitfall 218 | I should probably find a solution for this, but for now, if you run a SeriallyRepeatingJob like this one: 219 | ```js 220 | schedule({ /*some interval data*/ }, function (job) {}) 221 | ``` 222 | Calling job.done() will created a timer. But in this case we don't, if there are no other pending tasks in the event loop the process will exit. 223 | 224 | ### TODO 225 | ---- 226 | - support month and year intervals, calculated correctly 227 | - throw exception from jobs if error event is not handled or ignore errors flag is not set 228 | - add more events to job 229 | 230 | 231 | -------------------------------------------------------------------------------- /test/temporalUtil.test.js: -------------------------------------------------------------------------------- 1 | var tu = require('../lib/temporalUtil.js'); 2 | var assert = require('assert'); 3 | 4 | var tropicalYear = tu.millisConstants.TROPICAL_YEAR; 5 | 6 | describe('temporalUtil', function () { 7 | 8 | it('compute millis from an interval literal', function () { 9 | 10 | var literal = { 11 | millisecond: 5, 12 | second: 2, 13 | minute: 4, 14 | hour: 5, 15 | day: 3 16 | }; 17 | 18 | var actual = tu.intervalObjectToMillis(literal); 19 | 20 | assert.strictEqual(actual, 5 + (2 * 1000) + (60000 * 4) + (3600000 * 5) + (86400000 * 3)); 21 | }); 22 | 23 | 24 | it('count how many intervals occurred since epoch', function () { 25 | var now = new Date(); 26 | 27 | // roughly how many years since unix epoch 28 | var actual = tu.intervalCountSinceEpoch(tropicalYear, now.getTime()); 29 | 30 | assert.strictEqual(Math.floor(actual), now.getUTCFullYear() - 1970); 31 | }); 32 | 33 | it('find the most recent interval event (calculated since epoch)', function () { 34 | 35 | var now = new Date(); 36 | 37 | // day 38 | var actual = tu.recentIntervalEvent(tu.millisConstants.DAY, now.getTime()); 39 | var expected = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); 40 | 41 | assert.strictEqual(actual, expected); 42 | 43 | 44 | // hour 45 | actual = tu.recentIntervalEvent(tu.millisConstants.HOUR, now.getTime()); 46 | expected = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours()); 47 | 48 | assert.strictEqual(actual, expected); 49 | 50 | 51 | // minute 52 | actual = tu.recentIntervalEvent(tu.millisConstants.MINUTE, now.getTime()); 53 | expected = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes()); 54 | 55 | assert.strictEqual(actual, expected); 56 | 57 | // second 58 | actual = tu.recentIntervalEvent(tu.millisConstants.SECOND, now.getTime()); 59 | expected = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds()); 60 | 61 | assert.strictEqual(actual, expected); 62 | }); 63 | 64 | 65 | it('find the next interval event (calculated since epoch)', function () { 66 | 67 | var now = new Date(); 68 | 69 | // day 70 | var actual = tu.nextIntervalEvent(tu.millisConstants.DAY, now.getTime()); 71 | var expected = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1); 72 | 73 | assert.strictEqual(actual, expected); 74 | 75 | 76 | // hour 77 | actual = tu.nextIntervalEvent(tu.millisConstants.HOUR, now.getTime()); 78 | expected = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours() + 1); 79 | 80 | assert.strictEqual(actual, expected); 81 | 82 | 83 | // minute 84 | actual = tu.nextIntervalEvent(tu.millisConstants.MINUTE, now.getTime()); 85 | expected = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes() + 1); 86 | 87 | assert.strictEqual(actual, expected); 88 | 89 | // second 90 | actual = tu.nextIntervalEvent(tu.millisConstants.SECOND, now.getTime()); 91 | expected = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds() + 1); 92 | 93 | assert.strictEqual(actual, expected); 94 | }); 95 | 96 | 97 | 98 | /* 99 | 100 | in javascript Date object months are 0 - 11 !!! its stupid but thats life 101 | (and its probably not stupid but has a complex reason I don't care to find right now) 102 | 103 | */ 104 | it('rounds from now to next second', function () { 105 | var now = new Date(2013, 11, 25, 23, 23, 59, 123); 106 | 107 | var actual = tu.nextSecond(now); 108 | 109 | console.log('next second:'); 110 | console.log(now); 111 | console.log(actual + '\n'); 112 | 113 | assert.strictEqual(actual.getMilliseconds(), 0); 114 | assert.strictEqual(actual.getSeconds(), 0); 115 | assert.strictEqual(actual.getMinutes(), 24); 116 | assert.strictEqual(actual.getHours(), 23); 117 | assert.strictEqual(actual.getDate(), 25); 118 | assert.strictEqual(actual.getMonth(), 11); 119 | assert.strictEqual(actual.getFullYear(), 2013); 120 | }); 121 | 122 | it('rounds from now to next minute', function () { 123 | var now = new Date(2013, 11, 25, 23, 59, 59, 123); 124 | 125 | var actual = tu.nextMinute(now); 126 | 127 | console.log('next minute:'); 128 | console.log(now); 129 | console.log(actual + '\n'); 130 | 131 | assert.strictEqual(actual.getMilliseconds(), 0); 132 | assert.strictEqual(actual.getSeconds(), 0); 133 | assert.strictEqual(actual.getMinutes(), 0); 134 | assert.strictEqual(actual.getHours(), 0); 135 | assert.strictEqual(actual.getDate(), 26); 136 | assert.strictEqual(actual.getMonth(), 11); 137 | assert.strictEqual(actual.getFullYear(), 2013); 138 | }); 139 | 140 | it('rounds from now to next hour', function () { 141 | var now = new Date(2013, 11, 25, 22, 1, 1, 123); 142 | 143 | var actual = tu.nextHour(now); 144 | 145 | console.log('next hour:'); 146 | console.log(now); 147 | console.log(actual + '\n'); 148 | 149 | assert.strictEqual(actual.getMilliseconds(), 0); 150 | assert.strictEqual(actual.getSeconds(), 0); 151 | assert.strictEqual(actual.getMinutes(), 0); 152 | assert.strictEqual(actual.getHours(), 23); 153 | assert.strictEqual(actual.getDate(), 25); 154 | assert.strictEqual(actual.getMonth(), 11); 155 | assert.strictEqual(actual.getFullYear(), 2013); 156 | }); 157 | 158 | 159 | it('rounds from now to next date', function () { 160 | var now = new Date(2013, 11, 25, 22, 1, 1, 123); 161 | 162 | var actual = tu.nextDate(now); 163 | 164 | console.log('next date:'); 165 | console.log(now); 166 | console.log(actual + '\n'); 167 | 168 | assert.strictEqual(actual.getMilliseconds(), 0); 169 | assert.strictEqual(actual.getSeconds(), 0); 170 | assert.strictEqual(actual.getMinutes(), 0); 171 | assert.strictEqual(actual.getHours(), 0); 172 | assert.strictEqual(actual.getDate(), 26); 173 | assert.strictEqual(actual.getMonth(), 11); 174 | assert.strictEqual(actual.getFullYear(), 2013); 175 | }); 176 | 177 | it('rounds from now to next month', function () { 178 | var now = new Date(2013, 11, 25, 22, 1, 1, 123); 179 | 180 | var actual = tu.nextMonth(now); 181 | 182 | console.log('next month:'); 183 | console.log(now); 184 | console.log(actual + '\n'); 185 | 186 | assert.strictEqual(actual.getMilliseconds(), 0); 187 | assert.strictEqual(actual.getSeconds(), 0); 188 | assert.strictEqual(actual.getMinutes(), 0); 189 | assert.strictEqual(actual.getHours(), 0); 190 | assert.strictEqual(actual.getDate(), 1); 191 | assert.strictEqual(actual.getMonth(), 0); 192 | assert.strictEqual(actual.getFullYear(), 2014); 193 | }); 194 | 195 | it('rounds from now to next year', function () { 196 | var now = new Date(2013, 11, 25, 22, 1, 1, 123); 197 | 198 | var actual = tu.nextYear(now); 199 | 200 | console.log('next year:'); 201 | console.log(now); 202 | console.log(actual + '\n'); 203 | 204 | assert.strictEqual(actual.getMilliseconds(), 0); 205 | assert.strictEqual(actual.getSeconds(), 0); 206 | assert.strictEqual(actual.getMinutes(), 0); 207 | assert.strictEqual(actual.getHours(), 0); 208 | assert.strictEqual(actual.getDate(), 1); 209 | assert.strictEqual(actual.getMonth(), 0); 210 | assert.strictEqual(actual.getFullYear(), 2014); 211 | }); 212 | 213 | 214 | it('normalizes interval objects', function () { 215 | var io = { 216 | millisecond: 1001, 217 | second: 59, 218 | minute: 59, 219 | hour: 23 220 | } 221 | 222 | var actual = tu.normalizeIntervalObject(io); 223 | var expected = { millisecond: 1, second: 0, minute: 0, hour: 0, day: 1 }; 224 | 225 | for (var f in expected) { 226 | assert.strictEqual(actual[f], expected[f]); 227 | } 228 | }); 229 | 230 | }); --------------------------------------------------------------------------------