XuqmGroup-RNSDK/packages/common/src/retry.ts

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
}