/**
 * sinks/retry.ts
 *
 * Simple retry utility with exponential backoff for sink and notification
 * operations. Used by the sink runner and the notification service to retry
 * transient failures (network timeouts, HTTP 429/5xx) without requiring each
 * delivery path to implement retry logic internally.
 *
 * Policy: 3 retries, exponential backoff (1s → 2s → 4s).
 *
 * `withRetryResult` is the generic core: it retries any result type while a
 * caller-supplied `isFailed` predicate holds. `withRetry` is the sink-specific
 * wrapper (kept for its existing positional signature). The notification
 * service uses `withRetryResult` directly with `DeliveryOutcome`, which has the
 * same `failed` shape but reports success as `delivered` rather than `success`.
 *
 * @see docs/design-docs/report-store/sink-architecture.md
 */
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_BASE_DELAY_MS = 1000;
/**
 * Retry an operation with exponential backoff, generic over the result shape.
 *
 * Only retries while `isFailed(result)` is true — any other status (success,
 * skipped, delivered) is terminal and returned immediately. Thrown exceptions
 * are caught and converted to a failure via `onThrow`, then subject to the same
 * retry policy. Returns the final result; never throws.
 */
export async function withRetryResult(fn, options) {
    const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
    const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
    let lastResult = options.onThrow(new Error("No attempts made"));
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            const result = await fn();
            // Any non-failed status is terminal — no retry.
            if (!options.isFailed(result)) {
                return result;
            }
            lastResult = result;
        }
        catch (error) {
            lastResult = options.onThrow(error);
        }
        // Don't delay after the last attempt
        if (attempt < maxRetries) {
            const delayMs = baseDelayMs * Math.pow(2, attempt);
            await sleep(delayMs);
        }
    }
    return lastResult;
}
/**
 * Retry a sink publish operation with exponential backoff.
 *
 * Only retries on "failed" results — "success" and "skipped" are returned
 * immediately. Thrown exceptions are caught and converted to "failed" results.
 *
 * @param fn - The publish function to retry
 * @param maxRetries - Maximum number of retry attempts (default: 3)
 * @param baseDelayMs - Initial delay in milliseconds (default: 1000)
 * @returns The final SinkResult after all attempts
 */
export async function withRetry(fn, maxRetries = DEFAULT_MAX_RETRIES, baseDelayMs = DEFAULT_BASE_DELAY_MS) {
    return withRetryResult(fn, {
        isFailed: (result) => result.status === "failed",
        onThrow: (error) => ({
            error: error instanceof Error ? error.message : String(error),
            status: "failed",
        }),
        maxRetries,
        baseDelayMs,
    });
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
