npm.io
4.1.0 • Published 5d ago

@nktkas/rews

Licence
MIT
Version
4.1.0
Deps
0
Size
109 kB
Vulns
0
Weekly
0
Stars
1

@nktkas/rews

npm JSR bundlejs

Drop-in WebSocket replacement with automatic reconnection.


Without rews — manual reconnection, listener re-attachment, message queuing:

let ws: WebSocket;
let attempts = 0;
const queue: string[] = [];
const onMessage = (e: MessageEvent) => console.log(e.data);

function connect() {
  ws = new WebSocket("wss://example.com");
  ws.addEventListener("message", onMessage);
  ws.onopen = () => {
    attempts = 0;
    while (queue.length) ws.send(queue.shift()!);
  };
  ws.onclose = () => {
    if (attempts++ < 3) setTimeout(connect, 1000);
  };
}

function send(data: string) {
  ws.readyState === WebSocket.OPEN ? ws.send(data) : queue.push(data);
}

connect();
send("hello");

With rews — standard WebSocket API, no changes needed:

import { ReconnectingWebSocket } from "@nktkas/rews";

const ws = new ReconnectingWebSocket("wss://example.com");
ws.addEventListener("message", (e) => console.log(e.data));
ws.send("hello");

Features

  • Drop-in replacement — standard WebSocket API, swap one line
  • Auto-reconnection — configurable retries and delay strategy
  • Message bufferingsend() queues data while offline, flushes on reconnect
  • Persistent listenersaddEventListener and on* handlers survive reconnections
  • Dynamic URL & protocols — resolve fresh values on each reconnection
  • Zero dependencies — works in Node.js, Deno, Bun, and browsers

Installation

# Node.js 22.12+ / React Native 0.86+
npm i @nktkas/rews

# Deno 2+
deno add jsr:@nktkas/rews

# Bun 1+
bun add @nktkas/rews

React Native below 0.86 requires Event / EventTarget polyfills. On all versions, React Native's built-in AbortController polyfill drops terminationSignal.reason.

Options

interface ReconnectingWebSocketOptions {
  /** Maximum number of consecutive failed reconnection attempts. @default Infinity */
  maxRetries?: number;
  /** Maximum number of messages buffered while disconnected. @default Infinity */
  maxEnqueuedMessages?: number;
  /** Connection timeout in ms (null to disable). @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 attempt number. @default exponential backoff with jitter, max 10s */
  reconnectionDelay?: number | ((attempt: number) => number);
  /** Decide whether to reconnect after a non-user closure. @default () => true */
  shouldReconnect?: (event: CloseEvent, attempt: number) => boolean;
}

Beyond Standard WebSocket

Dynamic URL & Protocols

url and protocols accept functions (sync or async), invoked on each reconnection:

const ws = new ReconnectingWebSocket(
  () => `wss://example.com?token=${getToken()}`,
  () => ["v2"],
);

Errors thrown by these functions count as failed connection attempts and follow the normal retry flow. The same applies to a permanently invalid URL: with the default maxRetries: Infinity it is retried forever, each close event carrying the error in its reason.

Event Lifecycle

Standard open, close, error, and message events fire on every connection cycle — not just the first one. A single ReconnectingWebSocket instance may emit multiple open/close pairs over its lifetime as it reconnects.

ws.addEventListener("open", () => console.log("connected")); // fires on each (re)connection
ws.addEventListener("close", () => console.log("disconnected")); // fires on each disconnection

// use { once: true } if you only need the first occurrence
ws.addEventListener("open", () => init(), { once: true });
readyState

CLOSED means permanently terminated. While reconnecting — a retry pause, a connection attempt, a url/protocols factory await — readyState is CONNECTING; CLOSING is never reported.

retryCount

Number of consecutive failed reconnection attempts; resets after a connection stays open for stableTimeout.

Termination

terminationSignal is an AbortSignal that aborts when the connection is permanently closed. The abort reason is always a ReconnectingWebSocketError:

Code Description
RECONNECTION_LIMIT Max retries exceeded
RECONNECTION_DECLINED shouldReconnect returned false
TERMINATED_BY_USER close() called
UNKNOWN_ERROR Unhandled error in reconnectionDelay/shouldReconnect
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
});

Being a standard AbortSignal, it composes with AbortSignal.any(), fetch(), addEventListener(..., { signal }), and other platform APIs.

Closing Behavior
ws.close(code?, reason?); // permanently close — the standard WebSocket method
ws.reconnect(code?, reason?); // drop the current connection and reconnect immediately

reconnect() skips the current retry delay, is not counted towards maxRetries, and does nothing once the instance is permanently terminated.

License

@nktkas/rews is licensed under the MIT License.

Copyright 2025-present nktkas and contributors.

Keywords