"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WS_RECONNECT_DELAY_MS = void 0;
exports.runReconnectLoop = runReconnectLoop;
const wsErrorReporter_1 = require("./wsErrorReporter");
// Local one-liner — avoids dragging `src/classes/utils.ts` (and its
// transitive axios/decimal.js/token deps) into the WS foundation layer.
const noop = () => { };
exports.WS_RECONNECT_DELAY_MS = 500;
const MAX_RECONNECT_DELAY_MS = 30_000;
/** Sleep `ms`, but resolve immediately on abort — prevents teardown blocking
 *  on a stale timer for up to {@link MAX_RECONNECT_DELAY_MS}. */
function abortableSleep(ms, signal) {
    if (signal.aborted)
        return Promise.resolve();
    return new Promise((resolve) => {
        // Remove the abort listener on normal timeout — long-lived loops would
        // otherwise accumulate listeners on the same AbortSignal.
        const onAbort = () => {
            clearTimeout(timer);
            resolve();
        };
        const timer = setTimeout(() => {
            signal.removeEventListener('abort', onAbort);
            resolve();
        }, ms);
        signal.addEventListener('abort', onAbort, { once: true });
    });
}
/** Calls `onFirstMessage` exactly once when the first message arrives from
 *  the stream — the loop uses this to reset its backoff counter only on a
 *  "real" connection (one that actually received data from the wire).
 *
 *  We mark the connection healthy BEFORE invoking the consumer callback:
 *  message arrival proves the stream is alive, and a consumer-side throw is
 *  the consumer's bug, not a "no-message" connection. The previous
 *  ordering meant a throw in the very first `deliver()` left
 *  `firstDelivered=false`, so a later stream failure would incorrectly
 *  escalate backoff. */
async function consumeMessages(messageStream, signal, deliver, onError, onFirstMessage) {
    let firstDelivered = false;
    for await (const message of messageStream) {
        if (signal.aborted)
            return;
        if (!firstDelivered) {
            firstDelivered = true;
            onFirstMessage();
        }
        try {
            deliver(message);
        }
        catch (e) {
            onError(e);
        }
    }
}
function computeBackoffDelay(baseDelay, consecutiveFailures) {
    return Math.min(baseDelay * 2 ** consecutiveFailures, MAX_RECONNECT_DELAY_MS);
}
/** Wrap a no-arg sync hook so throws route through the reporter. Returns
 *  immediately, no microtask hop — used for `onSubscribed` so the connect
 *  path stays on the same tick. */
function safelyInvoke(reporter, hook) {
    return () => {
        if (!hook)
            return;
        try {
            hook();
        }
        catch (e) {
            reporter.reportError(e);
        }
    };
}
/** Wrap a varargs sync hook (logger) so throws route through the reporter. */
function safelyInvokeLog(reporter, hook) {
    return (...args) => {
        try {
            hook(...args);
        }
        catch (e) {
            reporter.reportError(e);
        }
    };
}
/** Wrap a no-arg async-or-sync hook (`onBeforeReconnect`) so rejections route
 *  through the reporter. */
function safelyInvokeAsync(reporter, hook) {
    return async () => {
        if (!hook)
            return;
        try {
            await hook();
        }
        catch (e) {
            reporter.reportError(e);
        }
    };
}
function runReconnectLoop(params) {
    const baseDelay = params.reconnectDelayMs ?? exports.WS_RECONNECT_DELAY_MS;
    const userOnLog = params.onLog ?? noop;
    const abortController = new AbortController();
    const { signal } = abortController;
    const errorReporter = (0, wsErrorReporter_1.makeWsErrorReporter)('[ws-reconnect-loop]', params.onError);
    const reportError = errorReporter.reportError;
    const safeOnLog = safelyInvokeLog(errorReporter, userOnLog);
    const safeOnBeforeReconnect = safelyInvokeAsync(errorReporter, params.onBeforeReconnect);
    const safeOnSubscribed = safelyInvoke(errorReporter, params.onSubscribed);
    const loop = async () => {
        let consecutiveFailures = 0;
        let hasEverConnected = false;
        while (!signal.aborted) {
            // Per-iteration flag: did THIS connection deliver any messages?
            // Drives the "backoff escalates only on no-message connections" rule
            // — see catch block below.
            let messageDeliveredThisConnection = false;
            try {
                if (hasEverConnected) {
                    safeOnLog('[ws] running onBeforeReconnect');
                    await safeOnBeforeReconnect();
                    if (signal.aborted)
                        break;
                }
                safeOnLog('[ws] subscribing');
                const messageStream = await params.subscribe(params.wsRpc, signal);
                // Don't reset `consecutiveFailures` here — handshake success doesn't
                // prove the connection is healthy. Reset happens in consumeMessages
                // once a message actually arrives.
                safeOnLog('[ws] subscribed');
                hasEverConnected = true;
                safeOnSubscribed();
                await consumeMessages(messageStream, signal, params.onMessage, reportError, () => {
                    consecutiveFailures = 0;
                    messageDeliveredThisConnection = true;
                });
                if (signal.aborted)
                    break;
                // Clean close = not a failure. Reset the counter so prior failures
                // (if any) don't escalate the next reconnect's backoff. Doc contract:
                // "Clean stream close → reconnect after baseDelay (no escalation)."
                consecutiveFailures = 0;
                safeOnLog('[ws] stream ended cleanly, reconnecting');
                await abortableSleep(baseDelay, signal);
            }
            catch (e) {
                if (signal.aborted)
                    break;
                reportError(e);
                const delay = computeBackoffDelay(baseDelay, consecutiveFailures);
                safeOnLog('[ws] reconnecting in', delay, 'ms (attempt', consecutiveFailures + 1, ')');
                // Only escalate when THIS connection delivered no messages. Doc
                // contract: "Backoff escalates ONLY after a connection that never
                // delivered a message." If a healthy connection drops, the next
                // reconnect uses baseDelay and we don't accumulate escalation.
                if (!messageDeliveredThisConnection) {
                    consecutiveFailures++;
                }
                await abortableSleep(delay, signal);
            }
        }
    };
    // loop() resolves cleanly — every throw is caught inside the while body.
    void loop();
    return () => abortController.abort();
}
//# sourceMappingURL=wsReconnectLoop.js.map