/** * RelayClient — WebSocket + JSON-RPC 2.0 protocol + event dispatch. * * One instance = one persistent WebSocket connection to SignalWire RELAY. * * Architecture: * - JSON-RPC requests tracked by `id` in `_pending`; responses resolve deferreds * - signalwire.event messages ACKed back to server, then dispatched to Call/Message * - Result code checking accepts any 2xx (regex /^2\d{2}$/) * - signalwire.connect responses skip code checking * - Execute queue for requests while disconnected * - Auto-reconnect with exponential backoff */ import { Call } from './Call.js'; import { Message } from './Message.js'; import type { CallHandler, MessageHandler, RelayClientOptions } from './types.js'; type WsLike = { send(data: string): void; close(code?: number, reason?: string): void; on(event: string, listener: (...args: any[]) => void): void; removeAllListeners(): void; readyState: number; ping?(data?: any, mask?: boolean, cb?: (err: Error) => void): void; }; /** * Real-time WebSocket client for SignalWire RELAY. * * One instance = one persistent JSON-RPC 2.0 WebSocket connection. Lets you * place / receive calls and SMS, play/record media, run TTS, conference calls * together, and subscribe to platform events — all without HTTP polling. * * Authentication supports either a project/token pair or a JWT. * * @example Inbound-call handler * ```ts * import { RelayClient } from '@signalwire/sdk'; * * const client = new RelayClient({ * project: process.env.SIGNALWIRE_PROJECT_ID!, * token: process.env.SIGNALWIRE_API_TOKEN!, * host: 'example.signalwire.com', * contexts: ['office'], * }); * * client.onCall(async (call) => { * await call.answer(); * await call.playTTS({ text: 'Thanks for calling!' }); * await call.hangup(); * }); * * await client.connect(); * ``` * * @example Outbound dial + SMS * ```ts * await client.connect(); * const call = await client.dial({ * devices: [[{ type: 'phone', to: '+15551234567', from: '+15557654321' }]], * }); * await client.sendMessage({ to: '+15551234567', from: '+15557654321', body: 'Hi!' }); * ``` * * @see {@link Call} * @see {@link Message} */ export declare class RelayClient { /** Project ID used for Basic Auth. */ readonly project: string; /** API token used for Basic Auth. */ readonly token: string; /** JWT used instead of project/token if provided. */ readonly jwtToken: string; /** Hostname of the RELAY endpoint (e.g. `example.signalwire.com`). */ readonly host: string; /** * WebSocket scheme — `'wss'` (production) or `'ws'` (loopback audits). * See {@link RelayClientOptions.scheme} for the full rationale. */ readonly scheme: 'ws' | 'wss'; /** Contexts this client subscribes to for inbound events. */ readonly contexts: string[]; private _ws; private _pending; private _pendingMethods; private _calls; private _messages; private _pendingDials; private _dialCallsByTag; private _executeQueue; private _onCallHandler; private _onMessageHandler; private _onAnyEventHandler; private _connected; private _closing; private _reconnectDelay; private _relayProtocol; private _identity; private _authorizationState; private _pingInterval; private _pingFailures; private _serverPingTimeout; private _maxActiveCalls; private _shutdownDeferred; private _signalHandlers; /** * WebSocket constructor override for testing. * @internal */ _wsFactory: ((url: string) => WsLike) | null; /** * Create a new RELAY client. * Credentials fall back to the `SIGNALWIRE_PROJECT_ID`, `SIGNALWIRE_API_TOKEN`, * `SIGNALWIRE_JWT_TOKEN`, and `SIGNALWIRE_SPACE` env vars when omitted. * @param options - Optional client configuration. */ constructor(options?: RelayClientOptions); /** The protocol name the server assigned to this client after `connect()`. */ get relayProtocol(): string; /** * Async disposable support — equivalent to Python's `__aexit__`. * * Enables usage with `await using`: * ```ts * await using client = new RelayClient({ ... }); * await client.connect(); * // ... automatically disconnects when scope exits * ``` * * For environments without `await using`, use try/finally: * ```ts * const client = new RelayClient({ ... }); * try { await client.connect(); ... } * finally { await client.disconnect(); } * ``` */ [Symbol.asyncDispose](): Promise; /** * Register the inbound call handler. * * The handler is invoked once per inbound call, with a fully-formed * {@link Call} already in state `"created"`. Answer, reject, or forward * the call from inside the handler. * * @param handler - Callback invoked for each inbound call. May return a * promise; errors are logged but do not tear down the client. * @returns The same handler, to support decorator-style usage. */ onCall(handler: CallHandler): CallHandler; /** * Register the inbound message handler. * * The handler is invoked once per inbound SMS/MMS delivered to a subscribed * context, with a {@link Message} already in state `"received"`. * * @param handler - Callback invoked for each inbound message. May return a * promise; errors are logged but do not tear down the client. * @returns The same handler, to support decorator-style usage. */ onMessage(handler: MessageHandler): MessageHandler; /** * Register a low-level observer that fires for every inbound * `signalwire.event`, regardless of event type or whether a typed * Call / Message could be matched. * * Most users want {@link onCall} / {@link onMessage} — those deliver * typed objects and handle correlation. `onEvent` is the generic * escape hatch used by the porting-sdk audit harness to react to * every event the platform pushes. * * Fires BEFORE typed routing, so the same event will be observed here * AND on any matching {@link Call} / {@link Message}. * * @param handler - Callback receiving `(eventType, params)`. May be async. * @returns The same handler, for decorator-style usage. */ onEvent(handler: (eventType: string, params: Record) => void | Promise): typeof handler; /** * Connect to RELAY and authenticate. * * Opens the WebSocket, runs the JSON-RPC `signalwire.connect` handshake, * and starts the client-side ping loop. Safe to call again after a * `disconnect()` to reconnect; the process-wide concurrent-connection limit * is enforced here. * * @returns Resolves once the client is connected and authenticated. * @throws {Error} When the process-wide connection limit is reached, * authentication fails, or the WebSocket cannot be opened. */ connect(): Promise; /** Create a real WebSocket (uses dynamic import for ws). */ private _createWebSocket; private _setupWsListeners; /** Send signalwire.connect and wait for the response. */ private _authenticate; /** * Cleanly close the connection. * * Stops the ping loop, drops the WebSocket, rejects every pending request * and dial with a `Connection closed` {@link RelayError}, and removes the * client from the process-wide active set. Safe to call repeatedly. * * @returns Resolves once all resources have been released. */ disconnect(): Promise; /** * Send a JSON-RPC request and await the response. * * This is the low-level escape hatch for calling RELAY methods that don't * yet have a higher-level helper on this class. Queued if the client is * currently disconnected; sent immediately otherwise. * * @param method - Fully-qualified JSON-RPC method name (e.g. `"calling.play"`). * @param params - Method-specific params object. * @returns The `result` field of the JSON-RPC response. * @throws {RelayError} When the server returns a non-2xx code. */ execute(method: string, params: Record): Promise>; /** * Fire-and-forget JSON-RPC notification (no response awaited). * * Sends a JSON-RPC frame on the open socket without registering a * pending-response future. Used by the audit harness to emit a * `signalwire.event`-method frame the fixture watches for; production * users should always use {@link execute} instead so the response code * is checked. * * No-ops when the socket is closed. * * @param method - JSON-RPC method name. * @param params - Method params object. */ notify(method: string, params: Record): void; /** * Initiate an outbound call. * * Accepts a "dial plan" — an outer array of serial groups, each containing * an inner array of devices dialled in parallel. Resolves when any device * answers; rejects if no device answers within `dialTimeout`. * * @param devices - Serial/parallel dial plan. `[[A], [B, C]]` dials A first, * then B and C in parallel. * @param options - Optional dial behaviour overrides. * @param options.tag - Client-provided tag for event correlation. * Auto-generated (UUID) when omitted. * @param options.maxDuration - Maximum call duration in minutes. * @param options.dialTimeout - Seconds to wait for the dial to complete. * Defaults to `120`. * @returns A {@link Call} representing the answered leg. * @throws {Error} When the dial times out. * @throws {RelayError} When the server rejects the dial request. */ dial(devices: Record[][], options?: { tag?: string; maxDuration?: number; /** Dial timeout in seconds (default 120). */ dialTimeout?: number; }): Promise; /** * Send an outbound SMS / MMS message. * * The method returns as soon as the server has accepted the send; track the * message's terminal state with {@link Message.wait} or the `onCompleted` * callback. * * @param options - Send parameters. * @param options.toNumber - Destination phone number in E.164 format. * @param options.fromNumber - Sender phone number in E.164 format. * @param options.context - Context for receiving state events. Defaults to * the negotiated relay protocol. * @param options.body - Text body of the message. * @param options.media - List of media URLs for MMS. * @param options.tags - Tags attached to the message for correlation. * @param options.region - Origination region override. * @param options.onCompleted - Optional callback fired when the message * reaches a terminal state (delivered / failed / undelivered). * @returns A {@link Message} tracking the outbound send. * @throws {RelayError} When the server rejects the send request. */ sendMessage(options: { toNumber: string; fromNumber: string; context?: string; body?: string; media?: string[]; tags?: string[]; region?: string; onCompleted?: (event: any) => void | Promise; }): Promise; /** * Subscribe to additional RELAY contexts on an already-connected client. * * Inbound calls and messages on any of the listed contexts will be delivered * to the `onCall` / `onMessage` handlers. A no-op when `contexts` is empty. * * @param contexts - Context names to subscribe to. * @returns Resolves once the server has confirmed the subscription. * @throws {RelayError} When the server rejects the subscribe request. */ receive(contexts: string[]): Promise; /** * Unsubscribe from contexts previously passed to {@link receive} (or the * constructor). A no-op when `contexts` is empty. * * @param contexts - Context names to unsubscribe from. * @returns Resolves once the server has confirmed the unsubscribe. * @throws {RelayError} When the server rejects the unsubscribe request. */ unreceive(contexts: string[]): Promise; /** * Blocking entry point — connects and maintains the connection with * auto-reconnect, returning only after a clean shutdown (Ctrl+C, SIGTERM, * or {@link disconnect} from another scope). * * Installs `SIGINT` / `SIGTERM` handlers, so typically only one `RelayClient` * per process should call `run()`. * * @returns Resolves once a shutdown has been requested and cleanup completes. * @throws {Error} Only on unrecoverable startup failures — normal * disconnects are handled internally by the reconnect loop. */ run(): Promise; private _sendRequest; private _flushExecuteQueue; private _clearPendingRequests; private _handleMessage; private _handleEvent; private _handleInboundCall; private _handleInboundMessage; private _handleMessageState; private _registerDialLeg; private _handleDialEvent; private _sendPong; private _sendEventAck; private _startPingLoop; private _stopPingLoop; private _resetServerPingTimeout; private _cancelServerPingTimeout; private _forceClose; } export {};