export interface RetryOptions {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
/** Called on each retry with (error, attempt) */
onRetry?: (error: unknown, attempt: number) => void;
}
const defaults: RetryOptions = {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 10000,
};
export async function withRetry<T>(
fn: () => Promise<T>,
opts: Partial<RetryOptions> = {}
): Promise<T> {
const { maxRetries, baseDelayMs, maxDelayMs, onRetry } = { ...defaults, ...opts };
let lastError: unknown;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
if (attempt === maxRetries) break;
const delay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
const jitter = delay * (0.5 + Math.random() * 0.5);
onRetry?.(err, attempt + 1);
await new Promise((r) => setTimeout(r, jitter));
}
}
throw lastError;
}