42 lines
1.2 KiB
TypeScript
42 lines
1.2 KiB
TypeScript
export interface RetryOptions {
|
|
attempts?: number
|
|
delay?: number
|
|
factor?: number
|
|
maxDelay?: number
|
|
retryIf?: (error: unknown) => boolean
|
|
}
|
|
|
|
const TRANSIENT_MESSAGES = [
|
|
"load failed",
|
|
"network connection was lost",
|
|
"network request failed",
|
|
"failed to fetch",
|
|
"econnreset",
|
|
"econnrefused",
|
|
"etimedout",
|
|
"socket hang up",
|
|
]
|
|
|
|
function isTransientError(error: unknown): boolean {
|
|
if (!error) return false
|
|
const message = String(error instanceof Error ? error.message : error).toLowerCase()
|
|
return TRANSIENT_MESSAGES.some((m) => message.includes(m))
|
|
}
|
|
|
|
export async function retry<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
|
|
const { attempts = 3, delay = 500, factor = 2, maxDelay = 10000, retryIf = isTransientError } = options
|
|
|
|
let lastError: unknown
|
|
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
try {
|
|
return await fn()
|
|
} catch (error) {
|
|
lastError = error
|
|
if (attempt === attempts - 1 || !retryIf(error)) throw error
|
|
const wait = Math.min(delay * Math.pow(factor, attempt), maxDelay)
|
|
await new Promise((resolve) => setTimeout(resolve, wait))
|
|
}
|
|
}
|
|
throw lastError
|
|
}
|