/**
 * @fileoverview Connection heartbeat monitor for MCP transports.
 * Periodically pings the connected client to verify liveness and emits
 * an OTel failure counter for alerting on unresponsive connections.
 *
 * The MCP spec recommends periodic pings to detect connection health.
 * This module implements server-initiated pings with configurable interval
 * and miss threshold, triggering cleanup when a client becomes unresponsive.
 * @module src/mcp-server/transports/heartbeat
 */
import { logger } from '../../utils/internal/logger.js';
import { requestContextService } from '../../utils/internal/requestContext.js';
import { ATTR_MCP_CONNECTION_TRANSPORT } from '../../utils/telemetry/attributes.js';
import { createCounter } from '../../utils/telemetry/metrics.js';
// ---------------------------------------------------------------------------
// Lazy OTel metric singletons
// ---------------------------------------------------------------------------
let heartbeatFailures;
function getMetrics() {
    heartbeatFailures ??= createCounter('mcp.heartbeat.failures', 'Heartbeat ping failures', '{failures}');
    return { heartbeatFailures };
}
/** Eagerly create metric series so they exist from the first export cycle. */
export function initHeartbeatMetrics() {
    getMetrics();
}
// ---------------------------------------------------------------------------
// HeartbeatMonitor
// ---------------------------------------------------------------------------
/**
 * Periodically pings the connected MCP client and tracks liveness.
 *
 * Uses a recursive `setTimeout` pattern (not `setInterval`) so pings
 * never overlap — each cycle waits for the previous ping to resolve
 * before scheduling the next one.
 *
 * On {@link HeartbeatOptions.missThreshold} consecutive failures the
 * monitor calls {@link HeartbeatOptions.onDead} and stops itself.
 */
export class HeartbeatMonitor {
    timer = null;
    consecutiveFailures = 0;
    stopped = false;
    opts;
    context;
    attrs;
    constructor(opts, parentContext) {
        this.opts = opts;
        this.context = requestContextService.createRequestContext({
            ...parentContext,
            component: 'HeartbeatMonitor',
            transport: opts.transport,
        });
        this.attrs = { [ATTR_MCP_CONNECTION_TRANSPORT]: opts.transport };
    }
    /** Begin the ping cycle. No-op if `intervalMs <= 0`. */
    start() {
        if (this.opts.intervalMs <= 0)
            return;
        this.stopped = false;
        logger.info(`Heartbeat enabled (interval=${this.opts.intervalMs}ms, threshold=${this.opts.missThreshold})`, this.context);
        this.scheduleNext();
    }
    /** Stop the ping cycle. */
    stop() {
        if (this.stopped)
            return;
        this.stopped = true;
        if (this.timer) {
            clearTimeout(this.timer);
            this.timer = null;
        }
    }
    // -------------------------------------------------------------------------
    // Internal
    // -------------------------------------------------------------------------
    scheduleNext() {
        if (this.stopped)
            return;
        this.timer = setTimeout(() => void this.tick(), this.opts.intervalMs);
        this.timer.unref?.();
    }
    async tick() {
        if (this.stopped)
            return;
        try {
            await this.opts.sendPing();
            if (this.consecutiveFailures > 0) {
                logger.debug(`Heartbeat recovered after ${this.consecutiveFailures} failure(s)`, this.context);
            }
            this.consecutiveFailures = 0;
        }
        catch (err) {
            this.consecutiveFailures++;
            getMetrics().heartbeatFailures.add(1, this.attrs);
            logger.warning(`Heartbeat ping failed (${this.consecutiveFailures}/${this.opts.missThreshold})`, {
                ...this.context,
                error: err instanceof Error ? err.message : String(err),
            });
            if (this.consecutiveFailures >= this.opts.missThreshold) {
                logger.error('Heartbeat: connection declared dead — initiating shutdown.', new Error(`${this.opts.missThreshold} consecutive heartbeat failures`), this.context);
                this.opts.onDead();
                this.stop();
                return;
            }
        }
        this.scheduleNext();
    }
}
//# sourceMappingURL=heartbeat.js.map