From 23514a5b04eda33d45c969cd59523252dc7079ef Mon Sep 17 00:00:00 2001 From: tim-kos Date: Mon, 19 Oct 2026 17:23:16 +0200 Subject: [PATCH 1/2] Pass the attempt to every operation timeout and repeat the last forever timeout as a number Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- lib/retry_operation.js | 8 +++++-- test/integration/test-forever.js | 19 ++++++++++++++++ test/integration/test-retry-operation.js | 28 ++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 976f688..a68a6e5 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ Available options: If `forever` is true, the following changes happen: * `RetryOperation.errors()` will only output an array of one item: the last error. -* `RetryOperation` will repeatedly use the `timeouts` array. Once all of its timeouts have been used up, it restarts with the first timeout, then uses the second and so on. +* `RetryOperation` will keep retrying with the last timeout once all of the `timeouts` have been used up. #### retryOperation.errors() @@ -211,7 +211,7 @@ Defines the function `fn` that is to be retried and executes it for the first time right away. The `fn` function can receive an optional `currentAttempt` callback that represents the number of attempts to execute `fn` so far. Optionally defines `timeoutOps` which is an object having a property `timeout` in miliseconds and a property `cb` callback function. -Whenever your retry operation takes longer than `timeout` to execute, the timeout callback function `cb` is called. +Whenever your retry operation takes longer than `timeout` to execute, the timeout callback function `cb` is called with the current attempt number. #### retryOperation.try(fn) diff --git a/lib/retry_operation.js b/lib/retry_operation.js index 105ce72..f8c7a0f 100644 --- a/lib/retry_operation.js +++ b/lib/retry_operation.js @@ -62,7 +62,7 @@ RetryOperation.prototype.retry = function(err) { if (this._cachedTimeouts) { // retry forever, only keep last error this._errors.splice(0, this._errors.length - 1); - timeout = this._cachedTimeouts.slice(-1); + timeout = this._cachedTimeouts[this._cachedTimeouts.length - 1]; } else { return false; } @@ -107,8 +107,12 @@ RetryOperation.prototype.attempt = function(fn, timeoutOps) { var self = this; if (this._operationTimeoutCb) { this._timeout = setTimeout(function() { - self._operationTimeoutCb(); + self._operationTimeoutCb(self._attempts); }, self._operationTimeout); + + if (this._options.unref) { + this._timeout.unref(); + } } this._operationStart = new Date().getTime(); diff --git a/test/integration/test-forever.js b/test/integration/test-forever.js index b41307c..752776a 100644 --- a/test/integration/test-forever.js +++ b/test/integration/test-forever.js @@ -1,6 +1,7 @@ var common = require('../common'); var assert = common.assert; var retry = require(common.dir.lib + '/retry'); +var RetryOperation = require(common.dir.lib + '/retry_operation'); (function testForeverUsesFirstTimeout() { var operation = retry.operation({ @@ -22,3 +23,21 @@ var retry = require(common.dir.lib + '/retry'); } }); })(); + +(function testForeverRepeatsLastTimeout() { + var operation = new RetryOperation([1, 2], {forever: true}); + var originalSetTimeout = global.setTimeout; + var delays = []; + global.setTimeout = function(fn, delay) { + delays.push(delay); + return originalSetTimeout(function() {}, 0); + }; + + operation.attempt(function() {}); + for (var i = 0; i < 4; i++) { + operation.retry(new Error('foo')); + } + global.setTimeout = originalSetTimeout; + + assert.deepStrictEqual(delays, [1, 2, 2, 2]); +})(); diff --git a/test/integration/test-retry-operation.js b/test/integration/test-retry-operation.js index 1176d3e..f2f6cc3 100644 --- a/test/integration/test-retry-operation.js +++ b/test/integration/test-retry-operation.js @@ -291,3 +291,31 @@ var retry = require(common.dir.lib + '/retry'); fn(); })(); + +(function testOperationTimeoutReceivesAttempts() { + var operation = retry.operation([1]); + var timedOutAttempts = []; + + operation.attempt(function() {}, { + timeout: 1, + cb: function(attempts) { + timedOutAttempts.push(attempts); + if (operation.retry(new Error('timeout'))) { + return; + } + assert.deepStrictEqual(timedOutAttempts, [1, 2]); + } + }); +})(); + +(function testOperationTimeoutIsUnrefed() { + var operation = retry.operation({unref: true}); + + operation.attempt(function() {}, { + timeout: 60 * 1000, + cb: function() {} + }); + + assert.strictEqual(operation._timeout.hasRef(), false); + operation.stop(); +})(); From 58570fa5c09de96aad4f1c05c0d9f110806bbd3e Mon Sep 17 00:00:00 2001 From: tim-kos Date: Mon, 19 Oct 2026 17:37:26 +0200 Subject: [PATCH 2/2] Move the timeout tests into their own files and document timeouts()' forever option Co-Authored-By: Claude Opus 5.5 --- README.md | 1 + test/integration/test-forever-timeout.js | 21 ++++++++++++++ test/integration/test-forever.js | 19 ------------- test/integration/test-operation-timeout.js | 32 ++++++++++++++++++++++ test/integration/test-retry-operation.js | 28 ------------------- 5 files changed, 54 insertions(+), 47 deletions(-) create mode 100644 test/integration/test-forever-timeout.js create mode 100644 test/integration/test-operation-timeout.js diff --git a/README.md b/README.md index a68a6e5..4146d65 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,7 @@ milliseconds. If `options` is an array, a copy of that array is returned. * `minTimeout`: The number of milliseconds before starting the first retry. Default is `1000`. * `maxTimeout`: The maximum number of milliseconds between two retries. Default is `Infinity`. * `randomize`: Randomizes the timeouts by multiplying with a factor between `1` to `2`. Default is `false`. +* `forever`: Whether the timeouts are for an operation that retries forever. If so and `retries` is `0`, one timeout is still returned, so there is a delay to repeat. Default is `false`. The formula used to calculate the individual timeouts is: diff --git a/test/integration/test-forever-timeout.js b/test/integration/test-forever-timeout.js new file mode 100644 index 0000000..3af083f --- /dev/null +++ b/test/integration/test-forever-timeout.js @@ -0,0 +1,21 @@ +var common = require('../common'); +var assert = common.assert; +var RetryOperation = require(common.dir.lib + '/retry_operation'); + +(function testForeverRepeatsLastTimeout() { + var operation = new RetryOperation([1, 2], {forever: true}); + var originalSetTimeout = global.setTimeout; + var delays = []; + global.setTimeout = function(fn, delay) { + delays.push(delay); + return originalSetTimeout(function() {}, 0); + }; + + operation.attempt(function() {}); + for (var i = 0; i < 4; i++) { + operation.retry(new Error('foo')); + } + global.setTimeout = originalSetTimeout; + + assert.deepStrictEqual(delays, [1, 2, 2, 2]); +})(); diff --git a/test/integration/test-forever.js b/test/integration/test-forever.js index 752776a..b41307c 100644 --- a/test/integration/test-forever.js +++ b/test/integration/test-forever.js @@ -1,7 +1,6 @@ var common = require('../common'); var assert = common.assert; var retry = require(common.dir.lib + '/retry'); -var RetryOperation = require(common.dir.lib + '/retry_operation'); (function testForeverUsesFirstTimeout() { var operation = retry.operation({ @@ -23,21 +22,3 @@ var RetryOperation = require(common.dir.lib + '/retry_operation'); } }); })(); - -(function testForeverRepeatsLastTimeout() { - var operation = new RetryOperation([1, 2], {forever: true}); - var originalSetTimeout = global.setTimeout; - var delays = []; - global.setTimeout = function(fn, delay) { - delays.push(delay); - return originalSetTimeout(function() {}, 0); - }; - - operation.attempt(function() {}); - for (var i = 0; i < 4; i++) { - operation.retry(new Error('foo')); - } - global.setTimeout = originalSetTimeout; - - assert.deepStrictEqual(delays, [1, 2, 2, 2]); -})(); diff --git a/test/integration/test-operation-timeout.js b/test/integration/test-operation-timeout.js new file mode 100644 index 0000000..0978d98 --- /dev/null +++ b/test/integration/test-operation-timeout.js @@ -0,0 +1,32 @@ +var common = require('../common'); +var assert = common.assert; +var retry = require(common.dir.lib + '/retry'); + +(function testOperationTimeoutReceivesAttempts() { + var operation = retry.operation([1]); + var timedOutAttempts = []; + + operation.attempt(function() {}, { + timeout: 1, + cb: function(attempts) { + timedOutAttempts.push(attempts); + operation.retry(new Error('timeout')); + } + }); + + process.on('exit', function() { + assert.deepStrictEqual(timedOutAttempts, [1, 2]); + }); +})(); + +(function testOperationTimeoutIsUnrefed() { + var operation = retry.operation({unref: true}); + + operation.attempt(function() {}, { + timeout: 60 * 1000, + cb: function() {} + }); + + assert.strictEqual(operation._timeout.hasRef(), false); + operation.stop(); +})(); diff --git a/test/integration/test-retry-operation.js b/test/integration/test-retry-operation.js index f2f6cc3..1176d3e 100644 --- a/test/integration/test-retry-operation.js +++ b/test/integration/test-retry-operation.js @@ -291,31 +291,3 @@ var retry = require(common.dir.lib + '/retry'); fn(); })(); - -(function testOperationTimeoutReceivesAttempts() { - var operation = retry.operation([1]); - var timedOutAttempts = []; - - operation.attempt(function() {}, { - timeout: 1, - cb: function(attempts) { - timedOutAttempts.push(attempts); - if (operation.retry(new Error('timeout'))) { - return; - } - assert.deepStrictEqual(timedOutAttempts, [1, 2]); - } - }); -})(); - -(function testOperationTimeoutIsUnrefed() { - var operation = retry.operation({unref: true}); - - operation.attempt(function() {}, { - timeout: 60 * 1000, - cb: function() {} - }); - - assert.strictEqual(operation._timeout.hasRef(), false); - operation.stop(); -})();