import { isTerminalPhase } from "../execution/execution-phases.js";
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
// The controller's FSM state is exactly the store's `StreamState` — they
// were once duplicated unions kept in lock-step by hand. The controller
// reuses the store's type so the lifecycle (including the `reconnecting`
// stage) is defined in one place and can never drift.
const IDLE = { stage: "idle" };
/**
 * Hard connect-timeout: time the stream may stay `connecting` (no first
 * snapshot) before the watchdog acts. Sized well above a healthy connect
 * (the server sends the initial snapshot in ~1 RTT) yet short enough that a
 * silent hang surfaces an affordance within a few seconds, not minutes.
 */
export const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
/**
 * Soft slow-stall threshold: a non-terminal stream that produces no new
 * snapshot for this long flips an informational "still working" hint. Long
 * enough that ordinary model thinking-time never trips it.
 */
export const DEFAULT_SLOW_THRESHOLD_MS = 60_000;
// ---------------------------------------------------------------------------
// StreamController
// ---------------------------------------------------------------------------
/**
 * Framework-agnostic finite state machine that manages the lifecycle
 * of a single execution stream subscription.
 *
 * Responsibilities:
 * - Track FSM state transitions (idle → connecting → streaming → complete/error)
 * - Buffer incoming snapshots and coalesce via `requestAnimationFrame`
 * - Flush terminal snapshots immediately (no rAF delay)
 * - Provide abort/reconnect semantics
 *
 * This class has no React dependency. It communicates outward through
 * a {@link StreamControllerSink} and a `scheduleFlush` function
 * (typically `requestAnimationFrame`).
 */
