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 { 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( action: (attempt: number) => Promise, options: RetryOptions = {}, ): Promise { 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 }