Skip to content

Commit f3cd1fd

Browse files
committed
Implement async's retry. Added some different features.
Followed async's api. Added ability to retry forever. Also allows the `fn` parameter to be synchronous or Promise based. async's version could only be callback based.
1 parent bedf25a commit f3cd1fd

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

src/index.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,57 @@ exports.doWhilst = (fn, test) => {
190190
};
191191
return exports.whilst(doTest, fn);
192192
};
193+
194+
exports.FOREVER = -1;
195+
196+
/*
197+
* keep calling `fn` until it returns a non-error value, doesn't throw, or returns a Promise that resolves. `fn` will be
198+
* attempted `times` many times before rejecting. If `times` is given as `exports.FOREVER`, then `retry` will attempt to
199+
* resolve forever (useful if you are just waiting for something to finish).
200+
* @param {Object|Number} options hash to provide `times` and `interval`. Defaults (times=5, interval=0). If this value
201+
* is a number, only `times` will be set.
202+
* @param {Function} fn the task/check to be performed. Can either return a synchronous value, throw an error, or
203+
* return a promise
204+
* @returns {Promise}
205+
*/
206+
exports.retry = (options, fn) => {
207+
let times = 5;
208+
let interval = 0;
209+
let attempts = [];
210+
let infinite = false;
211+
212+
if ('number' === typeof(options)) {
213+
times = options;
214+
}
215+
else if ('object' === typeof(options)) {
216+
if (options.times) times = parseInt(options.times, 10);
217+
if (options.interval) interval = parseInt(options.interval, 10);
218+
}
219+
else {
220+
throw new Error('Unsupported argument type for \'times\': ' + typeof(options));
221+
}
222+
223+
if (times === exports.FOREVER) infinite = true;
224+
225+
return new Promise((resolve, reject) => {
226+
let doIt = () => {
227+
Promise.resolve()
228+
.then(() => {
229+
return fn(attempts[attempts.length - 1]);
230+
})
231+
.then((results) => {
232+
if (results instanceof Error) throw results;
233+
else resolve(results);
234+
})
235+
.catch((err) => {
236+
attempts.push(err);
237+
if (!infinite && attempts.length === times) {
238+
reject(attempts);
239+
} else {
240+
setTimeout(doIt, interval);
241+
}
242+
});
243+
};
244+
doIt();
245+
});
246+
};

test/retry.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use strict"
2+
3+
let chai = require('chai');
4+
chai.use(require('chai-as-promised'));
5+
let expect = chai.expect;
6+
let promiseTools = require('../src');
7+
8+
let callCount = 0;
9+
let getTest = (times) => {
10+
return () => {
11+
callCount++;
12+
if (callCount === times) return callCount;
13+
else throw new Error('not done yet');
14+
}
15+
}
16+
17+
describe('retry', () => {
18+
beforeEach(() => {
19+
callCount = 0;
20+
});
21+
22+
it('should retry infinitely and resolve', () => {
23+
let retry = promiseTools.retry(-1, getTest(3));
24+
return expect(retry).to.eventually.equal(3);
25+
});
26+
27+
it('should reject', (done) => {
28+
let retry = promiseTools.retry(3, getTest(4));
29+
retry.catch((attempts) => {
30+
try {
31+
expect(attempts.length).to.eq(3);
32+
done();
33+
} catch (err) {
34+
done(err);
35+
}
36+
})
37+
});
38+
39+
[
40+
{options: {times: 5, interval: 10}, msg: 'times and interval'},
41+
{options: {times: 5}, msg: 'just times'},
42+
{options: {}, msg: 'neither times nor interval'}
43+
].forEach((args) => {
44+
it(`should accept first argument as an options hash with ${args.msg}`, () => {
45+
let retry = promiseTools.retry(args.options, getTest(5));
46+
return expect(retry).to.eventually.equal(5);
47+
});
48+
});
49+
50+
it('should return an error with invalid options argument', () => {
51+
let p = Promise.resolve().then(() => {
52+
// had to do this for some reason. Otherwise `chai-as-promised` always passed erroneously.
53+
try {
54+
return promiseTools.retry(undefined, getTest(1));
55+
} catch (err) {
56+
throw err;
57+
}
58+
});
59+
expect(p).to.eventually.be.rejectedWith('Unsupported argument type for \'times\': undefined');
60+
});
61+
62+
it('should reject when an Error value is resolved', () => {
63+
let retry = promiseTools.retry(1, () => {
64+
return new Error('boom');
65+
});
66+
return expect(retry).to.eventually.be.rejectedWith('boom');
67+
});
68+
69+
});

0 commit comments

Comments
 (0)