export class StreamController {
    _state = IDLE;
    _bufferedSnapshot = null;
    _rafId = null;
    _sink;
    _scheduleFlush;
    _cancelFlush;
    // -- Watchdog ------------------------------------------------------------
    _setTimer;
    _clearTimer;
    _connectTimeoutMs;
    _slowThresholdMs;
    _connectTimerId = null;
    _slowTimerId = null;
    constructor(sink, scheduleFlush = typeof requestAnimationFrame !== "undefined"
        ? (cb) => requestAnimationFrame(cb)
        : (cb) => setTimeout(cb, 16), cancelFlush = typeof cancelAnimationFrame !== "undefined"
        ? (id) => cancelAnimationFrame(id)
        : (id) => clearTimeout(id), watchdog) {
        this._sink = sink;
        this._scheduleFlush = scheduleFlush;
        this._cancelFlush = cancelFlush;
        this._setTimer =
            watchdog?.setTimer ??
                ((cb, ms) => setTimeout(cb, ms));
        this._clearTimer = watchdog?.clearTimer ?? ((id) => clearTimeout(id));
        this._connectTimeoutMs =
            watchdog?.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
        this._slowThresholdMs =
            watchdog?.slowThresholdMs ?? DEFAULT_SLOW_THRESHOLD_MS;
    }
    /** Current FSM state (read-only). */
    get state() {
        return this._state;
    }
    /**
     * Transition to `connecting` for the given execution ID.
     * If already active for a different ID, resets first.
     */
    start(executionId) {
        this._cancelPendingFlush();
        this._bufferedSnapshot = null;
        // Arm both watchdogs the moment we begin connecting. The connect-timeout
        // guards the first snapshot; the slow-stall hint guards every quiet stretch
        // thereafter. Both are reset by the next snapshot and cleared on any exit.
        this._armConnectTimer();
        this._armSlowTimer();
        this._transition({ stage: "connecting", executionId });
    }
    /**
     * Handle an incoming snapshot from the gRPC stream.
     * Non-terminal snapshots are buffered for rAF coalescing.
     * Terminal snapshots flush immediately.
     */
    handleSnapshot(snapshot) {
        const executionId = this._activeExecutionId();
        if (!executionId)
            return;
        const phase = snapshot.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
        const terminal = isTerminalPhase(phase);
        if (terminal) {
            this._cancelWatchdogs();
            this._sink.setSlow(false);
            this._cancelPendingFlush();
            this._bufferedSnapshot = null;
            this._sink.ingestSnapshot(snapshot);
            this._transition({ stage: "complete", executionId });
        }
        else {
            // A snapshot proves the (re)connection is healthy: the connect-timeout no
            // longer applies, and the slow-stall window restarts from now.
            this._cancelConnectTimer();
            this._armSlowTimer();
            // Advance from either the initial `connecting` or a `reconnecting` retry
            // into `streaming`.
            if (this._state.stage === "connecting" ||
                this._state.stage === "reconnecting") {
                this._transition({ stage: "streaming", executionId });
            }
            this._bufferedSnapshot = snapshot;
            this._scheduleFlushOnce();
        }
    }
    /**
     * Enter the `reconnecting` stage after a transient drop. Unlike
     * {@link start}, this preserves the buffered snapshot and never resets the
     * store, so the last-known-good conversation stays on screen while the
     * background retry is in flight. No-op once idle (the subscription is
     * already torn down).
     */
    handleReconnecting(attempt, error) {
        const executionId = this._activeExecutionId();
        if (!executionId)
            return;
        // Auto-reconnect (#174) has taken over with its own visible affordance and
        // bounded attempt budget, so the silence watchdogs stand down — they exist
        // for the case where the stream stalls *without* a drop. The slow hint is
        // cleared so "reconnecting" and "slow" never show at once.
        this._cancelWatchdogs();
        this._sink.setSlow(false);
        this._transition({ stage: "reconnecting", executionId, attempt, error });
    }
    /**
     * Handle stream completion (iterator exhausted without error).
     * If we still have a buffered snapshot, flush it first.
     */
    handleStreamEnd() {
        const executionId = this._activeExecutionId();
        if (!executionId)
            return;
        this._cancelWatchdogs();
        this._sink.setSlow(false);
        this._flushBuffer();
        if (this._state.stage !== "complete") {
            this._transition({ stage: "complete", executionId });
        }
    }
    /**
     * Handle a stream error. Flushes any buffered snapshot first
     * so the UI shows the last known good state alongside the error.
     */
    handleError(error) {
        const executionId = this._activeExecutionId();
        if (!executionId)
            return;
        this._cancelWatchdogs();
        this._sink.setSlow(false);
        this._cancelPendingFlush();
        this._flushBuffer();
        this._transition({ stage: "error", executionId, error });
    }
    /** Reset to idle. Cancels any pending flush and watchdog timers. */
    reset() {
        this._cancelWatchdogs();
        this._sink.setSlow(false);
        this._cancelPendingFlush();
        this._bufferedSnapshot = null;
        if (this._state.stage !== "idle") {
            this._transition(IDLE);
        }
    }
    /** Whether the controller has a buffered snapshot awaiting flush. */
    get hasPendingFlush() {
        return this._rafId !== null;
    }
    // -------------------------------------------------------------------------
    // Private
    // -------------------------------------------------------------------------
    _activeExecutionId() {
        if (this._state.stage === "idle")
            return null;
        return this._state.executionId;
    }
    _transition(next) {
        this._state = next;
        this._sink.setStreamState(next);
    }
    _scheduleFlushOnce() {
        if (this._rafId !== null)
            return;
        this._rafId = this._scheduleFlush(() => {
            this._rafId = null;
            this._flushBuffer();
        });
    }
    _flushBuffer() {
        if (this._bufferedSnapshot) {
            const snap = this._bufferedSnapshot;
            this._bufferedSnapshot = null;
            this._sink.ingestSnapshot(snap);
        }
    }
    _cancelPendingFlush() {
        if (this._rafId !== null) {
            this._cancelFlush(this._rafId);
            this._rafId = null;
        }
    }
    // -- Watchdog timers ------------------------------------------------------
    _armConnectTimer() {
        this._cancelConnectTimer();
        this._connectTimerId = this._setTimer(() => {
            this._connectTimerId = null;
            // Only meaningful while still awaiting the first snapshot; any later
            // stage has its own handling and has already cleared this timer.
            if (this._state.stage !== "connecting")
                return;
            this._sink.onConnectTimeout();
        }, this._connectTimeoutMs);
    }
    _armSlowTimer() {
        this._cancelSlowTimer();
        // Re-arming means activity just resumed (or is starting), so a prior slow
        // hint no longer holds — clear it before counting the next quiet stretch.
        this._sink.setSlow(false);
        this._slowTimerId = this._setTimer(() => {
            this._slowTimerId = null;
            if (this._state.stage !== "connecting" && this._state.stage !== "streaming")
                return;
            this._sink.setSlow(true);
        }, this._slowThresholdMs);
    }
    _cancelConnectTimer() {
        if (this._connectTimerId !== null) {
            this._clearTimer(this._connectTimerId);
            this._connectTimerId = null;
        }
    }
    _cancelSlowTimer() {
        if (this._slowTimerId !== null) {
            this._clearTimer(this._slowTimerId);
            this._slowTimerId = null;
        }
    }
    _cancelWatchdogs() {
        this._cancelConnectTimer();
        this._cancelSlowTimer();
    }
}
//# sourceMappingURL=stream-controller.js.map