60 lines
1.4 KiB
TypeScript
60 lines
1.4 KiB
TypeScript
type RetryOptions = {
|
|
maxRetries: number;
|
|
baseDelayMs: number;
|
|
maxDelayMs: number;
|
|
jitterFactor: number;
|
|
retryableErrors: RegExp[];
|
|
};
|
|
|
|
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
|
|
maxRetries: 3,
|
|
baseDelayMs: 2000,
|
|
maxDelayMs: 10000,
|
|
jitterFactor: 0.3,
|
|
retryableErrors: [
|
|
/timeout/i,
|
|
/ECONNRESET/,
|
|
/ETIMEDOUT/,
|
|
/ENOTFOUND/,
|
|
/exit code 1/,
|
|
/signal/,
|
|
/killed/
|
|
]
|
|
};
|
|
|
|
export async function withRetry<T>(
|
|
fn: () => Promise<T>,
|
|
options?: Partial<RetryOptions>
|
|
): Promise<T> {
|
|
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 0; attempt < opts.maxRetries; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
|
|
const isRetryable = opts.retryableErrors.some(
|
|
(pattern) => pattern.test(lastError!.message)
|
|
);
|
|
|
|
if (!isRetryable || attempt === opts.maxRetries - 1) {
|
|
throw lastError;
|
|
}
|
|
|
|
const baseDelay = opts.baseDelayMs * Math.pow(2, attempt);
|
|
const jitter = Math.random() * opts.jitterFactor * baseDelay;
|
|
const delay = Math.min(baseDelay + jitter, opts.maxDelayMs);
|
|
|
|
console.warn(
|
|
`[retry] Attempt ${attempt + 1}/${opts.maxRetries} failed, retrying in ${Math.round(delay)}ms: ${lastError.message}`
|
|
);
|
|
|
|
await Bun.sleep(delay);
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|