Skip to content

Commit

Permalink
Merge ee5dfe6 into 162ace0
Browse files Browse the repository at this point in the history
  • Loading branch information
DonutEspresso committed Jan 24, 2017
2 parents 162ace0 + ee5dfe6 commit 5cf20b7
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 72 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'
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ reissue takes the following options to its `create()` method:

* `opts.func` {Function} the function to execute. This function is invoked with
a callback function as it's last parameter.
* `opts.interval` {Number | Function} the inteval in ms to execute the function,
or a function that returns an interval, allowing usage of a dynamic interval.
* `opts.interval` {Number | Function} the interval in ms to execute the
function, or a function that returns an interval, allowing usage of a dynamic
interval.
* `[opts.timeout]` {Number} an optional timeout in ms. if any invocation of the
the supplied func exceeds this timeout, the `timeout` event is fired.
* `[opts.context]` {Context} an optional `this` context for the function. use
this in lieu of native `bind()` if you are concerned about performance. reissue
uses `apply()` under the hood to do context/arg binding.
Expand Down Expand Up @@ -98,8 +101,16 @@ If your function returns an error to the callback, this event will be emitted.
The subscribed function will receive an error as it's only parameter.

### handler.on('stop', function() {...})
The stop event is emitted when reissue successfully completes any ongoing
function invocations and stops all future invocations.
When the `stop()` method is called, this event is emitted when either the
current invocation is successfully completed, or when the next scheduled
invocation is successfully cancelled. If the current invocation is "stuck" in
the sense that the callback never returns, the stop event will never fire.

### handler.on('timeout', function() {...})
If a `timeout` value is specified, this event will be fired when any given
invocation of the function exceeds the specified value. However, if your user
supplied function is synchronous, and never gives up the event loop, it is
possible that this event may never get fired.


## Contributing
Expand Down Expand Up @@ -127,6 +138,6 @@ make codestyle-fix

## License

Copyright (c) 2015 Alex Liu
Copyright (c) 2017 Alex Liu

Licensed under the MIT license.
142 changes: 106 additions & 36 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ var bind = require('./bind');
* @param {Object} opts.func an the function to execute
* @param {Object | Function} opts.interval the interval to execute at, or a
* function that returns an interval. function allows for dynamic intervals.
* @param {Number} [opts.timeout] an optional timeout value that causes the
* `timeout` event to be fired when a given invocation exceeds this value.
* function that returns an interval. function allows for dynamic intervals.
* @param {Object} [opts.context] the context to bind the function to
* @param {Object} [opts.args] any arguments to pass to the function
*/
Expand All @@ -34,6 +37,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 +80,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 +104,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 +112,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 +141,31 @@ 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
// start invocation timer
self._startTime = Date.now();
// set flag so we know we're currently in user supplied func
self._inUserFunc = true;
// execute their func on a setImmediate, such that we can schedule the
// timeout first. to be clear though, user func could be sync and our
// timeout may never fire.
setImmediate(function _executeImmediately() {
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
);
}
};

Expand Down Expand Up @@ -163,25 +195,40 @@ 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);
// 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);
}
}
};



/**
* internal implementation of stop. clears all timeout handlers and emits the
* stop event.
Expand All @@ -199,10 +246,34 @@ Reissue.prototype._stop = function _stop() {
self._nextHandlerId = null;
}

// no need to clear timeout handlers, as they're already cleared
// in _done before we get here.

// emit stop, and we're done!
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;

// we might have called stop during current invocation. emit timeout event
// only if we're still active.
if (self._active === true) {
self.emit('timeout');
}
};

//------------------------------------------------------------------------------
// public methods
//------------------------------------------------------------------------------
Expand All @@ -216,9 +287,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 +302,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 +315,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 +329,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

0 comments on commit 5cf20b7

Please sign in to comment.