101 lines
2.7 KiB
TypeScript
101 lines
2.7 KiB
TypeScript
|
|
import * as plugins from '../plugins.js';
|
||
|
|
import type { IRetryOptions } from '../interfaces/index.js';
|
||
|
|
|
||
|
|
const defaultRetryOptions: Required<IRetryOptions> = {
|
||
|
|
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<IRetryOptions>
|
||
|
|
): 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<T>(
|
||
|
|
fn: () => Promise<T>,
|
||
|
|
options?: IRetryOptions
|
||
|
|
): Promise<T> {
|
||
|
|
const opts: Required<IRetryOptions> = { ...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<TArgs extends unknown[], TResult>(
|
||
|
|
fn: (...args: TArgs) => Promise<TResult>,
|
||
|
|
options?: IRetryOptions
|
||
|
|
): (...args: TArgs) => Promise<TResult> {
|
||
|
|
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<T extends Record<string, unknown>>(
|
||
|
|
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<unknown>,
|
||
|
|
options
|
||
|
|
) as T[typeof methodName];
|
||
|
|
}
|
||
|
|
|
||
|
|
export { defaultRetryOptions };
|