Skip to content

Commit 8e9fc4b

Browse files
committed
Merge pull request benbria#1 from benbria/retry
Add retry()
2 parents bedf25a + 0f7e4f5 commit 8e9fc4b

File tree

3 files changed

+144
-0
lines changed

3 files changed

+144
-0
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Then somewhere in your node.js application:
4242
* [`series`](#series)
4343
* [`timeout`](#timeout)
4444
* [`whilst`](#whilst), `doWhilst`
45+
* [`retry`](#retry)
4546

4647

4748
# Utilities
@@ -161,3 +162,37 @@ Example:
161162
.then(function(result) {
162163
// result will be 10 here.
163164
});
165+
166+
<a name="retry"/>
167+
### retry(options, fn)
168+
169+
Will continuously call `fn` until it returns a synchronous value, doesn't throw, or returns a Promise that resolves. It will be retried `options.times`. You can pass `{times: Infinity}` to retry indefinitely. The `fn` will be passed the `lastAttempt` object which is the Error object of the last attempt.
170+
171+
Options: `times` (Default=5) and `interval` (Default=0). `interval` is the time between retries in milliseconds. If the `options` argument is passed as just a number, only `times` will be set.
172+
173+
Examples:
174+
175+
var count = 0;
176+
promiseTools.retry({times: 4, interval: 5}, function(lastAttempt) {
177+
count++;
178+
if (count === 2) Promise.resolve(true);
179+
else Promise.reject(new Error('boom'));
180+
})
181+
.then(function(result) {
182+
// result will be `true` here.
183+
});
184+
185+
---------------------------------------------
186+
187+
var count = 0;
188+
promiseTools.retry(1, function(lastAttempt) {
189+
count++;
190+
if (count === 2) Promise.resolve(true);
191+
else Promise.reject(new Error('boom'));
192+
})
193+
.then(function(result) {
194+
// will not resolve.
195+
})
196+
.catch(function(err) {
197+
// err.message should be `boom` here.
198+
});

src/index.js

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

test/retry.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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(Infinity, 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((lastAttempt) => {
30+
try {
31+
expect(lastAttempt).to.be.instanceof(Error);
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+
});

0 commit comments

Comments
 (0)