Skip to content

Commit

Permalink
feat: initialDelay before retry execution (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
nbry authored Apr 29, 2022
1 parent a5a4559 commit c44a956
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 14 deletions.
20 changes: 18 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ const getOpt = function(option, _default) {
*
* @param {Object} context the context the wrapped function is called in
* @param {Function} context.fn the wrapped function
* @param {...*} context.args the arguments the wrapped function was called with
* @param {*} context.fnThis the `this` originally bound to `fn`
* @param {Number} context.retries # of times to retry the wrapped function
* @param {Number} context.initialDelay time to wait before making attempts
* @param {Number} context.timeout time to wait between retries (in ms)
* @param {Number} context.factor the exponential scaling factor
* @param {function} options.shouldRetry - Invoked with the thrown error,
Expand All @@ -40,9 +40,16 @@ const execute = async function(context) {
*
* In order to achieve this, retries is incremented at the end of each loop
* iteration as opposed to the beginning of each loop iteration.
*
* Setting an `initialDelay` pauses execution before the loop
*/
let retries = 0;

// Initial Delay
if (context.initialDelay !== 0) {
await new Promise((resolve) => setTimeout(resolve, context.initialDelay));
}

// eslint-disable-next-line no-constant-condition
while (true) {
/* eslint-disable no-await-in-loop */
Expand Down Expand Up @@ -73,6 +80,7 @@ const execute = async function(context) {
*/
const delay = context.timeout * Math.pow(context.factor, retries);

/* eslint-disable-next-line max-len*/
const msg = `retrying function ${context.fnName} in ${delay} ms : attempts: ${attempts}`;
context.log(msg);

Expand All @@ -89,7 +97,9 @@ const execute = async function(context) {
* @type {Object}
* @property {Number} [options.retries=3] Number of times to retry a wrapped
* function
* @property {Number} [options.timeout=300] Amount of time to wait between
* @property {Number} [options.initialDelay=0] Amount of time (ms) to wait before
* any function attempts
* @property {Number} [options.timeout=300] Amount of time (ms) to wait between
* retries
* @property {Number} [options.factor=2] The exponential factor to scale the
* timeout by every retry iteration. For example: with a factor of 2 and a
Expand Down Expand Up @@ -121,6 +131,7 @@ const retryify = function(options = {}) {
}

options.retries = getOpt(options.retries, 3);
options.initialDelay = getOpt(options.initialDelay, 0);
options.timeout = getOpt(options.timeout, 300);
options.factor = getOpt(options.factor, 2);
options.log = getOpt(options.log, function() {
Expand All @@ -146,6 +157,10 @@ const retryify = function(options = {}) {
}

const retries = getOpt(innerOptions.retries, options.retries);
const initialDelay = getOpt(
innerOptions.initialDelay,
options.initialDelay,
);
const timeout = getOpt(innerOptions.timeout, options.timeout);
const factor = getOpt(innerOptions.factor, options.factor);
const log = getOpt(innerOptions.log, options.log);
Expand All @@ -160,6 +175,7 @@ const retryify = function(options = {}) {
fnThis: this,
args,
retries,
initialDelay,
timeout,
factor,
shouldRetry,
Expand Down
11 changes: 6 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ object and the returned function is what is supposed to take the function
to retry.


| Param | Type | Description |
| --- | --- | --- |
| [options] | [<code>Options</code>](#Options) | Optional configuration object |
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| [options] | [<code>Options</code>](#Options) | <code>{}</code> | Optional configuration object |

<a name="retryify..retryWrapper"></a>

Expand All @@ -88,9 +88,10 @@ basis.
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| [options.retries] | <code>Number</code> | <code>3</code> | Number of times to retry a wrapped function |
| [options.timeout] | <code>Number</code> | <code>300</code> | Amount of time to wait between retries |
| [options.initialDelay] | <code>Number</code> | <code>0</code> | Amount of time (ms) to wait before any function attempts |
| [options.timeout] | <code>Number</code> | <code>300</code> | Amount of time (ms) to wait between retries |
| [options.factor] | <code>Number</code> | <code>2</code> | The exponential factor to scale the timeout by every retry iteration. For example: with a factor of 2 and a timeout of 100 ms, the first retry will fire after 100 ms, the second after 200 ms, the third after 400 ms, etc.... The formula used to calculate the delay between each retry: ```timeout * Math.pow(factor, attempts)``` |
| [options.errors] | <code>Error</code> \| <code>Array.&lt;Error&gt;</code> | <code>Error</code> | A single Error or an array Errors that trigger a retry when caught |
| [options.shouldRetry] | <code>function</code> | <code>() &#x3D;&gt; true</code> | Invoked with the thrown error, retryify will retry if this method returns true. |
| [options.log] | <code>function</code> | | Logging function that takes a message as |


Expand Down
54 changes: 47 additions & 7 deletions test/unit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const delay = (time) => new Promise((resolve) => setTimeout(resolve, time));

const retryify = retryLib({
retries: 2,
initialDelay: 0,
timeout: 5, // ms
factor: 1.5,
});
Expand All @@ -25,10 +26,10 @@ test('function passed to setup function', function(t) {

test('no times, standard fn', async function(t) {
const addABC = retryify(
{retries: 1},
function(a, b, c) {
return a + b + c;
},
{retries: 0},
);

const sum = await addABC(1, 2, 3);
Expand All @@ -37,12 +38,12 @@ test('no times, standard fn', async function(t) {

test('no times, promise fn', async function(t) {
const addABC = retryify(
{retries: 0},
function(a, b, c) {
return delay(5).then(function() {
return a + b + c;
});
},
{retries: 0},
);

const sum = await addABC(1, 2, 3);
Expand All @@ -53,6 +54,7 @@ test('once, error on first call, standard fn', async function(t) {
let retries = 1;

const addFail = retryify(
{retries},
function(a, b, c) {
if (retries > 0) {
retries -= 1;
Expand All @@ -61,7 +63,6 @@ test('once, error on first call, standard fn', async function(t) {
return a + b + c;
}
},
{retries},
);

const sum = await addFail(1, 2, 3);
Expand All @@ -72,6 +73,7 @@ test('once, error on first call, promise fn', async function(t) {
let retries = 1;

const addFail = retryify(
{retries},
function(a, b, c) {
return delay(5).then(function() {
if (retries > 0) {
Expand All @@ -82,7 +84,6 @@ test('once, error on first call, promise fn', async function(t) {
}
});
},
{retries},
);

const sum = await addFail(1, 2, 3);
Expand All @@ -93,6 +94,7 @@ test('twice, error on first call, promise fn', async function(t) {
let retries = 2;

const addFail = retryify(
{retries},
function(a, b, c) {
return delay(5).then(function() {
if (retries > 0) {
Expand All @@ -103,7 +105,6 @@ test('twice, error on first call, promise fn', async function(t) {
}
});
},
{retries},
);

const sum = await addFail(1, 2, 3);
Expand All @@ -114,12 +115,12 @@ test('always error, promise fn', async function(t) {
const retries = 2;

const fail = retryify(
{retries},
function() {
return delay(5).then(function() {
throw new Error('Fail!');
});
},
{retries},
);

const err = await t.throwsAsync(fail());
Expand All @@ -128,12 +129,12 @@ test('always error, promise fn', async function(t) {

test('retries but never error, promise fn', async function(t) {
const addABC = retryify(
{retries: 3},
function(a, b, c) {
return delay(5).then(function() {
return a + b + c;
});
},
{retries: 3},
);

const sum = await addABC(1, 2, 3);
Expand Down Expand Up @@ -219,3 +220,42 @@ test('log should get called on retry', async function(t) {
await t.throwsAsync(fail());
t.true(wasCalled, 'mockLog should get called at some point.');
});

/**
* Test state before and after initialDelay
*
* Time(seconds): 0s . . . 1s . . . 2s . . . 3s
* areWeThereYet: f t
* Testing checkpoints: * * * * *
*/

test('initial delay', async function(t) {
const options = {
retries: 0,
initialDelay: 1500,
};

let areWeThereYet = false;

const weArrived = () => {
areWeThereYet = true;
};

retryify(options, weArrived)();

t.false(areWeThereYet); // 0 sec

await delay(500); // 0.5 sec
t.false(areWeThereYet);

await delay(500); // 1 sec
t.false(areWeThereYet);

await delay(250); // 1.25 sec
t.false(areWeThereYet);

// After 1.5 seconds, we are finally there
await delay(500); // 1.75
t.true(areWeThereYet);

});

0 comments on commit c44a956

Please sign in to comment.