2025-12-22 09:48:57 +01:00

92 lines
2.1 KiB
TypeScript

/**
* Retry utility with exponential backoff
*/
export interface RetryOptions {
maxRetries?: number
initialDelay?: number
maxDelay?: number
backoffMultiplier?: number
retryable?: (error: Error) => boolean
}
const DEFAULT_OPTIONS: Required<RetryOptions> = {
maxRetries: 3,
initialDelay: 1000,
maxDelay: 10000,
backoffMultiplier: 2,
retryable: () => true,
}
/**
* Retry a function with exponential backoff
*/
export async function retryWithBackoff<T>(
fn: () => Promise<T>,
options: RetryOptions = {}
): Promise<T> {
const opts = { ...DEFAULT_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))
// Check if error is retryable
if (!opts.retryable(lastError)) {
throw lastError
}
// Don't retry on last attempt
if (attempt === opts.maxRetries) {
throw lastError
}
// Calculate delay with exponential backoff
const delay = Math.min(
opts.initialDelay * Math.pow(opts.backoffMultiplier, attempt),
opts.maxDelay
)
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
throw lastError || new Error('Retry failed')
}
/**
* Check if an error is a network error that should be retried
*/
export function isRetryableNetworkError(error: Error): boolean {
// Network errors
if (error.message.includes('network') || error.message.includes('fetch')) {
return true
}
// Timeout errors
if (error.message.includes('timeout') || error.message.includes('timed out')) {
return true
}
// Connection errors
if (error.message.includes('ECONNRESET') || error.message.includes('ECONNREFUSED')) {
return true
}
// Rate limiting (429)
if (error.message.includes('429') || error.message.includes('rate limit')) {
return true
}
// Server errors (5xx)
if (error.message.includes('50')) {
return true
}
return false
}