diff --git a/src/retrier.js b/src/retrier.js index efe23fd..4805618 100644 --- a/src/retrier.js +++ b/src/retrier.js @@ -1,5 +1,3 @@ -const { isNumber } = require('./utilities') - const createLinear = function (constants = {}) { const { m = 1, b = 0 } = constants @@ -27,49 +25,45 @@ const createExponential = function (constants = {}, m = 1) { module.exports.createExponential = createExponential /** - * @param {Function} - * @param {Function|Number} - * @param {Number} - * @param {Function} + * Wraps an `async` function for async retries as per the logic given in `curve`. + * Curve is expected to be a callback which will be called at each failed + * iteration with `err` (the error thrown by the wrapped function) and `count` + * which is the count of total attempts to `resolve` - it should either throw + * (should no more retries be required) or return an integer dictating the + * number of milliseconds to sleep before re-attempting resolution. + * + * @param {Function} fn + * @param {Function|Object} curve * @returns {Function} */ -const createRetrierFn = function (fn, curve = 2, limit = 2, shouldRetry = undefined) { - if (isNumber(curve)) { - limit = curve - curve = zero +const createRetrierFn = function (fn, curve) { + if (!curve) { + curve = function (err, count) { + if (count > 1) throw err + return 0 + } } - return function () { - const args = Array.prototype.slice.call(arguments) - - return new Promise(function (resolve, reject) { - (function recurse(attempt) { - function retry(error) { - const errorCount = attempt + 1 - - if (limit && errorCount >= limit) return reject(error) + return function (...args) { + let attempt = 0 - if (shouldRetry && !shouldRetry(error)) return reject(error) + const recurse = function (resolve, reject) { + return fn(...args).then(resolve).catch(function (err) { + let timeout - return recurse(errorCount) + try { + timeout = curve(err, ++attempt) + if (timeout <= 0) return recurse(resolve, reject) + return setTimeout(recurse, timeout, resolve, reject) + } catch (_err) { + return reject(_err) } + }) + } - setTimeout(function () { - try { - Promise - .resolve(fn.apply(null, args)) - .then(resolve) - .catch(function (asyncErr) { - return retry(asyncErr) - }) - } catch (syncErr) { - retry(syncErr) - } - }, curve(attempt)) - }(null, 0)) - }) + return new Promise(recurse) } } -module.exports.retry = createRetrierFn module.exports.createRetrierFn = createRetrierFn +module.exports.retry = createRetrierFn diff --git a/test/retrier.js b/test/retrier.js index ffb3043..04ed4b0 100644 --- a/test/retrier.js +++ b/test/retrier.js @@ -1,12 +1,8 @@ const test = require('ava') -const { - createLinear, - createExponential, - createRetrierFn, -} = require('../src/retrier') +const { createRetrierFn } = require('../src/retrier') test('createRetrierFn - retries async errors with delay', async function (t) { - const sleep = x => new Promise(r => setTimeout(r, x)) + const sleep = ms => new Promise(r => setTimeout(r, ms)) /** * Given 'i', create function that will return a promise @@ -22,146 +18,40 @@ test('createRetrierFn - retries async errors with delay', async function (t) { i-- throw new Error(i) } - return true - } - } - - // Will retry three times. - const succeeder = createRetrierFn(createFailer(2), () => 15, 3) - t.true(typeof succeeder === 'function') - - const start = new Date().getTime() - const success = await succeeder() - t.true(success) - t.true(new Date().getTime() - start >= 30) - const failer = createRetrierFn(createFailer(4), 2) - t.true(typeof succeeder === 'function') - - let failure - try { - failure = await failer() - } catch ({ message }) { - failure = +message // Coerce. + return i + } } - t.true(failure === 2) -}) - -test('createRetrierFn - retries sync errors', async function (t) { - const responses = [ - new Error('fail'), - true, - ] + // NOTE: This serves as an example of a curve function. + const createCurve = function (limit, ms) { + const curve = function (err, count) { + if (count > limit) throw err + return ms + } - function failOnce() { - const response = responses.shift() - if (response instanceof Error) throw response - return response + return curve } - const fn = createRetrierFn(failOnce, 2) - t.true(await fn()) -}) - -test('createRetrierFn - allows opt out of retries', async function (t) { - const shouldRetry = () => false - const responses = [ - new Error('fail'), - true, - ] + const succeeder = createRetrierFn(createFailer(2), createCurve(3, 200)) + t.true(typeof succeeder === 'function') - function failOnce() { - const response = responses.shift() - if (response instanceof Error) throw response - return response - } + const ts = new Date().getTime() - const fn = createRetrierFn(failOnce, 2, undefined, shouldRetry) - try { - await fn() - t.fail() - } catch (error) { - t.pass() - } -}) + const success = await succeeder() + t.true(success === 0) + t.true((new Date().getTime() - ts) >= 400) -test('createRetrierFn - allows indefinite retries', async function (t) { - const responses = [ - new Error('fail'), - new Error('fail'), - new Error('fail'), - new Error('fail'), - true, - ] + const failer = createRetrierFn(createFailer(5), createCurve(2, 200)) + t.true(typeof failer === 'function') - function failMany() { - const response = responses.shift() - if (response instanceof Error) throw response - return response - } + let value - const fn = createRetrierFn(failMany, 0, 0) try { - await fn() - t.pass() - } catch (error) { - t.fail() - } -}) - -test('createRetrierFn - supports curves', async function (t) { - const sleep = x => new Promise(r => setTimeout(r, x)) - - const cache = [] - const createFn = function () { - let failed = false - - const fn = async function (value) { - await sleep(100) - cache.push(value) - - if (!failed) { - failed = true - throw new Error('') - } - - return value - } - - return fn - } - - let f = createFn() - let fn = createRetrierFn(f, 2) - - t.true(await fn('x') === 'x') - t.true(cache.length === 2) - - cache.length = 0 - f = createFn() - fn = createRetrierFn(f, x => x, 2) - - t.true(await fn('x') === 'x') - t.true(cache.length === 2) -}) - -test('createRetrierFn - and inbuilt curves', async function (t) { - const range = function* (len) { - let count = 0 - while (count < len) { - yield count - count++ - } + await failer() + } catch ({ message }) { + value = +message // Coerce. } - let fx = createLinear({ m: 2, b: 0 }) - t.deepEqual([...range(4)].map(fx), [0, 2, 4, 6]) - - fx = createExponential({ a: 2, b: 1 }, 1) - t.deepEqual([...range(6)].map(fx), [1, 2, 4, 8, 16, 32]) - - fx = createExponential({ a: 2, b: 1 }, 1000) - t.deepEqual([...range(6)].map(fx), [1000, 2000, 4000, 8000, 16000, 32000]) + t.true(value === 2) }) -