/// /** * Reconnecting WebSocket with automatic retry logic. * * Features: * - **Drop-in replacement** — standard `WebSocket` API, swap one line * - **Auto-reconnection** — configurable retries and delay strategy * - **Message buffering** — `send()` queues data while offline, flushes on reconnect * - **Persistent listeners** — `addEventListener` and `on*` handlers survive reconnections * - **Dynamic URL & protocols** — resolve fresh values on each reconnection * - **Zero dependencies** — works in Node.js, Deno, Bun, and browsers * * @example * ```ts * import { ReconnectingWebSocket } from "@nktkas/rews"; * * const ws = new ReconnectingWebSocket("wss://api.example.com/ws", { * maxRetries: 10, * reconnectionDelay: (attempt) => Math.min(2 ** attempt * 100, 5_000), * }); * * ws.addEventListener("open", () => console.log("connected")); // fires on every reconnect * ws.addEventListener("message", (event) => console.log(event.data)); * * ws.send("hello"); // buffered while offline, sent once connected * ``` * * @module */ /** * Specifies the type of binary data being received over a `WebSocket` connection. * * - `"blob"`: Binary data is returned as `Blob` objects * - `"arraybuffer"`: Binary data is returned as `ArrayBuffer` objects * * @example * ```ts * // Setting up WebSocket for binary data as ArrayBuffer * const ws = new WebSocket("ws://localhost:8080"); * ws.binaryType = "arraybuffer"; * * ws.onmessage = (event) => { * if (event.data instanceof ArrayBuffer) { * // Process binary data * const view = new Uint8Array(event.data); * console.log(`Received binary data of ${view.length} bytes`); * } else { * // Process text data * console.log(`Received text: ${event.data}`); * } * }; * * // Sending binary data * const binaryData = new Uint8Array([1, 2, 3, 4]); * ws.send(binaryData.buffer); * ``` * * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType * @category WebSockets */ export type BinaryType = "blob" | "arraybuffer"; /** * Interface mapping `WebSocket` event names to their corresponding event types. * Used for strongly typed event handling with `addEventListener` and `removeEventListener`. * * @example * ```ts * // Using with TypeScript for strongly-typed event handling * const ws = new WebSocket("ws://localhost:8080"); * * ws.addEventListener("open", (event) => { * console.log("Connection established"); * }); * * ws.addEventListener("message", (event: MessageEvent) => { * console.log(`Received: ${event.data}`); * }); * ``` * * @category WebSockets */ export interface WebSocketEventMap { /** Connection closed. */ close: CloseEvent; /** Connection failed. */ error: Event; /** Data received. */ message: MessageEvent; /** Connection established. */ open: Event; } /** * Callback function invoked when an event fires. * * @category Events */ export interface EventListener { /** * The `EventListener` interface represents a callback function to be called * whenever an event of a specific type occurs on a target object. * * This is a basic event listener, represented by a simple function * that receives an Event object as its only parameter. * * @example * ```ts * // Create an event listener function * const handleEvent = (event: Event) => { * console.log(`Event of type "${event.type}" occurred`); * console.log(`Event phase: ${event.eventPhase}`); * * // Access event properties * if (event.cancelable) { * event.preventDefault(); * } * }; * * // Attach the event listener to a target * const target = new EventTarget(); * target.addEventListener('custom', handleEvent); * * // Or create a listener inline * target.addEventListener('message', (event) => { * console.log('Message received:', event); * }); * ``` * * @category Events */ (evt: Event): void; } /** * The `EventListenerObject` interface represents an object that can handle events * dispatched by an `EventTarget` object. * * This interface provides an alternative to using a function as an event listener. * When implementing an object with this interface, the `handleEvent()` method * will be called when the event is triggered. * * @example * ```ts * // Creating an object that implements `EventListenerObject` * const myEventListener = { * handleEvent(event: Event) { * console.log(`Event of type ${event.type} occurred`); * * // You can use 'this' to access other methods or properties * this.additionalProcessing(event); * }, * * additionalProcessing(event: Event) { * // Additional event handling logic * console.log('Additional processing for:', event); * } * }; * * // Using with any EventTarget (server or client contexts) * const target = new EventTarget(); * target.addEventListener('message', myEventListener); * * // Later, to remove it: * target.removeEventListener('message', myEventListener); * ``` * * @category Events */ export interface EventListenerObject { /** Called when a subscribed event fires. */ handleEvent(evt: Event): void; } /** * Event listener in either callback or object form. * * @category Events */ export type EventListenerOrEventListenerObject = EventListener | EventListenerObject; /** URL or factory function that returns a URL for the WebSocket connection (sync or async). */ export type UrlProvider = string | URL | (() => string | URL | Promise); /** Subprotocol(s) or factory function that returns subprotocol(s) for the WebSocket connection (sync or async). */ export type ProtocolsProvider = string | string[] | undefined | (() => string | string[] | undefined | Promise); /** Error code indicating the type of reconnection failure. */ export type ReconnectingWebSocketErrorCode = "RECONNECTION_LIMIT" | "RECONNECTION_DECLINED" | "TERMINATED_BY_USER" | "UNKNOWN_ERROR"; /** Configuration options for the {@link ReconnectingWebSocket}. */ export interface ReconnectingWebSocketOptions { /** * Maximum number of consecutive failed reconnection attempts. * * The counter resets after a connection stays open for {@link stableTimeout}. * * @default `Infinity` */ maxRetries?: number; /** * Maximum number of messages buffered while disconnected; sends beyond the limit are silently dropped. * * @default `Infinity` */ maxEnqueuedMessages?: number; /** * Maximum time in ms to wait for a connection to open. Set to `null` to disable. * * Does not limit the time spent in url/protocols factories. * * @default `10_000` */ connectionTimeout?: number | null; /** * Time in ms a connection must stay open before the retry counter resets. * * @default `3_000` */ stableTimeout?: number; /** * Delay before reconnection in ms, or a function of the attempt number (0-based). * * @default Exponential backoff `2 ** n * 150` capped at 10s, with equal jitter. */ reconnectionDelay?: number | ((attempt: number) => number); /** * Decide whether to reconnect after a non-user closure. Return `false` to permanently terminate. * * Receives the close event and the attempt number (0-based). * Not consulted for `close()` and `reconnect()` calls. * * @default `() => true` */ shouldReconnect?: (event: CloseEvent, attempt: number) => boolean; } /** * Error thrown when reconnection fails in {@link ReconnectingWebSocket}. * * @example * ```ts * import { ReconnectingWebSocket, ReconnectingWebSocketError } from "@nktkas/rews"; * * const ws = new ReconnectingWebSocket("wss://api.example.com/ws"); * * ws.terminationSignal.aborted; // boolean * ws.terminationSignal.reason; // ReconnectingWebSocketError, once aborted * * ws.terminationSignal.addEventListener("abort", () => { * ws.terminationSignal.reason.code; // ReconnectingWebSocketErrorCode * ws.terminationSignal.reason.cause; // original error, if any * }); * ``` */ export declare class ReconnectingWebSocketError extends Error { /** * Error code indicating the type of reconnection error: * - `RECONNECTION_LIMIT`: Maximum reconnection attempts reached. * - `RECONNECTION_DECLINED`: `shouldReconnect` returned `false`. * - `TERMINATED_BY_USER`: Closed via `close()` method. * - `UNKNOWN_ERROR`: Unhandled error outside a connection attempt (e.g. in `reconnectionDelay` or `shouldReconnect`). */ readonly code: ReconnectingWebSocketErrorCode; /** * Create a termination error. * * @param code Reconnection failure code. * @param cause Underlying error, stored as [`Error#cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause). */ constructor(code: ReconnectingWebSocketErrorCode, cause?: unknown); } export interface ReconnectingWebSocket { addEventListener(type: K, listener: (this: ReconnectingWebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; removeEventListener(type: K, listener: (this: ReconnectingWebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; } /** * Drop-in replacement for [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) that automatically reconnects. * * @example * ```ts * import { ReconnectingWebSocket } from "@nktkas/rews"; * * const ws = new ReconnectingWebSocket("wss://api.example.com/ws", { * maxRetries: 10, * reconnectionDelay: (attempt) => Math.min(2 ** attempt * 100, 5_000), * }); * * ws.addEventListener("open", () => console.log("connected")); // fires on every reconnect * ws.addEventListener("message", (event) => console.log(event.data)); * * ws.send("hello"); // buffered while offline, sent once connected * ``` */ export declare class ReconnectingWebSocket extends EventTarget implements WebSocket { /** URL provider for creating new connections. */ private readonly _urlProvider; /** Protocols provider for creating new connections. */ private readonly _protocolsProvider; /** Current underlying WebSocket instance. */ private _socket; /** Binary data type for the WebSocket. */ private _binaryType; /** Buffer for messages sent while disconnected. */ private _messageBuffer; /** Current reconnection attempt number. */ private _retryCount; /** Resolves the in-flight {@link _awaitSocketLifecycle} promise; `undefined` when idle. */ private _settleLifecycle; /** Resolves the in-flight retry delay; `undefined` when not sleeping. */ private _skipDelay; /** Set while a user-requested reconnect closes the current socket; that close is not counted as a retry. */ private _reconnectRequested; /** Skips the upcoming retry delay; set by {@link reconnect} when no sleep is active. */ private _skipNextDelay; /** Attribute-style listener for the `close` event. */ private _onclose; /** Attribute-style listener for the `error` event. */ private _onerror; /** Attribute-style listener for the `message` event. */ private _onmessage; /** Attribute-style listener for the `open` event. */ private _onopen; /** Map of currently active attribute-style listeners. */ private _attributeListeners; /** Controller used to signal permanent termination. */ private readonly _abortController; /** Reconnection configuration options. Read on every attempt, so changes apply live. */ reconnectOptions: Required; /** * AbortSignal that is aborted when the instance is permanently terminated. * The abort reason is always a {@link ReconnectingWebSocketError}. */ get terminationSignal(): AbortSignal; /** * Create a new ReconnectingWebSocket with URL and options. * * @param url URL or factory function for the WebSocket connection. * @param options Configuration options. * * @throws {TypeError} If no WebSocket implementation is available. */ constructor(url: UrlProvider, options?: ReconnectingWebSocketOptions); /** * Create a new ReconnectingWebSocket with URL, protocols and options. * * @param url URL or factory function for the WebSocket connection. * @param protocols Subprotocol(s) or factory function. * @param options Configuration options. * * @throws {TypeError} If no WebSocket implementation is available. */ constructor(url: UrlProvider, protocols?: ProtocolsProvider, options?: ReconnectingWebSocketOptions); /** Run the main reconnection loop. */ private _runLoop; /** Resolve the connection URL and protocols from their providers. */ private _resolveEndpoint; /** * Close the socket and settle its lifecycle if it does not open within the timeout. * * @param socket Socket to monitor. */ private _armConnectionTimeout; /** Await the full lifecycle of the given socket until it closes. */ private _awaitSocketLifecycle; /** Flush buffered messages; a message whose send() throws is dropped. */ private _flushBuffer; /** Wait between retries; cut short by reconnect() and termination. */ private _delay; /** Close the current socket and settle its lifecycle if it was still connecting. */ private _dropSocket; /** Permanently terminate the instance and clean up resources. */ private _terminate; /** * Returns the URL that was used to establish the WebSocket connection. * * --- * * `""` before the first connection for a factory. */ get url(): string; /** * Returns the state of the WebSocket object's connection. It can have the values described below. * * --- * * `CLOSED` only after permanent termination; any reconnection phase reports `CONNECTING`. */ get readyState(): 0 | 1 | 2 | 3; /** Number of consecutive failed reconnection attempts; resets after a connection stays open for `stableTimeout`. */ get retryCount(): number; /** * Returns the number of bytes of application data (UTF-8 text and binary data) that have been queued using send() but not yet been transmitted to the network. * * If the WebSocket connection is closed, this attribute's value will only increase with each call to the send() method. (The number does not reset to zero once the connection closes.) * * ```ts * const ws = new WebSocket("ws://localhost:8080"); * ws.send("Hello, world!"); * console.log(ws.bufferedAmount); // 13 * ``` */ get bufferedAmount(): number; /** * Returns the extensions selected by the server, if any. * * WebSocket extensions add optional features negotiated during the handshake via * the `Sec-WebSocket-Extensions` header. * * At the time of writing, there are two registered extensions: * * - [`permessage-deflate`](https://www.rfc-editor.org/rfc/rfc7692.html): Enables per-message compression using DEFLATE. * - [`bbf-usp-protocol`](https://usp.technology/): Used by the Broadband Forum's User Services Platform (USP). * * See the full list at [IANA WebSocket Extensions](https://www.iana.org/assignments/websocket/websocket.xml#extension-name). * * Example: * * ```ts * const ws = new WebSocket("ws://localhost:8080"); * console.log(ws.extensions); // e.g., "permessage-deflate" * ``` */ get extensions(): string; /** * Returns the subprotocol selected by the server, if any. * It can be used in conjunction with the array form of the constructor's second argument to perform subprotocol negotiation. */ get protocol(): string; /** * Returns a string that indicates how binary data from the WebSocket object is exposed to scripts: * * Can be set, to change how binary data is returned. The default is "blob". * * ```ts * const ws = new WebSocket("ws://localhost:8080"); * ws.binaryType = "arraybuffer"; * ``` */ get binaryType(): BinaryType; set binaryType(value: BinaryType); /** Connection is being established. */ readonly CONNECTING = 0; /** Connection is open and ready to communicate. */ readonly OPEN = 1; /** Connection is in the process of closing. */ readonly CLOSING = 2; /** Connection is closed. */ readonly CLOSED = 3; /** Connection is being established. */ static readonly CONNECTING = 0; /** Connection is open and ready to communicate. */ static readonly OPEN = 1; /** Connection is in the process of closing. */ static readonly CLOSING = 2; /** Connection is closed. */ static readonly CLOSED = 3; /** Attribute-style handler for `close` events. */ get onclose(): ((this: WebSocket, ev: CloseEvent) => any) | null; /** Set the attribute-style handler for `close` events. */ set onclose(handler: ((this: WebSocket, ev: CloseEvent) => any) | null); /** Attribute-style handler for `error` events. */ get onerror(): ((this: WebSocket, ev: Event) => any) | null; /** Set the attribute-style handler for `error` events. */ set onerror(handler: ((this: WebSocket, ev: Event) => any) | null); /** Attribute-style handler for `message` events. */ get onmessage(): ((this: WebSocket, ev: MessageEvent) => any) | null; /** Set the attribute-style handler for `message` events. */ set onmessage(handler: ((this: WebSocket, ev: MessageEvent) => any) | null); /** Attribute-style handler for `open` events. */ get onopen(): ((this: WebSocket, ev: Event) => any) | null; /** Set the attribute-style handler for `open` events. */ set onopen(handler: ((this: WebSocket, ev: Event) => any) | null); /** Attach or detach the dispatcher for an attribute-style event handler. */ private _setAttributeListener; /** * Closes the WebSocket connection, optionally using code as the WebSocket connection close code and reason as the WebSocket connection close reason. * * @param code Status code for the closure. * @param reason Human-readable reason for the closure. * * @throws {DOMException} If the code is not 1000 or in 3000-4999, or the reason is longer than 123 UTF-8 bytes. * * @example * ```ts ignore * ws.close(1000, "work complete"); * ``` * * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close */ close(code?: number, reason?: string): void; /** * Drop the current connection and reconnect immediately. * * - The closed socket does not count towards `maxRetries`. * - The current or next retry delay is skipped. * - Cannot interrupt an in-flight url/protocols factory. * - Does nothing once permanently terminated. * * @param code Status code for closing the current socket. * @param reason Human-readable reason for the closure. * * @throws {DOMException} If the code is not 1000 or in 3000-4999, or the reason is longer than 123 UTF-8 bytes. * * @example * ```ts ignore * ws.reconnect(4000, "auth token refreshed"); * ``` */ reconnect(code?: number, reason?: string): void; /** * Transmits data using the WebSocket connection. data can be a string, a Blob, an ArrayBuffer, or an ArrayBufferView. * * If the connection is not open, the data is buffered (up to `maxEnqueuedMessages`) * and sent once it is established. After permanent termination, data is silently discarded. * * @param data Payload to transmit or buffer. * * @example * ```ts ignore * ws.send("hello"); * ``` * * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send */ send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void; }