UNPKG

@mr-hooks/use-retry

Version:

A hook for auto retrying asynchronous operations with a backoff strategy

127 lines (119 loc) 6.05 kB
'use strict'; 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;