import type { FetchContext } from './types.js' export const RETRY_METHODS = ['GET', 'PUT', 'DELETE'] as const // 429 is intentionally absent — it has a dedicated branch in execute(). shouldRetry covers // transport errors / 408 / 413 / 5xx only. export const RETRY_STATUS_CODES = [408, 413, 500, 502, 503, 504] as const const RETRY_METHODS_SET: ReadonlySet = new Set(RETRY_METHODS) const RETRY_STATUS_SET: ReadonlySet = new Set(RETRY_STATUS_CODES) // GET/PUT/DELETE are idempotent — safe to auto-retry. The 429 branch reuses this to decide which // methods may wait-and-retry a throttle without risking a double-run. export function isIdempotentRetryMethod(method: string): boolean { return RETRY_METHODS_SET.has(method) } export function shouldRetry(target: Response | unknown, ctx: FetchContext): boolean { if (!RETRY_METHODS_SET.has(ctx.options.method)) return false if (target instanceof Response) return RETRY_STATUS_SET.has(target.status) // Any other transport error on a retryable method retries. User aborts are filtered // out earlier in dispatch (before this hook ever runs), so they never reach here. return true } // Exponential backoff: 300ms base, doubling each attempt, capped at 30s // (300ms, 600ms, 1.2s, ...). const BACKOFF_BASE_MS = 300 const BACKOFF_CAP_MS = 30_000 export function backoffDelay(attempt: number): number { if (attempt <= 0) return 0 const exp = BACKOFF_BASE_MS * 2 ** (attempt - 1) return Math.min(exp, BACKOFF_CAP_MS) }