rx-player
Version:
Canal+ HTML5 Video Player
101 lines (95 loc) • 3.63 kB
text/typescript
import getFuzzedDelay from "./get_fuzzed_delay";
import isNullOrUndefined from "./is_null_or_undefined";
import sleep from "./sleep";
import type { CancellationSignal } from "./task_canceller";
/**
* Retry the given Promise (if it rejects) with an exponential
* backoff.
* The backoff behavior can be tweaked through the options given.
*
* @param {Function} runProm
* @param {Object} options - Configuration object.
* This object contains the following properties:
*
* - retryDelay {Number} - The initial delay, in ms.
* This delay will be fuzzed to fall under the range +-30% each time a new
* retry is done.
* Then, this delay will be multiplied by 2^(n-1), n being the counter of
* retry we performed (beginning at 1 for the first retry).
*
* - totalRetry {Number} - The amount of time we should retry. 0
* means no retry, 1 means a single retry, Infinity means infinite retry
* etc.
* If the Promise still rejects after this number of retry, the error will
* be throwed through the returned Promise.
*
* - shouldRetry {Function|undefined} - Function which will receive the
* error each time it fails, and should return a boolean. If this boolean
* is false, the error will be directly thrown (without anymore retry).
*
* - onRetry {Function|undefined} - Function which will be triggered at
* each retry. Will receive two arguments:
* 1. The error
* 2. The current retry count, beginning at 1 for the first retry
*
* @param {Object} cancelSignal
* @returns {Promise}
* TODO Take errorSelector out. Should probably be entirely managed in the
* calling code via a catch (much simpler to use and to understand).
*/
export default function retryPromiseWithBackoff<T>(
runProm: () => Promise<T>,
options: IBackoffOptions,
cancelSignal: CancellationSignal,
): Promise<T> {
const { baseDelay, maxDelay, totalRetry, shouldRetry, onRetry } = options;
let retryCount = 0;
return iterate();
async function iterate(): Promise<T> {
if (cancelSignal.cancellationError !== null) {
throw cancelSignal.cancellationError;
}
try {
const res = await runProm();
return res;
} catch (error) {
if (cancelSignal.cancellationError !== null) {
throw cancelSignal.cancellationError;
}
if (
(!isNullOrUndefined(shouldRetry) && !shouldRetry(error)) ||
retryCount++ >= totalRetry
) {
throw error;
}
if (typeof onRetry === "function") {
onRetry(error, retryCount);
}
const delay = getRetryDelay(baseDelay, retryCount, maxDelay);
await sleep(delay);
const res = iterate();
return res;
}
}
}
/**
* Get the delay that should be applied to the following retry, it depends on the base delay
* and is increaser for with the retry count. The result is ceiled by the maxDelay.
* @param baseDelay delay after wich the first request is retried after a failure
* @param retryCount count of retries
* @param maxDelay maximum delay
* @returns the delay that should be applied to the following retry
*/
export function getRetryDelay(baseDelay: number, retryCount: number, maxDelay: number) {
const delay = baseDelay * Math.pow(2, retryCount - 1);
const fuzzedDelay = getFuzzedDelay(delay);
return Math.min(fuzzedDelay, maxDelay);
}
export interface IBackoffOptions {
baseDelay: number;
maxDelay: number;
totalRetry: number;
shouldRetry?: (error: unknown) => boolean;
errorSelector?: (error: unknown, retryCount: number) => Error;
onRetry?: (error: unknown, retryCount: number) => void;
}