interface SubscribeOptions { /** * When true, the server stamps each broadcast message on this * channel with the sender's JWT `peerMetadata` claim as * `fromMetadata`. Opt-in because the metadata may contain customer * PII (display name, avatar URL) that not every subscriber needs. * Default false. */ includeSenderMetadata?: boolean; } type PublishOptions = Record; type SendOptions = Record; interface WelcomePayload { peerId: string; /** Unix-second expiry of the JWT, or null for pk_ keys (no expiry). */ expiresAt: number | null; serverTime: number; maxMessageSize: number; /** * Opaque metadata pass-through from the JWT `metadata` claim. The * wire protocol treats this as a generic JSON bag — the server * doesn't interpret it. WebRTC customers put TURN credentials at * `metadata.iceServers` by convention; the SDK reads them from there * and surfaces them on the `connected` event for the WebRTC layer to * consume. */ metadata?: Record; } /** * JSON-serializable mirror of the DOM `RTCIceServer` dictionary. * Declared here (rather than importing the DOM type) so this file * stays a pure wire-protocol definition usable in Node + non-DOM * contexts. Not a wire-protocol field itself — peer-lib extracts * iceServers from `metadata.iceServers` by convention. */ interface IceServerConfig { urls: string | string[]; username?: string; credential?: string; } /** Channel broadcast — server-side type is "message". */ interface MessageEvent { channel: string; from: string; fromMetadata?: Record; data: unknown; } /** Direct point-to-point send. Server-side type is "direct". */ interface DirectMessageEvent { from: string; fromMetadata?: Record; data: unknown; } interface PresencePeer { peerId: string; metadata?: Record; } interface PresenceEvent { channel: string; joined: PresencePeer[]; left: PresencePeer[]; } interface ServerErrorEvent { /** Echo of the client's `requestId` when correlatable. */ requestId?: string; /** * Machine-readable error code. The `ErrorCode` union lists the * codes known to this SDK version; the field is deliberately open * (`string`) so a NEWER server's codes still flow through — a * correlated request rejects immediately with the real code instead * of timing out, and uncorrelated ones surface on `server-error`. * Branch with equality checks, not exhaustive switches. * * (`ErrorCode | (string & {})` keeps IDE autocomplete for the known * codes while accepting any string.) */ code: ErrorCode | (string & {}); message?: string; } interface GoingAwayEvent { /** Hint for clients with custom backoff — typical value 1000. */ retryAfterMs: number; } /** Stable machine-readable error codes from the server's `error` frames. */ type ErrorCode = "malformed_message" | "unknown_type" | "invalid_channel" | "invalid_peer_id" | "channel_not_authorized" | "channel_reserved" | "channel_limit_exceeded" | "peer_not_found" | "missing_data" | "action_not_permitted" | "over_message_quota"; /** * WebSocket close codes the server may emit. Re-exported as a const * object (NOT enum) for tree-shakability and so customers can branch * on a runtime value without importing types-only. */ declare const WsCloseCode: { readonly GoingAway: 1001; readonly PolicyViolation: 1008; readonly MessageTooBig: 1009; readonly ClientInactivity: 4000; readonly InvalidToken: 4001; readonly TokenExpired: 4002; readonly ChannelNotAuthorized: 4003; readonly OverConcurrentLimit: 4010; readonly OverMessageRate: 4011; readonly AccountSuspended: 4012; readonly AdminDisconnect: 4020; }; type WsCloseCode = (typeof WsCloseCode)[keyof typeof WsCloseCode]; /** * Error thrown when the server closes the connection with a 4xxx * close code (auth failure, quota exceeded, admin kick). The * `closeCode` field lets customers branch on the specific reason. * * `message` is a fixed string that NEVER interpolates server-supplied * `reason` text — a malicious server could otherwise stuff control * characters or HTML-ish content into a customer's * `console.error(err.message)` output, or into a UI surface that * renders `error.message`. The full server reason is available as * the `closeReason` field for customers who explicitly need it. */ declare class SignallingConnectError extends Error { readonly closeCode: number; readonly closeReason: string; constructor(closeCode: number, closeReason: string); } /** * Thrown by `publish()` / `send()` when the JSON-serialized payload * exceeds the server's per-message cap (advertised on the `connected` * event as `maxMessageSize`). Rejecting client-side keeps the * connection alive — an oversized frame reaching the server would * close the socket with code 1009 and take every subscription down * with it. Branch on `instanceof` or `err.name === * "OversizedPayloadError"`; the `size` / `cap` fields carry the * measured payload size and the active limit in bytes. */ declare class OversizedPayloadError extends Error { readonly name = "OversizedPayloadError"; readonly size: number; readonly cap: number; constructor(size: number, cap: number); } /** Drops every log. Default when the customer doesn't supply one. */ declare const NoopLogger: Logger; /** * Routes to globalThis.console with level-named methods. Useful in * dev; customers should swap in their structured logger for prod. */ declare const ConsoleLogger: Logger; type Handler

