src/retry.ts

const RETRY_BASE_DELAY_MS = 180; const RETRY_MAX_DELAY_MS = 900;

const RETRYABLE_HTTP_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);

const RETRYABLE_FETCH_ERROR_CODES = new Set([ "ECONNRESET", "ENOTFOUND", "ETIMEDOUT", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT" ]);

export type ErrorWithAttempts = Error & { attempts?: number; };

export function shouldRetryHttpStatus(status: unknown) { return RETRYABLE_HTTP_STATUS.has(Number(status)); }

export function isRetryableFetchError(error: unknown) { const code = String((error as { code?: unknown; cause?: { code?: unknown } } | null)?.code || (error as { code?: unknown; cause?: { code?: unknown } } | null)?.cause?.code || "") .trim() .toUpperCase(); if (RETRYABLE_FETCH_ERROR_CODES.has(code)) return true;

const name = String((error as { name?: unknown } | null)?.name || ""); if (name === "AbortError" || name === "TimeoutError") return true;

const message = String((error as { message?: unknown } | null)?.message || "").toLowerCase(); return message.includes("timeout") || message.includes("timed out") || message.includes("fetch failed"); }

export function withAttemptCount(error: unknown, attempts: number) { const resolvedAttempts = Number(attempts || 1);

if (error instanceof Error) { const errorWithAttempts = error as ErrorWithAttempts; errorWithAttempts.attempts = resolvedAttempts; return errorWithAttempts; }

if (error && typeof error === "object") { const candidate = error as ErrorWithAttempts; candidate.attempts = resolvedAttempts; return candidate; }

const wrapped = new Error(String((error as { message?: unknown } | null)?.message || error || "unknown error")); return Object.assign(wrapped, { attempts: resolvedAttempts }) as ErrorWithAttempts; }

export function getRetryDelayMs(attempt: unknown) { return Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** Math.max(0, Number(attempt || 0) - 1)); }

export function isRedirectStatus(status: unknown) { const code = Number(status); return code === 301 || code === 302 || code === 303 || code === 307 || code === 308; }