Package Exports
- tiny-resilient-websocket
- tiny-resilient-websocket/package.json
Readme
resilient-websocket
A tiny, dependency-free WebSocket client with everything you actually need in production:
- Exponential-backoff reconnect with jitter, capped by attempts and/or wall-clock duration.
- Pluggable transport — works in browsers, Node (
ws), Deno, Bun, and edge runtimes. - Dynamic URL provider for token refresh on every connection attempt.
- Heartbeats with stale-connection detection.
- Outgoing message queue that survives reconnects.
- First-class state machine with hooks and a typed
on()/off()API. - ~6 KB minified, zero runtime dependencies, full TypeScript types.
npm install resilient-websocket
# or: pnpm add resilient-websocket
# or: yarn add resilient-websocketQuick start
Browser
import { ResilientWebSocketClient } from 'resilient-websocket';
const client = new ResilientWebSocketClient({
url: 'wss://example.com/feed',
onConnected: (socket) => {
socket.send(JSON.stringify({ type: 'subscribe', channel: 'ticks' }));
},
hooks: {
onMessage: (data) => console.log('msg', data),
onClose: (event, willReconnect) => console.log('closed', event.code, { willReconnect }),
},
policy: {
initialReconnectDelayMs: 500,
maxReconnectDelayMs: 30_000,
maxReconnectDurationMs: 5 * 60_000,
},
});
await client.connect();Node.js (with the ws package)
import WebSocket from 'ws';
import { ResilientWebSocketClient } from 'resilient-websocket';
const client = new ResilientWebSocketClient({
urlProvider: async () => `wss://example.com/feed?token=${await getToken()}`,
webSocketFactory: (url, protocols) =>
new WebSocket(url, protocols, { headers: { 'User-Agent': 'my-app/1.0' } }),
heartbeat: { intervalMs: 15_000, pongTimeoutMs: 5_000 },
});
await client.connect();API
new ResilientWebSocketClient(options)
Required (one of):
url: string— static endpoint.urlProvider: () => string | Promise<string>— invoked before every connect attempt. Use for token refresh.
Optional:
| Option | Type | Default | Description |
|---|---|---|---|
protocols |
string | string[] |
— | WebSocket subprotocols. |
webSocketFactory |
WebSocketFactory |
global WebSocket |
Override the constructor (Node, mocks, custom headers). |
binaryType |
'arraybuffer' | 'blob' |
'arraybuffer' |
Underlying socket binaryType. |
onConnected |
(socket) => void |
— | Called on every successful open (good place for subscribe frames). |
queueOutgoingWhileDisconnected |
boolean |
true |
Buffer send() while disconnected and flush on next open. |
maxQueuedMessages |
number |
1000 |
Drop oldest beyond this cap. |
policy |
ResilientWebSocketPolicy |
see below | Backoff & give-up policy. |
heartbeat |
ResilientWebSocketHeartbeat |
off | Enable application-level keep-alive. |
hooks |
ResilientWebSocketHooks |
— | Lifecycle hooks. |
logger |
ResilientWebSocketLogger |
— | console-shaped logger for breadcrumbs. |
Policy defaults
| Field | Default |
|---|---|
maxReconnectAttempts |
Infinity |
maxReconnectDurationMs |
Infinity |
initialReconnectDelayMs |
1_000 |
maxReconnectDelayMs |
30_000 |
reconnectBackoffMultiplier |
2 |
reconnectJitterRatio |
0.15 |
Methods
connect(): Promise<void>— start (or restart afterstop()/ give-up). Resolves on nextopen, rejects on give-up.stop(code?, reason?): void— soft shutdown: closes the socket and prevents reconnects, but keepson()listeners and queued messages so a follow-upconnect()resumes with the same wiring.disconnect(options?): void— full teardown:stop()+ clears the outgoing queue + removes every listener registered viaon()/once()+ resets state to'idle'. Use this to "unsubscribe everything" before reusing the instance with fresh listeners or discarding it.reconnect(): Promise<void>— force-close and reconnect immediately.send(data): boolean—trueif sent,falseif queued.clearQueue(): void— empty the outgoing queue.on(event, listener) / off(event, listener) / once(event, listener)— typed event subscriptions.removeAllListeners(): void
stop() vs disconnect() — at a glance
| Behavior | stop() |
disconnect() |
|---|---|---|
| Closes the socket | ✓ | ✓ |
| Cancels reconnect / heartbeat timers | ✓ | ✓ |
Rejects pending connect() promises |
✓ | ✓ |
| Keeps queued outgoing messages | ✓ | — (cleared) |
Keeps on()/once() listeners |
✓ | — (removed) |
| Final state | closed |
idle |
Getters
state—'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed' | 'gaveUp'isConnected—truewhen the socket isOPEN.hasGivenUp—trueafter policy give-up.queuedMessageCountrawSocket— the underlyingWebSocketLike, ornull.
Events
| Event | Payload |
|---|---|
open |
void |
message |
{ data: string; event: MessageEvent } |
error |
Event |
close |
{ event: CloseEvent; willReconnect: boolean } |
reconnectScheduled |
{ attempt: number; delayMs: number } |
giveUp |
{ reason; reconnectAttempts; elapsedSinceReconnectPhaseMs } |
stateChange |
{ next; previous } |
Patterns
Authentication that may rotate
new ResilientWebSocketClient({
urlProvider: async () => `wss://api.example.com/ws?token=${await refreshToken()}`,
});Graceful shutdown on page unload
window.addEventListener('beforeunload', () => client.stop(1001, 'page unload'));Pause/resume on offline/online
window.addEventListener('offline', () => client.stop());
window.addEventListener('online', () => client.connect());Resetting after give-up
onGiveUp is the policy saying "I've tried enough." The socket is closed; counters stay frozen
so you can inspect them. Call connect() again to reset everything and try once more.
Browser / runtime support
Anything with a global WebSocket (all modern browsers, Deno, Bun, Cloudflare Workers,
Node 22 with --experimental-websocket or via the ws package).
License
MIT