= (payload: P) => void; interface EventEmitterOptions { /** * Invoked when a listener throws. Defaults to console.error so * the failure is visible during development; production consumers * can swap to their logger via the SignallingClient logger option. */ onListenerError?: (err: unknown, eventName: string) => void; /** * Warn (via onListenerWarn or the default console.warn) when an * event's listener count crosses this threshold — typical signal * for a React-useEffect-without-cleanup leak. Default 100. The * warning fires ONCE per event-name as the count crosses the * threshold; further additions don't re-warn until the count drops * below and crosses again. */ maxListenersWarn?: number; onListenerWarn?: (msg: string, ctx: Record) => void; } declare class EventEmitter { private readonly listeners; private readonly onListenerError; private readonly maxListenersWarn; private readonly onListenerWarn; private readonly warnedEvents; constructor(opts?: EventEmitterOptions); on(event: K, handler: Handler): this; /** * Remove a previously-registered handler. Noop if the handler ref * isn't currently registered (matches Node's behaviour — silent). * Only removes the FIRST matching entry so `on(e, h); on(e, h);` * needs two off() calls to fully unregister, again matching Node. */ off(event: K, handler: Handler): this; /** * Emit a one-shot warning when an event's listener count crosses * the threshold for the first time (or after dropping back below * and re-crossing). Customers add listeners in many places — * React useEffects, addEventListener-style integrations — so a * runaway count usually points at a missing cleanup hook. */ private checkListenerCountThreshold; /** * Register a one-shot handler. The wrapper auto-removes itself * BEFORE invoking the customer's handler, so a handler that * re-emits the same event won't infinite-loop (defensive — Node * orders the cleanup before invocation for the same reason). */ once(event: K, handler: Handler): this; /** * Synchronous dispatch in registration order. Snapshots the list * first so a handler that calls .off() on itself or another * listener during dispatch doesn't trip the iteration. */ emit(event: K, payload: EventMap[K]): boolean; /** Test / introspection helper. */ listenerCount(event: K): number; /** Remove every handler for every event. */ removeAllListeners(): this; } /** * Per-remote-peer lifecycle state. Coarser than the underlying * RTCPeerConnection states so the public API stays stable across * spec evolution. Mapping (driven by connectionState): * * idle: constructed but no negotiation kicked off yet * connecting: RTCPeerConnection.connectionState in {new, connecting} * connected: connectionState === "connected" * reconnecting: connectionState === "disconnected" (transient) * OR "failed" (ICE restart pending) * closed: connectionState === "closed" — terminal */ type PeerConnectionState = "idle" | "connecting" | "connected" | "reconnecting" | "closed"; interface PeerConnectionLogger { debug?(msg: string, ctx?: Record): void; warn?(msg: string, ctx?: Record): void; error?(msg: string, ctx?: Record): void; } /** * Subset of RTCPeerConnection the wrapper uses. Production passes * the platform constructor (browser globalThis.RTCPeerConnection or * node's `wrtc` binding) via `rtcPeerConnectionFactory`. Tests pass * a fake. Structural compat: a real RTCPeerConnection satisfies this * by virtue of being a superset. * * Defined explicitly (not `Pick`) because the * DOM types include legacy callback-style overloads on createOffer / * createAnswer / setLocalDescription that the fake can't satisfy. * Only the modern Promise-based signatures matter here. */ interface RTCPeerConnectionLike { readonly connectionState: RTCPeerConnectionState; readonly signalingState: RTCSignalingState; readonly iceConnectionState: RTCIceConnectionState; readonly localDescription: RTCSessionDescription | null; readonly remoteDescription: RTCSessionDescription | null; createOffer(options?: RTCOfferOptions): Promise; createAnswer(options?: RTCAnswerOptions): Promise; setLocalDescription(description?: RTCSessionDescriptionInit): Promise; setRemoteDescription(description: RTCSessionDescriptionInit): Promise; addIceCandidate(candidate?: RTCIceCandidateInit | null): Promise; addTrack(track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender; removeTrack(sender: RTCRtpSender): void; getSenders(): RTCRtpSender[]; createDataChannel(label: string, dataChannelDict?: RTCDataChannelInit): RTCDataChannel; /** * Update ICE servers (typically refreshed TURN credentials) on a * live connection. The spec restricts which fields can change here * — iceServers is one that's allowed; bundlePolicy isn't, post-construction. */ setConfiguration(configuration?: RTCConfiguration): void; addEventListener(type: string, listener: (ev: unknown) => void): void; removeEventListener(type: string, listener: (ev: unknown) => void): void; close(): void; } /** * Subset of RTCDataChannel the DataChannel wrapper relies on. * Production: the underlying RTCDataChannel returned by * RTCPeerConnection.createDataChannel or surfaced via the * `datachannel` event. Tests: a fake supplying the same surface. * * Mutable bufferedAmountLowThreshold (the spec has it writable). All * other state is read-only on RTCDataChannel. */ interface RTCDataChannelLike { readonly label: string; readonly readyState: RTCDataChannelState; readonly bufferedAmount: number; bufferedAmountLowThreshold: number; send(data: string | ArrayBuffer): void; close(): void; addEventListener(type: string, listener: (ev: unknown) => void): void; removeEventListener(type: string, listener: (ev: unknown) => void): void; } interface DataChannelOptions { /** * Backpressure ceiling. `send()` won't actually call the underlying * channel.send() until `bufferedAmount` is below this number. Default * 1_048_576 (1 MB) — a reasonable browser-default cap that avoids * unbounded memory growth when the producer outruns the link. */ maxBufferedAmount?: number; /** * Threshold at which the channel emits `bufferedamountlow`. Default * is `maxBufferedAmount / 2`. The wrapper sets this on the underlying * channel; customers can override per-instance if they need finer * control over the drain cadence. */ bufferedAmountLowThreshold?: number; /** * Maximum sends queued behind the backpressure chain. Past this, * `send()` rejects synchronously with `DataChannelOverflowError` * rather than letting the JS heap grow unboundedly while waiting * on a stalled link. Default 256. */ maxQueuedSends?: number; logger?: PeerConnectionLogger; } interface DataChannelEventMap { open: void; /** Fired when the channel transitions to "closed". Mirrors RTCDataChannel.close event. */ close: void; /** Underlying-channel error. Distinct from `send()` rejection (which is per-call). */ error: { err: Error; }; /** Inbound payload. */ message: { data: string | ArrayBuffer; }; } interface RemotePeerEventMap { "state-change": { from: PeerConnectionState; to: PeerConnectionState; }; /** * SDP negotiation flow (createOffer / createAnswer / * setRemoteDescription) threw. Usually transient — negotiation * recovers on the next exchange and no action is needed. * * The one terminal case is `err.name === "IceRestartExhaustedError"`: * the SDK's automatic ICE-restart budget for this peer is spent and * the connection will not recover on its own. Branch on `err.name` * (message strings are not a stable contract) and recover by closing * and re-joining, or by prompting the user. */ "negotiation-error": { err: Error; }; /** Inbound remote ICE candidate rejected by addIceCandidate (usually benign stale candidate). */ "ice-candidate-error": { err: Error; }; /** * Remote added a track. Customer code attaches to MediaElement. * * `metadata` is populated if the sender stamped per-track metadata * via `peer.addTrack(t, s, meta)` / `peer.addStream(s, meta)`. The * metadata arrives via a separate signalling message ordered BEFORE * the SDP offer that delivers the track, so it's stashed by the * time this event fires. */ track: { track: MediaStreamTrack; streams: ReadonlyArray; metadata?: StreamMetadata; }; /** Remote opened a DataChannel. */ "data-channel": { channel: RTCDataChannel; }; /** * Fires the first time a previously-unseen MediaStream id appears * on this peer. Symmetric with the sender's `addStream(stream, meta)`. * * For uniform-metadata streams (the addStream path), `metadata` is * the metadata the sender passed. For mixed-metadata streams (the * advanced addTrack-per-track path), `metadata` is whatever the * FIRST track received carried — for per-track precision use the * per-track `metadata` field on the `track` event. * * Re-fires after a transient WS drop + reconcile: the remote sender * is the same RemotePeer, but the underlying PC was swapped, so the * new PC's track events surface fresh MediaStream objects with the * same `stream.id` as before. Customers binding `