58 行
1.5 KiB
TypeScript
58 行
1.5 KiB
TypeScript
export interface RetryOptions {
|
|
maxAttempts?: number
|
|
baseDelayMs?: number
|
|
maxDelayMs?: number
|
|
jitterRatio?: number
|
|
signal?: AbortSignal
|
|
shouldRetry?: (error: unknown) => boolean
|
|
}
|
|
|
|
function abortError(): Error {
|
|
const error = new Error('Operation aborted')
|
|
error.name = 'AbortError'
|
|
return error
|
|
}
|
|
|
|
function wait(delayMs: number, signal?: AbortSignal): Promise<void> {
|
|
if (signal?.aborted) return Promise.reject(abortError())
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(resolve, delayMs)
|
|
signal?.addEventListener(
|
|
'abort',
|
|
() => {
|
|
clearTimeout(timer)
|
|
reject(abortError())
|
|
},
|
|
{ once: true },
|
|
)
|
|
})
|
|
}
|
|
|
|
/** 后台网络任务统一使用三次指数退避并加入抖动,禁止各扩展自行无限重试。 */
|
|
export async function retryWithBackoff<T>(
|
|
action: (attempt: number) => Promise<T>,
|
|
options: RetryOptions = {},
|
|
): Promise<T> {
|
|
const {
|
|
maxAttempts = 3,
|
|
baseDelayMs = 500,
|
|
maxDelayMs = 8_000,
|
|
jitterRatio = 0.2,
|
|
signal,
|
|
shouldRetry = () => true,
|
|
} = options
|
|
let lastError: unknown
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
try {
|
|
return await action(attempt)
|
|
} catch (error) {
|
|
lastError = error
|
|
if (attempt >= maxAttempts || !shouldRetry(error)) throw error
|
|
const base = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1))
|
|
const jitter = base * jitterRatio * (Math.random() * 2 - 1)
|
|
await wait(Math.max(0, Math.round(base + jitter)), signal)
|
|
}
|
|
}
|
|
throw lastError
|
|
}
|