/**
 * Retry with exponential backoff for upstream requests.
 * Only retries on 5xx or connection errors. Never retries 4xx or provider
 * timeouts; a timeout has already consumed the wall-clock budget and should
 * be surfaced for an explicit user/operator retry.
 */
import { logWarn } from '../logger.js';
const DEFAULT_OPTIONS = {
    maxRetries: 1,
    baseDelayMs: 100,
    maxDelayMs: 2000,
    timeoutMs: 60000,
    retryableStatuses: [500, 502, 503, 504, 529],
};
export { DEFAULT_OPTIONS as RETRY_DEFAULTS };
function isRetryableError(err) {
    if (err instanceof Error) {
        const msg = err.message.toLowerCase();
        if (err.name === 'AbortError' || msg.includes('timeout') || msg.includes('timed out')) {
            return false;
        }
        return msg.includes('econnrefused') ||
            msg.includes('econnreset') ||
            msg.includes('enotfound') ||
            msg.includes('fetch failed') ||
            msg.includes('network') ||
            msg.includes('stream closed');
    }
    return false;
}
function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
/** Add ±25% jitter to a delay value. */
function jitter(ms) {
    const factor = 0.75 + Math.random() * 0.5; // 0.75 to 1.25
    return Math.round(ms * factor);
}
/**
 * Execute an async function with retry logic.
 * The function should return a Response object.
 * Supports per-request timeout via AbortSignal.
 */
export async function withRetry(fn, options) {
    const opts = { ...DEFAULT_OPTIONS, ...options };
    let lastError = null;
    for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
        const controller = new AbortController();
        const timer = opts.timeoutMs > 0
            ? setTimeout(() => controller.abort(), opts.timeoutMs)
            : null;
        try {
            const response = await fn(controller.signal);
            if (timer)
                clearTimeout(timer);
            // Don't retry on success or client errors (4xx)
            if (response.ok || (response.status >= 400 && response.status < 500)) {
                return response;
            }
            // Check if this status is retryable
            if (!opts.retryableStatuses.includes(response.status)) {
                return response;
            }
            // Retryable server error
            lastError = new Error(`HTTP ${response.status}`);
            if (attempt < opts.maxRetries) {
                const delay = jitter(Math.min(opts.baseDelayMs * Math.pow(2, attempt), opts.maxDelayMs));
                logWarn('retry', `Attempt ${attempt + 1}/${opts.maxRetries} failed`, { status: response.status, delayMs: delay });
                await sleep(delay);
                continue;
            }
            return response;
        }
        catch (err) {
            if (timer)
                clearTimeout(timer);
            lastError = err;
            // Map AbortError to timeout message
            if (err instanceof Error && err.name === 'AbortError') {
                lastError = new Error(`Request timed out after ${opts.timeoutMs}ms`);
                throw lastError;
            }
            if (!isRetryableError(err)) {
                throw err;
            }
            if (attempt < opts.maxRetries) {
                const delay = jitter(Math.min(opts.baseDelayMs * Math.pow(2, attempt), opts.maxDelayMs));
                logWarn('retry', `Attempt ${attempt + 1}/${opts.maxRetries} failed`, { error: err instanceof Error ? err.message : 'unknown', delayMs: delay });
                await sleep(delay);
            }
        }
    }
    throw lastError ?? new Error('All retry attempts exhausted');
}
//# sourceMappingURL=retry.js.map