@nktkas/rews
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
WebSocketAPI, swap one line - Auto-reconnection — configurable retries and delay strategy
- Message buffering —
send()queues data while offline, flushes on reconnect - Persistent listeners —
addEventListenerandon*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/EventTargetpolyfills. On all versions, React Native's built-inAbortControllerpolyfill dropsterminationSignal.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.