Skip to content

Commit

Permalink
Merge 8534523 into 162ace0
Browse files Browse the repository at this point in the history
  • Loading branch information
DonutEspresso committed Jan 23, 2017
2 parents 162ace0 + 8534523 commit 51ff1b7
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: node_js
node_js:
- "0.12"
- "4"
- "6"
- "node"
after_success: 'make report-coverage'
171 changes: 140 additions & 31 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function Reissue(opts) {
assert.func(opts.func, 'func');
assert.optionalObject(opts.context, 'context');
assert.optionalArray(opts.args, 'args');
assert.optionalNumber(opts.timeout, 'timeout');

// assert options of different types
var typeofInterval = typeof opts.interval;
Expand Down Expand Up @@ -76,6 +77,12 @@ function Reissue(opts) {
self._funcArgs = (opts.args) ?
opts.args.concat(boundDone) : [boundDone];

/**
* schedule an optional timeout which will trigger timeout event if a
* given invocation exceeds this number.
* @type {Number}
*/
self._timeoutMs = opts.timeout || null;

//--------------------------------------------------------------------------
// internal properties
Expand All @@ -94,12 +101,6 @@ function Reissue(opts) {
*/
self._startTime = 0;

/**
* setTimeout handler of next invocation
* @type {Function}
*/
self._nextHandlerId = null;

/**
* boolean flag set when we are waiting for user supplied function to
* complete. technically we should know this if self._nextHandlerId had
Expand All @@ -108,6 +109,19 @@ function Reissue(opts) {
* @type {Boolean}
*/
self._inUserFunc = false;

/**
* setTimeout handler of next invocation
* @type {Function}
*/
self._nextHandlerId = null;

/*
* setTimeout handler of an internal timeout implementation. used to fire
* the timeout event if a user supplied function takes too long.
* @type {Function}
*/
self._timeoutHandlerId = null;
}
util.inherits(Reissue, events.EventEmitter);

Expand All @@ -124,16 +138,35 @@ util.inherits(Reissue, events.EventEmitter);
* @return {undefined}
*/
Reissue.prototype._execute = function _execute() {

var self = this;
self._startTime = Date.now();

// this last invocation might have been scheduled before user called
// stop(). ensure we're still active before invocation.
if (self._active === true) {
// set flag so we know we're currently in user supplied func
self._inUserFunc = true;
// execute their func
self._func.apply(self._funcContext, self._funcArgs);

// if timeout option is specified, schedule one here. basically, we
// execute the next invocation immediately above, then schedule a
// timeout handler, so we basically have two set timeout functions
// being scheduled.
if (self._timeoutMs !== null) {
// assign timeout to self so that we can cancel it if we complete
// on time.
self._timeoutHandlerId = setTimeout(
bind(self._onTimeout, self),
self._timeoutMs
);
// set our own stateful flags on timeout handler
self._timeoutHandlerId.___reissue = {
fired: false,
done: false
};
}
} else {
self._stop();
}
};

Expand Down Expand Up @@ -163,23 +196,48 @@ Reissue.prototype._done = function _done(err) {
self.emit('error', err);
}

// if user called stop(), we're done! don't queue up another invocation.
if (self._active === false) {
return;
} else {
// start invocation immediately if: the elapsedTime is greater than the
// interval, which means the last execution took longer than the
// interval itself. otherwise, subtract the time the previous
// invocation took.
var timeToInvocation = (elapsedTime >= interval) ?
0 : (interval - elapsedTime);

self._nextHandlerId = setTimeout(function _nextInvocation() {
self._execute();
}, timeToInvocation);
// if a timeout was setup, and user supplied fn exceeded that timeout and
// we're still waiting for it to complete, block.
if (self._timeoutHandlerId) {
if (self._timeoutHandlerId.___reissue.fired === true &&
self._timeoutHandlerId.___reissue.done === false) {
return self.once('_timeoutComplete', function() {
_internalDone();
});
}
}
};
// in every other case, we're fine, since we've finished before the
// timeout event has occurred. call _internalDone where we will clear
// the timeout event.
return _internalDone();

// common completion function called by forked code above
function _internalDone() {

// clear any timeout handlers
if (self._timeoutHandlerId) {
clearTimeout(self._timeoutHandlerId);
self._timeoutHandlerId = null;
}

// if user called stop() sometime during last invocation, we're done!
// don't queue up another invocation.
if (self._active === false) {
self._stop();
} else {
// start invocation immediately if: the elapsedTime is greater than
// the interval, which means the last execution took longer than
// the interval itself. otherwise, subtract the time the previous
// invocation took.
var timeToInvocation = (elapsedTime >= interval) ?
0 : (interval - elapsedTime);

self._nextHandlerId = setTimeout(function _nextInvocation() {
self._execute();
}, timeToInvocation);
}
}
};


/**
Expand All @@ -199,10 +257,62 @@ Reissue.prototype._stop = function _stop() {
self._nextHandlerId = null;
}

// clear any timeouts scheduled
if (self._timeoutHandlerId) {
clearTimeout(self._timeoutHandlerId);
self._timeoutHandlerId = null;
}

self.emit('stop');
};


/**
* called when the interval function "times out", or in other words takes
* longer than then specified timeout interval. this blocks the next invocation
* of the interval function until user calls the callback on the timeout
* event.
* @private
* @method _onTimeout
* @return {undefined}
*/
Reissue.prototype._onTimeout = function _onTimeout() {

var self = this;

// before we do anything, see if we got stopped.
if (self._active === false) {
self._stop();
return;
}

self._timeoutHandlerId.___reissue.fired = true;

function _internalOnTimeoutDone() {
self._timeoutHandlerId.___reissue.done = true;
self.emit('_timeoutComplete');
}

// if user is listening to timeout event, fire the event and block on its
// completion.
if (self.listenerCount('timeout') > 0) {
self.emit('timeout', function onTimeoutHandlerComplete(cancel) {
_internalOnTimeoutDone();

// if cancel is true, clear the currently timed out invocation,
// schedule another one immediately.
if (cancel === true) {
clearTimeout(self._nextHandlerId);
self._execute();
}
});
}
// otherwise, we're done
else {
_internalOnTimeoutDone();
}
};

//------------------------------------------------------------------------------
// public methods
//------------------------------------------------------------------------------
Expand All @@ -216,9 +326,12 @@ Reissue.prototype._stop = function _stop() {
* @return {undefined}
*/
Reissue.prototype.start = function start(delay) {
var self = this;

assert.optionalNumber(delay);

var self = this;
var realDelay = (typeof delay === 'number' && delay >= 0) ? delay : 0;

// before starting, see if reissue is already active. if so, throw an
// error.
if (self._active === true) {
Expand All @@ -228,13 +341,9 @@ Reissue.prototype.start = function start(delay) {
// set the flag and off we go!
self._active = true;

if (typeof delay === 'number') {
self._nextHandlerId = setTimeout(function _nextInvocation() {
self._execute();
}, delay);
} else {
self._nextHandlerId = setTimeout(function _nextInvocation() {
self._execute();
}
}, realDelay);
};


Expand All @@ -245,6 +354,7 @@ Reissue.prototype.start = function start(delay) {
* @return {undefined}
*/
Reissue.prototype.stop = function stop() {

var self = this;

// NOTE: while the below if statements could be collapsed to be more more
Expand All @@ -258,7 +368,6 @@ Reissue.prototype.stop = function stop() {
// here, we are either:
// 1) queued up waiting for the next invocation
// 2) waiting for user supplied function to complete

if (self._inUserFunc === false) {
// case #1
// if we're just waiting for the next invocation, call stop now
Expand Down
82 changes: 61 additions & 21 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,27 +341,6 @@ describe('Reissue module', function() {
});


it('should start asynchronously after explicit 0 delay', function(done) {

var async = false;

var timer = reissue.create({
func: function(callback) {

// if async === false, this was called synchronously
assert.equal(async, true);
return done();
// no need to call reissue's callback here, as that will just
// invoke this function again and call done() which will fail
// the test.
},
interval: 100
});
timer.start(0);
async = true;
});


it('should not execute first invocation if stop was called',
function(done) {

Expand Down Expand Up @@ -457,4 +436,65 @@ describe('Reissue module', function() {
timer.stop();
}, 1000);
});


it('should emit stop, first invocation and timeout should never fire',
function(done) {

var timer = reissue.create({
func: function(callback) {
assert.fail('should not get here!');
return setTimeout(callback, 500);
},
interval: 100,
timeout: 200
});

timer.on('timeout', function(callback) {
assert.fail('should not get here!');
return callback();
});

timer.on('stop', done);

timer.start();
timer.stop();
});


it('should cancel next invocation via timeout callback', function(done) {

var timer = reissue.create({
func: function(callback) {
return setTimeout(callback, 500);
},
interval: 100,
timeout: 200
});

var start = Date.now();

timer.once('timeout', function(callback) {
timer.once('timeout', function(cb) {
// since we've thrown the first one away, and invoked it again
// immediately, we should be somewhere between 400-500ms
// elapsed
var elapsed = Date.now() - start;
assert.isAtLeast(elapsed, 400);
assert.isAtMost(elapsed, 500);
timer.stop();
return cb();
});

// true cancels the timed out invocation and schedules one
// immediately
return callback(true);
});

timer.on('stop', function() {
return done();
});

timer.start();
});
});

0 comments on commit 51ff1b7

Please sign in to comment.