import type { AnyFunction, CommonLogger, ErrorData } from '..' import { _errorDataAppend, _since, pDelay, pTimeout } from '..' export interface PRetryOptions { /** * If set - will be included in the error message. * Can be used to identify the place in the code that failed. */ name?: string /** * Timeout for each Try, in milliseconds. * * Defaults to no timeout. */ timeout?: number /** * How many attempts to try. * First attempt is not a retry, but "initial try". It still counts. * maxAttempts of 4 will be 1 try and 3 retries. * * @default 4 */ maxAttempts?: number /** * @default 1000 ms */ delay?: number /** * @default 2 */ delayMultiplier?: number /** * Called on every retry (since 2nd attempt, cause 1st attempt is not a retry). * * True - keep retrying. * False - stop retrying and return immediately. * * @default () => true */ predicate?: (err: Error, attempt: number, maxAttempts: number) => boolean /** * Log the first attempt (which is not a "retry" yet). * * @default false */ logFirstAttempt?: boolean /** * Log retries - attempts that go after the first one. * * @default true */ logRetries?: boolean /** * @default false */ logSuccess?: boolean /** * @default true */ logFailures?: boolean /** * @default false */ logAll?: boolean /** * @default false */ logNone?: boolean /** * Default to `console` */ logger?: CommonLogger /** * Will be merged with `err.data` object. */ errorData?: ErrorData } /** * Returns a Function (!), enhanced with retry capabilities. * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try. */ export function pRetryFn(fn: T, opt: PRetryOptions = {}): T { return async function pRetryFunction(this: any, ...args: any[]) { return await pRetry(() => fn.call(this, ...args), opt) } as any } export async function pRetry( fn: (attempt: number) => Promise, opt: PRetryOptions = {}, ): Promise { const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name, timeout, } = opt const fakeError = timeout ? new Error('TimeoutError') : undefined let { logFirstAttempt = false, logRetries = true, logFailures = false, logSuccess = false } = opt if (opt.logAll) { logSuccess = logFirstAttempt = logRetries = logFailures = true } if (opt.logNone) { logSuccess = logFirstAttempt = logRetries = logFailures = false } const fname = name || fn.name || 'pRetry function' let delay = initialDelay let attempt = 0 /* eslint-disable no-constant-condition */ while (true) { const started = Date.now() try { attempt++ if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) { logger.log(`${fname} attempt #${attempt}...`) } let result: any if (timeout) { result = await pTimeout(async () => await fn(attempt), { timeout, name: fname, errorData: opt.errorData, fakeError, }) } else { result = await fn(attempt) } if (logSuccess) { logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`) } return result } catch (err) { if (logFailures) { logger.warn(`${fname} attempt #${attempt} error in ${_since(started)}:`, err) } if (attempt >= maxAttempts || (predicate && !predicate(err as Error, attempt, maxAttempts))) { // Give up throw _errorDataAppend(err, opt.errorData) } // Retry after delay delay *= delayMultiplier await pDelay(delay) // back to while(true) loop } } }