import { TSchema, Static } from 'typebox'; import { T as TransportClientId, l as TelemetryInfo, P as ProtocolVersion, h as PartialTransportMessage, E as EncodedTransportMessage, C as CustomHandshakeErrorCodeSchema, j as HandshakeErrorCustomHandlerFatalResponseCodes, k as CustomHandshakeErrorCode, O as OpaqueTransportMessage, g as HandshakeErrorResponseCodes, b as TransportMessage, B as BuiltInHandshakeErrorCode } from './message-C2qDAau6.js'; import { L as Logger, M as MessageMetadata, T as Tags, a as LogFn, b as LoggingLevel } from './index-CIMpdW6z.js'; import { C as Codec } from './types-BGGvYIJM.js'; import { C as CodecMessageAdapter, E as EncodeResult, S as SendResult, a as SendBufferResult } from './adapter-K2HsfQLt.js'; import { Tracer } from '@opentelemetry/api'; interface PromiseWithResolvers { promise: Promise; resolve: (value: T) => void; reject: (reason: unknown) => void; } declare const enum SessionState { NoConnection = "NoConnection", BackingOff = "BackingOff", Connecting = "Connecting", Handshaking = "Handshaking", Connected = "Connected", WaitingForHandshake = "WaitingForHandshake" } declare abstract class StateMachineState { abstract readonly state: SessionState; _isConsumed: boolean; abstract _handleStateExit(): void; abstract _handleClose(): void; /** * Cleanup this state machine state and mark it as consumed. * After calling close, it is an error to access any properties on the state. * You should never need to call this as a consumer. * * If you're looking to close the session from the client, * use `.hardDisconnect` on the client transport. */ close(): void; constructor(); } interface SessionOptions { /** * Frequency at which to send heartbeat acknowledgements */ heartbeatIntervalMs: number; /** * Number of elapsed heartbeats without a response message before we consider * the connection dead. */ heartbeatsUntilDead: number; /** * Max duration that a session can be without a connection before we consider * it dead. This deadline is carried between states and is used to determine * when to consider the session a lost cause and delete it entirely. * Generally, this should be strictly greater than the sum of * {@link connectionTimeoutMs} and {@link handshakeTimeoutMs}. */ sessionDisconnectGraceMs: number; /** * Connection timeout in milliseconds */ connectionTimeoutMs: number; /** * Handshake timeout in milliseconds */ handshakeTimeoutMs: number; /** * Whether to enable transparent session reconnects */ enableTransparentSessionReconnects: boolean; /** * Number of messages in the session's send buffer at or above which * {@link Writable.write} reports backpressure (returns false). This is * purely advisory: writes are never dropped or blocked regardless of * this value. Must be at least 1 — with a value of 0 the buffer can * never drain below the mark and waiters would only resolve on * session close. */ sendBufferHighWaterMark: number; /** * The codec to use for encoding/decoding messages over the wire */ codec: Codec; } interface CommonSessionProps { from: TransportClientId; options: SessionOptions; codec: CodecMessageAdapter; tracer: Tracer; log: Logger | undefined; } declare abstract class CommonSession extends StateMachineState { readonly from: TransportClientId; readonly options: SessionOptions; readonly codec: CodecMessageAdapter; tracer: Tracer; log?: Logger; abstract get loggingMetadata(): MessageMetadata; constructor({ from, options, log, tracer, codec }: CommonSessionProps); } type SessionId = string; interface IdentifiedSessionListeners { onMessageSendFailure: (msg: PartialTransportMessage & { seq: number; }, reason: string) => void; } interface IdentifiedSessionProps extends CommonSessionProps { id: SessionId; to: TransportClientId; seq: number; ack: number; seqSent: number; sendBuffer: Array; sendBufferDrainWaiter: PromiseWithResolvers | undefined; telemetry: TelemetryInfo; protocolVersion: ProtocolVersion; listeners: IdentifiedSessionListeners; } declare abstract class IdentifiedSession extends CommonSession { readonly id: SessionId; readonly telemetry: TelemetryInfo; readonly to: TransportClientId; readonly protocolVersion: ProtocolVersion; listeners: IdentifiedSessionListeners; /** * Index of the message we will send next (excluding handshake) */ seq: number; /** * Last seq we sent over the wire this session (excluding handshake) and retransmissions */ seqSent: number; /** * Number of unique messages we've received this session (excluding handshake) */ ack: number; sendBuffer: Array; /** * Shared promise for pending {@link waitForSendBufferDrain} calls, created * lazily on the first waiter of a pressure episode and cleared on drain. * Carried across session state transitions alongside {@link sendBuffer}. */ sendBufferDrainWaiter: PromiseWithResolvers | undefined; constructor(props: IdentifiedSessionProps); get loggingMetadata(): MessageMetadata; encodeMsg(partialMsg: PartialTransportMessage): EncodeResult; nextSeq(): number; isSendBufferFull(): boolean; /** * Resolves once the send buffer drops back below * {@link SessionOptions.sendBufferHighWaterMark}, or immediately if it * already is. Also resolves (never rejects) when the session closes so * waiting producers don't hang forever. */ waitForSendBufferDrain(): Promise; protected notifySendBufferDrain(): void; send(msg: PartialTransportMessage): SendResult; _handleStateExit(): void; _handleClose(): void; } interface IdentifiedSessionWithGracePeriodListeners extends IdentifiedSessionListeners { onSessionGracePeriodElapsed: () => void; } interface IdentifiedSessionWithGracePeriodProps extends IdentifiedSessionProps { graceExpiryTime: number; listeners: IdentifiedSessionWithGracePeriodListeners; } declare abstract class IdentifiedSessionWithGracePeriod extends IdentifiedSession { graceExpiryTime: number; protected gracePeriodTimeout?: ReturnType; listeners: IdentifiedSessionWithGracePeriodListeners; constructor(props: IdentifiedSessionWithGracePeriodProps); _handleStateExit(): void; _handleClose(): void; } type ConnectionExtras = Record; /** * A connection is the actual raw underlying transport connection. * It's responsible for dispatching to/from the actual connection itself * This should be instantiated as soon as the client/server has a connection * It's tied to the lifecycle of the underlying transport connection (i.e. if the WS drops, this connection should be deleted) */ declare abstract class Connection { id: string; telemetry?: TelemetryInfo; extras?: ConnectionExtras; constructor(extras?: ConnectionExtras); get loggingMetadata(): MessageMetadata; dataListener?: (msg: Uint8Array) => void; closeListener?: () => void; errorListener?: (err: Error) => void; onData(msg: Uint8Array): void; onError(err: Error): void; onClose(): void; /** * Set the callback for when a message is received. * @param cb The message handler callback. */ setDataListener(cb: (msg: Uint8Array) => void): void; removeDataListener(): void; /** * Set the callback for when the connection is closed. * This should also be called if an error happens and after notifying the error listener. * @param cb The callback to call when the connection is closed. */ setCloseListener(cb: () => void): void; removeCloseListener(): void; /** * Set the callback for when an error is received. * This should only be used for logging errors, all cleanup * should be delegated to setCloseListener. * * The implementer should take care such that the implemented * connection will call both the close and error callbacks * on an error. * * @param cb The callback to call when an error is received. */ setErrorListener(cb: (err: Error) => void): void; removeErrorListener(): void; /** * Sends a message over the connection. * @param msg The message to send. * @returns true if the message was sent, false otherwise. */ abstract send(msg: Uint8Array): boolean; /** * Closes the connection. */ abstract close(): void; } type ConstructHandshake = () => Static | Promise>; type ValidateHandshake = (metadata: Static, previousParsedMetadata?: ParsedMetadata, from?: TransportClientId, connectionExtras?: ConnectionExtras) => Static | CustomHandshakeErrorCode | ParsedMetadata | Promise | CustomHandshakeErrorCode | ParsedMetadata>; interface ClientHandshakeOptions { /** * Schema for the metadata that the client sends to the server * during the handshake. */ schema: MetadataSchema; /** * Custom rejection codes the server may answer the handshake * with, sent in the response's `code` field. Must match the server's * {@link ServerHandshakeOptions.rejectionCodeSchema}: an unconfigured code * is rejected as a malformed handshake response. Pass a TypeBox union of * literals. */ rejectionCodeSchema?: RejectionCodeSchema; /** * Gets the {@link HandshakeRequestMetadata} to send to the server. */ construct: ConstructHandshake; /** * When true, the client constructs handshake metadata as soon as it begins * dialing, so a slow {@link construct} (e.g. fetching a fresh token) overlaps * establishing the connection rather than running after it. The trade-off is * that `construct` then runs on every connection attempt, including ones that * never connect, so leave it unset when constructing is expensive or * rate-limited. */ eager?: boolean; } interface ServerHandshakeOptions { /** * Schema for the metadata that the server receives from the client * during the handshake. */ schema: MetadataSchema; /** * Custom rejection codes that {@link validate} may return. * They travel in the handshake response's `code` field and are fatal like * the built-in custom-handler codes. Clients must register the same codes * in {@link ClientHandshakeOptions.rejectionCodeSchema} or they reject the * response as malformed. Pass a TypeBox union of literals. */ rejectionCodeSchema?: RejectionCodeSchema; /** * Parses the metadata sent by the client during the handshake into the * server-side {@link ParsedMetadata}, or returns a handshake failure code to * reject the connection. * * @param metadata - The metadata sent by the client. * @param previousParsedMetadata - The parsed metadata from the previous * connection on this session, if any (e.g. on reconnect). * @param from - The client id the peer presented in its handshake. Use it to * confirm the presented id is the one the metadata authorizes before * returning parsed metadata. * @param connectionExtras - Context attached to the current transport * connection, if any. */ validate: ValidateHandshake>; /** * When the credential expires (or undefined if it never does). The server * re-handshakes one `handshakeTimeoutMs` beforehand — re-validating fresh * metadata and live-replacing the stored value — so the session never serves * past expiry: a refresh lands first, or an unanswered re-handshake tears the * session down by then. Re-evaluated on every (re)validation. * * Scheduling only — it does not gate requests, so reject already-expired * credentials in {@link validate} or against the live `ctx.metadata`. */ expiry?: (parsedMetadata: ParsedMetadata) => Date | undefined; } declare function createClientHandshakeOptions(schema: MetadataSchema, construct: ConstructHandshake, eager?: boolean, rejectionCodeSchema?: RejectionCodeSchema): ClientHandshakeOptions; declare function createServerHandshakeOptions(schema: MetadataSchema, validate: ValidateHandshake>, expiry?: (parsedMetadata: ParsedMetadata) => Date | undefined, rejectionCodeSchema?: RejectionCodeSchema): ServerHandshakeOptions; /** * Options to control the backoff and retry behavior of the client transport's connection behaviour. * * River implements exponential backoff with jitter to prevent flooding the server * when there's an issue with connection establishment. * * The backoff is calculated via the following: * backOff = min(jitter + {@link baseIntervalMs} * 2 ^ budget_consumed, {@link maxBackoffMs}) * * We use a leaky bucket rate limit with a budget of {@link attemptBudgetCapacity} reconnection attempts. * Budget only starts to restore after a successful handshake at a rate of one budget per {@link budgetRestoreIntervalMs}. */ interface ConnectionRetryOptions { /** * The base interval to wait before retrying a connection. */ baseIntervalMs: number; /** * The maximum random jitter to add to the total backoff time. */ maxJitterMs: number; /** * The maximum amount of time to wait before retrying a connection. * This does not include the jitter. */ maxBackoffMs: number; /** * The max number of times to attempt a connection before a successful handshake. * This persists across connections but starts restoring budget after a successful handshake. * The restoration interval depends on {@link budgetRestoreIntervalMs} */ attemptBudgetCapacity: number; /** * After a successful connection attempt, how long to wait before we restore a single budget. */ budgetRestoreIntervalMs: number; /** * A function to determine whether an error is fatal and should not be retried. * If this function returns true, the client transport will not attempt to reconnect. */ isFatalConnectionError: (err: Error) => boolean; } declare class LeakyBucketRateLimit { private budgetConsumed; private intervalHandle?; private readonly options; constructor(options: ConnectionRetryOptions); getBackoffMs(): number; get totalBudgetRestoreTime(): number; consumeBudget(): void; getBudgetConsumed(): number; hasBudget(): boolean; startRestoringBudget(): void; private stopLeak; resetBudget(): void; close(): void; } type TransportOptions = SessionOptions; type ProvidedTransportOptions = Partial; type ClientTransportOptions = TransportOptions & ConnectionRetryOptions; type ProvidedClientTransportOptions = Partial; type ServerTransportOptions = TransportOptions; type ProvidedServerTransportOptions = Partial; interface SessionConnectingListeners extends IdentifiedSessionWithGracePeriodListeners { onConnectionEstablished: (conn: Connection) => void; onConnectionFailed: (err: unknown) => void; onConnectionTimeout: () => void; } interface SessionConnectingProps extends IdentifiedSessionWithGracePeriodProps { connPromise: Promise; listeners: SessionConnectingListeners; } declare class SessionConnecting extends IdentifiedSessionWithGracePeriod { readonly state: SessionState.Connecting; connPromise: Promise; listeners: SessionConnectingListeners; connectionTimeout?: ReturnType; constructor(props: SessionConnectingProps); bestEffortClose(): void; _handleStateExit(): void; _handleClose(): void; } declare class SessionNoConnection extends IdentifiedSessionWithGracePeriod { readonly state: SessionState.NoConnection; _handleClose(): void; _handleStateExit(): void; } interface SessionHandshakingListeners extends IdentifiedSessionWithGracePeriodListeners { onConnectionErrored: (err: unknown) => void; onConnectionClosed: () => void; onHandshake: (msg: OpaqueTransportMessage) => void; onInvalidHandshake: (reason: string, code: Static) => void; onHandshakeTimeout: () => void; } interface SessionHandshakingProps extends IdentifiedSessionWithGracePeriodProps { conn: ConnType; listeners: SessionHandshakingListeners; } declare class SessionHandshaking extends IdentifiedSessionWithGracePeriod { readonly state: SessionState.Handshaking; conn: ConnType; listeners: SessionHandshakingListeners; handshakeTimeout?: ReturnType; constructor(props: SessionHandshakingProps); get loggingMetadata(): { protocolVersion?: ProtocolVersion | undefined; clientId?: string | undefined; connectedTo?: string | undefined; sessionId?: string | undefined; connId?: string | undefined; transportMessage?: Partial | undefined; validationErrors?: { path: string; message: string; }[] | undefined; tags?: Tags[] | undefined; telemetry?: { traceId: string; spanId: string; } | undefined; extras?: Record | undefined; }; onHandshakeData: (msg: Uint8Array) => void; sendHandshake(msg: TransportMessage): SendResult; _handleStateExit(): void; _handleClose(): void; } interface SessionConnectedListeners extends IdentifiedSessionListeners { onConnectionErrored: (err: unknown) => void; onConnectionClosed: () => void; onMessage: (msg: OpaqueTransportMessage) => void; /** * A frame arrived on the reserved re-handshake stream. The transport consumes * it to drive the follow-up handshake rather than surfacing it to the router. */ onRehandshake: (msg: OpaqueTransportMessage) => void; /** * A scheduled re-handshake went unanswered within its deadline. Only the server * arms this (via {@link SessionConnected.scheduleRehandshake}); it tears the * session down rather than keep serving a credential past its expiry. */ onRehandshakeTimeout?: () => void; onInvalidMessage: (reason: string) => void; } interface SessionConnectedProps extends IdentifiedSessionProps { conn: ConnType; listeners: SessionConnectedListeners; } declare class SessionConnected extends IdentifiedSession { readonly state: SessionState.Connected; conn: ConnType; listeners: SessionConnectedListeners; private heartbeatHandle?; private heartbeatWatchdog?; private lastInboundAt; private isActivelyHeartbeating; private rehandshakeTimer?; private credentialExpiry?; updateBookkeeping(ack: number, seq: number): void; private assertSendOrdering; send(msg: PartialTransportMessage): SendResult; constructor(props: SessionConnectedProps); sendBufferedMessages(): SendBufferResult; get loggingMetadata(): { protocolVersion?: ProtocolVersion | undefined; clientId?: string | undefined; connectedTo?: string | undefined; sessionId?: string | undefined; connId?: string | undefined; transportMessage?: Partial | undefined; validationErrors?: { path: string; message: string; }[] | undefined; tags?: Tags[] | undefined; telemetry?: { traceId: string; spanId: string; } | undefined; extras?: Record | undefined; }; /** * Arms the watchdog that closes the connection once the peer stops sending. * * A single interval for the lifetime of the session compares wall time against * the last inbound message, so a busy session does not allocate a timer per * frame. Elapsed time comes from {@link Date.now}, thus a throttled or suspended * timer can only delay detection of a dead connection, never report a heartbeat * as missed while messages keep arriving. */ startHeartbeatWatchdog(): void; private clearHeartbeatWatchdog; startActiveHeartbeat(): void; private sendHeartbeat; /** * Schedules the next proactive re-handshake from the credential's expiry. The * server calls this after each (re)validation, mirroring {@link startActiveHeartbeat}: * once armed the session drives the exchange itself — one handshake window before * expiry it sends a re-handshake request and waits for the response, firing * {@link SessionConnectedListeners.onRehandshakeTimeout} if none arrives in time. * Passing `undefined` (a credential that never expires) cancels any schedule. */ scheduleRehandshake(expiry: number | undefined): void; /** * Sends a re-handshake request immediately and arms the response deadline, * bypassing the expiry schedule. Returns false if the request couldn't be sent. */ requestRehandshakeNow(): boolean; private sendRehandshakeRequest; clearRehandshakeTimer(): void; onMessageData: (msg: Uint8Array) => void; _handleStateExit(): void; _handleClose(): void; } interface SessionBackingOffListeners extends IdentifiedSessionWithGracePeriodListeners { onBackoffFinished: () => void; } interface SessionBackingOffProps extends IdentifiedSessionWithGracePeriodProps { backoffMs: number; listeners: SessionBackingOffListeners; } declare class SessionBackingOff extends IdentifiedSessionWithGracePeriod { readonly state: SessionState.BackingOff; listeners: SessionBackingOffListeners; backoffTimeout?: ReturnType; constructor(props: SessionBackingOffProps); _handleClose(): void; _handleStateExit(): void; } type ClientSession = SessionNoConnection | SessionBackingOff | SessionConnecting | SessionHandshaking | SessionConnected; type ServerSession = SessionConnected | SessionNoConnection; type Session = ClientSession | ServerSession; declare const ProtocolError: { readonly RetriesExceeded: "conn_retry_exceeded"; readonly HandshakeFailed: "handshake_failed"; readonly MessageOrderingViolated: "message_ordering_violated"; readonly InvalidMessage: "invalid_message"; readonly MessageSendFailure: "message_send_failure"; }; type ProtocolErrorType = (typeof ProtocolError)[keyof typeof ProtocolError]; /** * Transport events. `HandshakeFailureCode` is the full set of codes observable * on handshake-failed protocol errors, including built-in and custom codes. */ interface EventMap { message: OpaqueTransportMessage; sessionStatus: { status: 'created' | 'closing'; session: Session; } | { status: 'closed'; session: Pick, 'id' | 'to'>; }; sessionTransition: { state: SessionState.Connected; id: SessionId; } | { state: SessionState.Handshaking; id: SessionId; } | { state: SessionState.Connecting; id: SessionId; } | { state: SessionState.BackingOff; id: SessionId; } | { state: SessionState.NoConnection; id: SessionId; }; protocolError: { type: (typeof ProtocolError)['HandshakeFailed']; code: HandshakeFailureCode; message: string; } | { type: Omit; message: string; }; transportStatus: { status: TransportStatus; }; } type EventTypes = keyof EventMap; type EventHandler = (event: EventMap[K]) => unknown; declare class EventDispatcher { private eventListeners; removeAllListeners(): void; numberOfListeners(eventType: K): number; addEventListener(eventType: K, handler: EventHandler): void; removeEventListener(eventType: K, handler: EventHandler): void; dispatchEvent(eventType: K, event: EventMap[K]): void; } /** * Represents the possible states of a transport. * @property {'open'} open - The transport is open and operational (note that this doesn't mean it is actively connected) * @property {'closed'} closed - The transport is permanently closed and cannot be reopened. */ type TransportStatus = 'open' | 'closed'; interface DeleteSessionOptions { unhealthy: boolean; } type SessionBoundSendFn = (msg: PartialTransportMessage) => string; /** * Advisory backpressure accessors scoped to a specific session, * see {@link Transport.getSessionBackpressure}. */ interface SessionBackpressure { isSendBufferFull: () => boolean; waitForSendBufferDrain: () => Promise; } /** * Transports manage the lifecycle (creation/deletion) of sessions * * ```plaintext * ▲ * incoming │ * messages │ * ▼ * ┌─────────────┐ 1:N ┌───────────┐ 1:1* ┌────────────┐ * │ Transport │ ◄─────► │ Session │ ◄─────► │ Connection │ * └─────────────┘ └───────────┘ └────────────┘ * ▲ * (may or may not be initialized yet) * │ * ▼ * ┌───────────┐ * │ Message │ * │ Listeners │ * └───────────┘ * ``` * @abstract */ declare abstract class Transport { /** * The status of the transport. */ private status; /** * The client ID of this transport. */ clientId: TransportClientId; /** * The event dispatcher for handling events of type EventTypes. */ eventDispatcher: EventDispatcher; /** * The options for this transport. */ protected options: TransportOptions; log?: Logger; tracer: Tracer; sessions: Map>; /** * Creates a new Transport instance. * @param codec The codec used to encode and decode messages. * @param clientId The client ID of this transport. */ constructor(clientId: TransportClientId, providedOptions?: ProvidedTransportOptions); bindLogger(fn: LogFn | Logger, level?: LoggingLevel): void; /** * Called when a message is received by this transport. * You generally shouldn't need to override this in downstream transport implementations. * @param message The received message. */ protected handleMsg(message: OpaqueTransportMessage): void; /** * Adds a listener to this transport. * @param the type of event to listen for * @param handler The message handler to add. */ addEventListener>(type: K, handler: T): void; /** * Removes a listener from this transport. * @param the type of event to un-listen on * @param handler The message handler to remove. */ removeEventListener>(type: K, handler: T): void; protected protocolError(message: EventMap['protocolError']): void; /** * Default close implementation for transports. You should override this in the downstream * implementation if you need to do any additional cleanup and call super.close() at the end. * Closes the transport. Any messages sent while the transport is closed will be silently discarded. */ close(): void; getStatus(): TransportStatus; protected createSession>(session: S): void; protected updateSession>(session: S): void; protected deleteSession(session: Session, options?: DeleteSessionOptions): void; protected onSessionGracePeriodElapsed(session: Session): void; protected onConnectingFailed(session: SessionConnecting): SessionNoConnection; protected onConnClosed(session: SessionHandshaking | SessionConnected): SessionNoConnection; /** * Gets a send closure scoped to a specific session. Sending using the returned * closure after the session has transitioned to a different state will be a noop. * * Session objects themselves can become stale as they transition between * states. As stale sessions cannot be used again (and will throw), holding * onto a session object is not recommended. */ getSessionBoundSendFn(to: TransportClientId, sessionId: SessionId): SessionBoundSendFn; /** * Gets advisory backpressure accessors scoped to a specific session, for * use by {@link Writable}s writing to that session. Unlike * {@link getSessionBoundSendFn}, these never throw once the session scope * has ended — they report "not full" / resolve immediately instead, since * the writable is torn down separately via session status events. */ getSessionBackpressure(to: TransportClientId, sessionId: SessionId): SessionBackpressure; } export { type SessionId as A, Connection as C, type DeleteSessionOptions as D, type EventHandler as E, LeakyBucketRateLimit as L, type ProvidedClientTransportOptions as P, type Session as S, Transport as T, type EventMap as a, type EventTypes as b, ProtocolError as c, type ProtocolErrorType as d, type ProvidedServerTransportOptions as e, SessionConnected as f, SessionConnecting as g, SessionHandshaking as h, SessionNoConnection as i, SessionState as j, type ProvidedTransportOptions as k, type TransportStatus as l, type ClientTransportOptions as m, type ClientHandshakeOptions as n, type ClientSession as o, SessionBackingOff as p, type ConnectionExtras as q, type ServerHandshakeOptions as r, createClientHandshakeOptions as s, createServerHandshakeOptions as t, CommonSession as u, type CommonSessionProps as v, type ServerTransportOptions as w, type ServerSession as x, type SessionBoundSendFn as y, type SessionOptions as z };