@mr-hooks/use-retry
Version:
A hook for auto retrying asynchronous operations with a backoff strategy
127 lines (119 loc) • 6.05 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
/**
* makeSimpleBackoffStrategy will make a backoff strategy that returns the passed time out for
* the passed number of retries
* @param {number} timeout - the number of ms to be returned for each backoff
* @param {number} maxRetries - the maximum number of times to retry before abandoning the retry
*/
var makeSimpleBackoffStrategy = function (_a) {
var timeout = _a.timeout, maxRetries = _a.maxRetries;
return (React.useCallback(function (retryCount) { return (retryCount < maxRetries ? timeout : -1); }, []));
};
var fib = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181];
var maxFib = fib.length;
/**
* makeFibonacciBackoffStrategy will make a backoff strategy that returns a fibonacci sequence.
* The sequence will be multiplied the passed `timeoutMultiplier`, allowing you to scale the
* backoff by some number.
* This implementation only goes up to `4181`, from then on it repeats that number
* This implementation uses a precalculated list of
* fibonacci numbers for cpu efficiency at the cost of memory
* @param {number} timeoutMultiplier [timeoutMultiplier=1000] - The multiplier to the fibonacci
* sequence, defaults to `1000` resulting in a fibonacci sequence in seconds
* @param {number} maxRetries - the maximum number of times to retry before abandoning the retry
*/
var makeFibonacciBackoffStrategy = function (_a) {
var maxRetries = _a.maxRetries, _b = _a.timeoutMultiplier, timeoutMultiplier = _b === void 0 ? 1000 : _b;
return (React.useCallback(function (retryCount) {
if (retryCount < maxRetries) {
if (retryCount < maxFib) {
return fib[retryCount] * timeoutMultiplier;
}
// once we are out of pre-calculated numbers, just use the last one
// using the default timeoutMultiplier this is already over 1 hour
return fib[maxFib - 1] * timeoutMultiplier;
}
return -1;
}, []));
};
/**
* makeExponentialBackoffStrategy will make a backoff strategy that performs the `base`
* to the power of the (retryCount + offset), then multiplied by the timeoutMultiplier
* until maxRetries is met.
* By default, it will produce powers of 2 in hundreds of ms starting at 400ms
* Note: Even with very small bases, this function grows very fast.
* @param {number} base [base=2] - the base of the exponent
* @param {number} offset [offset=2] - the offset added to `retryCount`
* ensuring the first value is not `1`
* @param {number} timeoutMultiplier [timeoutMultiplier=100] - The multiplier to the exponential
* sequence, defaults to `100` resulting in an exponential sequence in hundreds of ms
* @param {number} maxRetries - the maximum number of times to retry before abandoning the retry
*/
var makeExponentialBackoffStrategy = function (_a) {
var maxRetries = _a.maxRetries, _b = _a.base, base = _b === void 0 ? 2 : _b, _c = _a.offset, offset = _c === void 0 ? 2 : _c, _d = _a.timeoutMultiplier, timeoutMultiplier = _d === void 0 ? 100 : _d;
return (React.useCallback(function (retryCount) { return (retryCount < maxRetries ? (Math.pow(base, (retryCount + offset))) * timeoutMultiplier : -1); }, []));
};
var IDLE = Symbol('status: Idle');
var BACKOFF = Symbol('status: Backoff');
var ABANDONED = Symbol('status: Abandoned');
var SUCCEEDED = Symbol('status: Succeeded');
var RUNNING = Symbol('status: Running');
/**
* useRetry acts like useState, but keeps the url in sync.
*/
var useRetry = function (_a) {
var isFailing = _a.isFailing, isSucceeded = _a.isSucceeded, isRunning = _a.isRunning, retry = _a.retry, _b = _a.backoffStrategy, backoffStrategy = _b === void 0 ? makeSimpleBackoffStrategy({ timeout: 1000, maxRetries: 5 }) : _b;
// Use refs so that this hook never causes re-renders
var retryCountRef = React.useRef(0);
var statusRef = React.useRef(IDLE);
// if this hook in unmounted while the retry is queued,
// it will call a function that is no-longer 'valid'
// so we MUST cancel that timeout
var timeoutHandle = React.useRef(undefined);
if (isSucceeded) {
// The caller is telling us that the call has succeeded, reset and do nothing
retryCountRef.current = 0;
statusRef.current = SUCCEEDED;
}
else if (isRunning) {
// While the caller is loading, we also dont need to do anything
statusRef.current = RUNNING;
}
else if (isFailing && statusRef.current !== BACKOFF && statusRef.current !== ABANDONED) {
// The call has failed, and we are not already retrying,
// check the backoff strategy then queue up the retry
var nextTimeout = backoffStrategy(retryCountRef.current);
if (nextTimeout >= 0) {
retryCountRef.current += 1;
statusRef.current = BACKOFF;
timeoutHandle.current = setTimeout(function () {
timeoutHandle.current = undefined;
retry();
}, nextTimeout);
}
else {
statusRef.current = ABANDONED;
// Also reset the retry count, so that if the call goes loading again,
// the backoff strategy is reset
retryCountRef.current = 0;
}
}
// clear the timeout on unmount if its running
React.useEffect(function () { return function () {
if (timeoutHandle.current !== undefined) {
clearTimeout(timeoutHandle.current);
}
}; }, []);
return { retryCount: retryCountRef.current, status: statusRef.current };
};
exports.ABANDONED = ABANDONED;
exports.BACKOFF = BACKOFF;
exports.IDLE = IDLE;
exports.RUNNING = RUNNING;
exports.SUCCEEDED = SUCCEEDED;
exports.default = useRetry;
exports.makeExponentialBackoffStrategy = makeExponentialBackoffStrategy;
exports.makeFibonacciBackoffStrategy = makeFibonacciBackoffStrategy;
exports.makeSimpleBackoffStrategy = makeSimpleBackoffStrategy;