import { ProtectError } from "../errors.ts"; import type { Clock } from "../clock.ts"; import type { EventStream } from "../transport/events-stream.ts"; import type { ProtectLogging } from "../logging.ts"; import type { RawPacket } from "../protocol/packet.ts"; import type { StateStore } from "../state/store.ts"; import type { StreamOptions } from "../event-bus.ts"; import type { TypedEvent } from "../protocol/events.ts"; /** @internal */ type RecoveryPhase = "degraded" | "healthy" | "lost" | "reconnecting"; /** * The connection's observable state. `healthy` is steady operation; `degraded` means a fault was detected and reachability is being probed; `reconnecting` means the * controller is reachable and the monitor is re-bootstrapping and relaunching the events stream; `lost` means the controller is unreachable and recovery is polling; * `throttled` means the transport's circuit breaker is open (HTTP is paused on a cooldown). `throttled` is derived live from the transport and takes precedence. * * @category Client */ export type ConnectionState = RecoveryPhase | "throttled"; /** * One edge of the connection-state machine, yielded by {@link ConnectionMonitor.observe} and carried on the `stateChanged` event. `reason` is present when a typed fault * (a stall, a failed liveness probe) drove the edge. * * @category Client */ export interface ConnectionTransition { from: ConnectionState; reason?: string; to: ConnectionState; } /** * The events {@link ConnectionMonitor} publishes on its three-rail surface. * * - `stateChanged` fires on every connection-state edge. * - `controllerLost` fires once when the controller is concluded unreachable, carrying the typed fault. * - `controllerRebooted` fires when the controller's self-reported boot time changes, carrying the previous and new values. * - `controllerRecovered` fires when the connection returns to healthy after having been lost. * - `throttleEntered` / `throttleExited` forward the transport breaker's transitions. * * @category Client */ export interface ConnectionEvents { controllerLost: [reason: ProtectError]; controllerRebooted: [info: { newUpSince: number; previousUpSince: number; }]; controllerRecovered: []; stateChanged: [transition: ConnectionTransition]; throttleEntered: []; throttleExited: []; } /** * The narrow Transport surface the monitor depends on - exactly the throttle verdict and the two throttle rails, nothing more. Expressing the dependency as this * structural type (rather than the concrete {@link Transport}) is what makes the contract's "narrow coupling to Transport" a compile-time guarantee: the monitor cannot * reach into the transport's request path or breaker internals because the type does not expose them. The concrete `Transport` satisfies this structurally. * * @category Client */ export interface ThrottleSource { readonly isThrottled: boolean; on(event: "throttleEntered" | "throttleExited", handler: () => void): Disposable; } /** * Construction options for {@link ConnectionMonitor}. Every I/O dependency is an injected seam wired at the composition root, mirroring how every other subsystem in the * library inverts its dependencies. * * - `eventStreamFactory` builds an events stream for a given `lastUpdateId`; it is the single construction path, used for the initial stream and every relaunch. The * optional `signal` is bound only to the initial connect, never to a relaunch (a relaunch must not inherit a possibly-stale connect signal). * - `packetSink` receives every decoded {@link TypedEvent} from whichever inner stream is live (the composition root fans it into the store and the client firehose). * - `rawPacketSink` receives the {@link RawPacket} of every decoded frame from whichever inner stream is live (the composition root fans it into the client's raw * firehose); it carries the unmodeled frames `packetSink` never sees. Bridged across relaunches exactly as `packetSink` is, so the raw firehose survives recovery. * - `reBootstrap` fetches a fresh bootstrap, dispatches it, and returns its `lastUpdateId` to seed the relaunch - wired to the shared bootstrap fetch and the store's * single dispatch chokepoint, so there is one fetch source and one write path. * - `verify` is the liveness probe - a GET against the bootstrap endpoint (the controller does not answer HEAD there), surfaced as {@link ConnectionMonitor.verify}. * - `store` is read only through `observe`/`snapshot` (reboot detection); `transport` is the narrow {@link ThrottleSource}. * - `signal` cancels the initial events-stream connect. * * @category Client */ export interface ConnectionMonitorOptions { clock?: Clock; eventStreamFactory: (lastUpdateId: string, signal?: AbortSignal) => EventStream; initialLastUpdateId: string; log?: ProtectLogging; packetSink: (event: TypedEvent) => void; rawPacketSink: (packet: RawPacket) => void; reBootstrap: (signal?: AbortSignal) => Promise; signal?: AbortSignal; store: StateStore; transport: ThrottleSource; verify: (signal?: AbortSignal) => Promise; } /** * The connection-state coordinator. Constructed by `ProtectClient.connect()`; consumers read it as `client.connection`. * * @category Client */ export declare class ConnectionMonitor { #private; constructor(options: ConnectionMonitorOptions); /** The current connection state. Derived live from the recovery phase and the transport's throttle verdict, so it can never drift from a separately-stored enum. */ get state(): ConnectionState; /** Whether the connection is in steady operation. */ get isHealthy(): boolean; /** Whether the transport's throttle breaker is open. Delegates verbatim to the transport - the single source of truth for reachability backoff. */ get isThrottled(): boolean; /** The initial events-channel open handshake. The composition root awaits this to complete the atomic connect; a failure tears the client down. */ get opened(): Promise; /** * Subscribe to a connection event. Returns a `Disposable`; prefer `using sub = connection.on(...)` so the listener detaches at scope exit. * * @param event - The event to listen for. * @param handler - Invoked on each emission. * * @returns A `Disposable` that removes the listener when disposed. */ on(event: K, handler: (...args: ConnectionEvents[K]) => void): Disposable; /** * Wait for the next emission of a connection event. * * @param event - The event to await. * @param opts - Optional abort signal. * * @returns A promise resolving to the event's argument tuple. */ once(event: K, opts?: { signal?: AbortSignal; }): Promise; /** * Stream every subsequent emission of a connection event until the signal aborts. * * @param event - The event to stream. * @param opts - Stream options, including the abort signal. * * @returns An async iterable of the event's argument tuples. */ stream(event: K, opts?: StreamOptions): AsyncIterable; /** * Observe the connection-state machine: every {@link ConnectionTransition} until the signal aborts. Sugar over `stream("stateChanged")` that unwraps the single-element * argument tuple and ends quietly on the caller's own abort. * * @param opts - Optional abort signal that terminates the iteration. * * @returns An async iterable of state transitions. */ observe(opts?: { signal?: AbortSignal; }): AsyncGenerator; /** * Explicitly probe the controller's liveness (a GET against the bootstrap endpoint). Resolves when the controller answers, throws the classified error otherwise. The * same probe the recovery loop uses; exposed so a consumer can check reachability on demand. * * @param opts - Optional abort signal. * * @throws The classified `FatalError` on a non-2xx, or a transport-level `ProtectError` when the controller is unreachable. */ verify(opts?: { signal?: AbortSignal; }): Promise; /** * Arm the monitor to treat the next connection fault as a *known* controller reboot, so recovery enters the long, return-time-anticipating backoff track rather than * the prompt one. Called by {@link ProtectClient.reboot} after the controller accepts the reboot - the one moment a fault's cause is certain. Package-internal: it is * not a consumer recovery trigger, only a hint that colors the recovery episode the imminent fault will trigger on its own. * * The anticipation is bounded: it self-expires {@link PROTECT_REBOOT_ANTICIPATION_WINDOW_MS} after now, and is consumed by the next fault. So a reboot whose fault * never arrives cannot mis-route a much-later unrelated fault into the known track, and a fault after the window correctly takes the unknown track. */ expectReboot(): void; /** * Dispose the monitor: end the reboot-observation loop and any recovery wait, detach the throttle subscriptions, and tear the events stream down. Idempotent. */ [Symbol.asyncDispose](): Promise; } export {}; //# sourceMappingURL=connection.d.ts.map