import * as plugins from '../plugins.js'; import type { IRetryOptions } from '../interfaces/index.js'; const defaultRetryOptions: Required = { maxRetries: 5, baseDelay: 1000, maxDelay: 16000, multiplier: 2, jitter: true, }; /** * Calculates the delay for a retry attempt using exponential backoff */ function calculateDelay( attempt: number, options: Required ): number { const delay = Math.min( options.baseDelay * Math.pow(options.multiplier, attempt), options.maxDelay ); if (options.jitter) { // Add random jitter of +/- 25% const jitterRange = delay * 0.25; return delay + (Math.random() * jitterRange * 2 - jitterRange); } return delay; } /** * Executes a function with retry logic using exponential backoff * * @param fn - The async function to execute * @param options - Retry configuration options * @returns The result of the function * @throws The last error if all retries fail */ export async function withRetry( fn: () => Promise, options?: IRetryOptions ): Promise { const opts: Required = { ...defaultRetryOptions, ...options }; let lastError: Error | undefined; for (let attempt = 0; attempt <= opts.maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if (attempt === opts.maxRetries) { break; } const delay = calculateDelay(attempt, opts); await plugins.smartdelay.delayFor(delay); } } throw lastError ?? new Error('Retry failed with unknown error'); } /** * Creates a retryable version of an async function * * @param fn - The async function to wrap * @param options - Retry configuration options * @returns A wrapped function that retries on failure */ export function createRetryable( fn: (...args: TArgs) => Promise, options?: IRetryOptions ): (...args: TArgs) => Promise { return (...args: TArgs) => withRetry(() => fn(...args), options); } /** * Retry decorator for class methods * Note: Use as a wrapper function since TC39 decorators have different semantics */ export function retryMethod>( target: T, methodName: keyof T & string, options?: IRetryOptions ): void { const originalMethod = target[methodName]; if (typeof originalMethod !== 'function') { throw new Error(`${methodName} is not a function`); } target[methodName] = createRetryable( originalMethod.bind(target) as (...args: unknown[]) => Promise, options ) as T[typeof methodName]; } export { defaultRetryOptions };