| |
| |
|
|
| interface HttpError extends Error { |
| response?: { |
| status: number; |
| statusText: string; |
| }; |
| } |
|
|
| export interface RetryOptions { |
| maxRetries?: number; |
| initialDelay?: number; |
| maxDelay?: number; |
| backoffMultiplier?: number; |
| shouldRetry?: (error: Error | HttpError) => boolean; |
| } |
|
|
| const defaultOptions: Required<RetryOptions> = { |
| maxRetries: 3, |
| initialDelay: 1000, |
| maxDelay: 30000, |
| backoffMultiplier: 2, |
| shouldRetry: (error) => { |
| |
| const httpError = error as HttpError; |
| if (!httpError.response) return true; |
| const status = httpError.response.status; |
| return status >= 500 && status < 600; |
| }, |
| }; |
|
|
| function calculateDelay(attempt: number, options: Required<RetryOptions>): number { |
| const delay = options.initialDelay * Math.pow(options.backoffMultiplier, attempt); |
| const jitter = Math.random() * 0.3 * delay; |
| return Math.min(delay + jitter, options.maxDelay); |
| } |
|
|
| export async function withRetry<T>( |
| fn: () => Promise<T>, |
| options: RetryOptions = {} |
| ): Promise<T> { |
| const opts = { ...defaultOptions, ...options }; |
| let lastError: Error | HttpError | unknown; |
|
|
| for (let attempt = 0; attempt <= opts.maxRetries; attempt++) { |
| try { |
| return await fn(); |
| } catch (error) { |
| lastError = error; |
|
|
| |
| if (attempt === opts.maxRetries || !opts.shouldRetry(error as Error)) { |
| throw error; |
| } |
|
|
| |
| const delay = calculateDelay(attempt, opts); |
| await new Promise(resolve => setTimeout(resolve, delay)); |
| } |
| } |
|
|
| throw lastError; |
| } |
|
|
| |
| export async function fetchWithRetry( |
| url: string, |
| options?: RequestInit, |
| retryOptions?: RetryOptions |
| ): Promise<Response> { |
| return withRetry(async () => { |
| const response = await fetch(url, options); |
|
|
| if (!response.ok) { |
| const error = new Error(`HTTP ${response.status}: ${response.statusText}`) as HttpError; |
| error.response = { status: response.status, statusText: response.statusText }; |
| throw error; |
| } |
|
|
| return response; |
| }, retryOptions); |
| } |
|
|