"use strict";
/**
 * Retry Strategy Utility
 *
 * Reusable exponential-backoff retry wrapper extracted from
 * scattered retry logic in glmt-proxy and binary/downloader.
 *
 * Usage:
 *   const data = await withRetry(() => fetch(url), { maxRetries: 3, baseDelayMs: 100 });
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.withRetry = void 0;
const error_types_1 = require("../errors/error-types");
const DEFAULT_MAX_DELAY_MS = 30000;
const DEFAULT_MULTIPLIER = 2;
const JITTER_RATIO = 0.2; // 20% of delay as random jitter
/**
 * Check whether an unknown thrown value is retryable.
 * Uses CCSError.recoverable flag and RetryableError instance check.
 */
function defaultRetryableCheck(error) {
    if (error instanceof error_types_1.RetryableError) {
        return true;
    }
    if ((0, error_types_1.isRecoverableError)(error)) {
        return true;
    }
    return false;
}
/**
 * Compute backoff delay: base * multiplier^attempt + jitter, capped at maxDelay.
 * Jitter is applied before the final cap to ensure the result never exceeds maxDelayMs.
 */
function computeDelay(attempt, baseDelayMs, maxDelayMs, multiplier, retryAfter) {
    const exponentialDelay = Math.min(baseDelayMs * Math.pow(multiplier, attempt), maxDelayMs);
    const jitter = exponentialDelay * JITTER_RATIO * Math.random();
    const delay = Math.min(exponentialDelay + jitter, maxDelayMs);
    if (retryAfter !== undefined && retryAfter > 0) {
        return Math.max(delay, retryAfter);
    }
    return delay;
}
/**
 * Sleep for the specified number of milliseconds.
 */
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
 * Execute `fn` with automatic retries on retryable failures.
 *
 * Retryability defaults to checking for `RetryableError` instances
 * and CCSError with `recoverable === true`. Override with `retryableCheck`.
 *
 * @param fn - The async function to execute
 * @param options - Retry configuration
 * @returns The resolved value from `fn`
 * @throws The last error encountered after exhausting retries
 */
async function withRetry(fn, options) {
    const { maxRetries, baseDelayMs, maxDelayMs = DEFAULT_MAX_DELAY_MS, backoffMultiplier = DEFAULT_MULTIPLIER, retryableCheck = defaultRetryableCheck, onRetry, } = options;
    if (maxRetries < 0) {
        throw new Error('withRetry: maxRetries must be >= 0');
    }
    if (baseDelayMs < 0) {
        throw new Error('withRetry: baseDelayMs must be >= 0');
    }
    const isRetryable = retryableCheck;
    let lastError;
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            return await fn();
        }
        catch (error) {
            lastError = error;
            // No more retries left
            if (attempt >= maxRetries) {
                break;
            }
            // Check retryability
            if (!isRetryable(error)) {
                break;
            }
            const err = error instanceof Error ? error : new Error(String(error));
            try {
                onRetry?.(err, attempt + 1);
            }
            catch {
                // Swallow callback errors — retry decision is already made
            }
            const retryAfter = error instanceof error_types_1.RetryableError ? error.retryAfter : undefined;
            const delay = computeDelay(attempt, baseDelayMs, maxDelayMs, backoffMultiplier, retryAfter);
            await sleep(delay);
        }
    }
    throw lastError;
}
exports.withRetry = withRetry;
//# sourceMappingURL=retry-strategy.js.map