(
operation: () => Promise<T>,
options: {
maxRetries?: number
retryIf?: (error: any) => boolean
onRetry?: (error: any, attempt: number) => void
retryDelayMs?: number
} = {},
)
| 1 | export const INITIAL_RETRY_DELAY = 1000 // 1 second |
| 2 | |
| 3 | export async function withRetry<T>( |
| 4 | operation: () => Promise<T>, |
| 5 | options: { |
| 6 | maxRetries?: number |
| 7 | retryIf?: (error: any) => boolean |
| 8 | onRetry?: (error: any, attempt: number) => void |
| 9 | retryDelayMs?: number |
| 10 | } = {}, |
| 11 | ): Promise<T> { |
| 12 | const { |
| 13 | maxRetries = 3, |
| 14 | retryIf = (error) => error?.type === 'APIConnectionError', |
| 15 | onRetry = () => {}, |
| 16 | retryDelayMs = INITIAL_RETRY_DELAY, |
| 17 | } = options |
| 18 | |
| 19 | let lastError: any = null |
| 20 | |
| 21 | for (let attempt = 0; attempt < maxRetries; attempt++) { |
| 22 | try { |
| 23 | return await operation() |
| 24 | } catch (error) { |
| 25 | lastError = error |
| 26 | |
| 27 | if (!retryIf(error) || attempt === maxRetries - 1) { |
| 28 | throw error |
| 29 | } |
| 30 | |
| 31 | onRetry(error, attempt + 1) |
| 32 | |
| 33 | // Exponential backoff with jitter (±20%) to prevent thundering herd |
| 34 | const baseDelayMs = retryDelayMs * Math.pow(2, attempt) |
| 35 | const jitter = 0.8 + Math.random() * 0.4 // Random multiplier between 0.8 and 1.2 |
| 36 | const delayMs = Math.round(baseDelayMs * jitter) |
| 37 | await new Promise((resolve) => setTimeout(resolve, delayMs)) |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | throw lastError |
| 42 | } |
| 43 | |
| 44 | export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) |
| 45 |
no test coverage detected