Skip to content
36 changes: 28 additions & 8 deletions lib/runnable.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,43 @@ function Runnable(title, fn) {
utils.inherits(Runnable, EventEmitter);

/**
* Set & get timeout `ms`.
* Get current timeout value in msecs.
*
* @api private
* @param {number|string} ms
* @return {Runnable|number} ms or Runnable instance.
* @private
* @returns {number} current timeout threshold value
*/
/**
* @summary
* Set timeout threshold value (msecs).
*
* @description
* A string argument can use shorthand (e.g., "2s") and will be converted.
* The value will be clamped to range [<code>0</code>, <code>2^<sup>31</sup>-1</code>].
* If clamped value matches either range endpoint, timeouts will be disabled.
*
* @private
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value}
* @param {number|string} ms - Timeout threshold value.
* @returns {Runnable} this
* @chainable
*/
Runnable.prototype.timeout = function(ms) {
if (!arguments.length) {
return this._timeout;
}
// see #1652 for reasoning
if (ms === 0 || ms > Math.pow(2, 31)) {
this._enableTimeouts = false;
}
if (typeof ms === 'string') {
ms = milliseconds(ms);
}

// Clamp to range
var INT_MAX = Math.pow(2, 31) - 1;
var range = [0, INT_MAX];
ms = utils.clamp(ms, range);

// see #1652 for reasoning
if (ms === range[0] || ms === range[1]) {
this._enableTimeouts = false;
}
debug('timeout %d', ms);
this._timeout = ms;
if (this.timer) {
Expand Down
11 changes: 11 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,17 @@ exports.isPromise = function isPromise(value) {
return typeof value === 'object' && typeof value.then === 'function';
};

/**
* Clamps a numeric value to an inclusive range.
*
* @param {number} value - Value to be clamped.
* @param {numer[]} range - Two element array specifying [min, max] range.
* @returns {number} clamped value
*/
exports.clamp = function clamp(value, range) {
return Math.min(Math.max(value, range[0]), range[1]);
};

/**
* It's a noop.
* @api
Expand Down
86 changes: 76 additions & 10 deletions test/unit/runnable.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,84 @@ describe('Runnable(title, fn)', function() {
});

describe('#timeout(ms)', function() {
it('should set the timeout', function() {
var run = new Runnable();
run.timeout(1000);
assert(run.timeout() === 1000);
var MIN_TIMEOUT = 0;
var MAX_TIMEOUT = 2147483647; // INT_MAX (32-bit signed integer)

describe('when value is less than lower bound', function() {
it('should clamp to lower bound given numeric', function() {
var run = new Runnable();
run.timeout(-1);
assert(run.timeout() === MIN_TIMEOUT);
});
// :TODO: Our internal version of `ms` can't handle negative time,
// but package version can. Skip this check until that change is merged.
it.skip('should clamp to lower bound given timestamp', function() {
var run = new Runnable();
run.timeout('-1 ms');
assert(run.timeout() === MIN_TIMEOUT);
});
});
});

describe('#timeout(ms) when ms>2^31', function() {
it('should set disabled', function() {
var run = new Runnable();
run.timeout(1e10);
assert(run.enableTimeouts() === false);
describe('when value is equal to lower bound', function() {
it('should set the value and disable timeouts given numeric', function() {
var run = new Runnable();
run.timeout(MIN_TIMEOUT);
assert(run.timeout() === MIN_TIMEOUT);
assert(run.enableTimeouts() === false);
});
it('should set the value and disable timeouts given timestamp', function() {
var run = new Runnable();
run.timeout(MIN_TIMEOUT + 'ms');
assert(run.timeout() === MIN_TIMEOUT);
assert(run.enableTimeouts() === false);
});
});

describe('when value is within `setTimeout` bounds', function() {
var oneSecond = 1000;

it('should set the timeout given numeric', function() {
var run = new Runnable();
run.timeout(oneSecond);
assert(run.timeout() === oneSecond);
assert(run.enableTimeouts() === true);
});
it('should set the timeout given timestamp', function() {
var run = new Runnable();
run.timeout('1s');
assert(run.timeout() === oneSecond);
assert(run.enableTimeouts() === true);
});
});

describe('when value is equal to upper bound', function() {
it('should set the value and disable timeout given numeric', function() {
var run = new Runnable();
run.timeout(MAX_TIMEOUT);
assert(run.timeout() === MAX_TIMEOUT);
assert(run.enableTimeouts() === false);
});
it('should set the value and disable timeout given timestamp', function() {
var run = new Runnable();
run.timeout(MAX_TIMEOUT + 'ms');
assert(run.timeout() === MAX_TIMEOUT);
assert(run.enableTimeouts() === false);
});
});

describe('when value is greater than `setTimeout` limit', function() {
it('should clamp to upper bound given numeric', function() {
var run = new Runnable();
run.timeout(MAX_TIMEOUT + 1);
assert(run.timeout() === MAX_TIMEOUT);
assert(run.enableTimeouts() === false);
});
it('should clamp to upper bound given timestamp', function() {
var run = new Runnable();
run.timeout('24.9d'); // 2151360000ms
assert(run.timeout() === MAX_TIMEOUT);
assert(run.enableTimeouts() === false);
});
});
});

Expand Down