import {getTimer, TIMER_WAIT} from '../internal/function/timer'; import {isPlainObject} from '../internal/is'; import type {GenericAsyncCallback, GenericCallback} from '../models'; // #region Types /** * An error thrown when a retry fails */ export class RetryError extends Error { constructor( message: string, readonly original: unknown, ) { super(message); this.name = RETRY_ERROR_NAME; } } export type RetryOptions = { delay?: number; times?: number; when?: (error: unknown) => boolean; }; // #endregion // #region Functions /** * Retry a callback a specified number of times, with a delay between attempts * * _Available as `asyncRetry` and `retry.async`_ * * @param callback Callback to retry * @param options Retry options * @returns Callback result */ async function asyncRetry( callback: Callback, options?: RetryOptions, ): Promise>>; /** * Retry a callback a specified number of times, with a delay between attempts * * _Available as `asyncRetry` and `retry.async`_ * * @param callback Callback to retry * @param options Retry options * @returns Callback result */ async function asyncRetry( callback: Callback, options?: RetryOptions, ): Promise>; /** * Retry a callback a specified number of times, with a delay between attempts * * _Available as `asyncRetry` and `retry.async`_ * * @param callback Callback to retry * @param options Retry options * @returns Callback result */ async function asyncRetry( callback: Callback, options?: RetryOptions, ): Promise> { if (typeof callback !== 'function') { throw new TypeError(RETRY_MESSAGE_EXPECTATION); } async function handle(): Promise { try { const result = await callback(); resolver(result); } catch (error) { if (attempts >= times || !when(error)) { rejector(new RetryError(RETRY_MESSAGE_FAILED, error)); } else { attempts += 1; void timer(); } } } const {delay, times, when} = getRetryOptions(options); const timer = getTimer(TIMER_WAIT, handle, delay); let attempts = 0; let rejector: (reason?: unknown) => void; let resolver: (value: Awaited>) => void; return new Promise>>((resolve, reject) => { rejector = reject; resolver = resolve; void handle(); }); } function getRetryNumber(value?: unknown): number { return typeof value === 'number' && value > 0 ? value : 0; } function getRetryOptions(input?: RetryOptions): Required { const options = isPlainObject(input) ? input : {}; return { delay: getRetryNumber(options.delay), times: getRetryNumber(options.times), when: typeof options.when === 'function' ? options.when : shouldRetry, }; } /** * Retry a callback a specified number of times * * @param callback Callback to retry * @param options Retry options * @returns Callback result */ export function retry( callback: Callback, options?: Omit, ): ReturnType { if (typeof callback !== 'function') { throw new TypeError(RETRY_MESSAGE_EXPECTATION); } const {times, when} = getRetryOptions(options); let last: unknown; for (let index = 0; index <= times; index += 1) { try { const result = callback(); return result; } catch (error) { if (index >= times || !when(error)) { last = error; break; } } } throw new RetryError(RETRY_MESSAGE_FAILED, last); } retry.async = asyncRetry; function shouldRetry(): boolean { return true; } // #endregion // #region Variables const RETRY_ERROR_NAME = 'RetryError'; const RETRY_MESSAGE_EXPECTATION = 'Retry expected a function'; const RETRY_MESSAGE_FAILED = 'Retry failed'; // #endregion