A TypeScript WebSocket client focused on simplicity and developer experience. Features lazy connections, automatic resource management, and intelligent reconnection strategies.
Traditional WebSocket libraries require manual connection lifecycle management. @soffinal/websocket takes a different approach: communication-first design where connections are handled automatically, letting you focus on your application logic.
npm install @soffinal/websocketyarn add @soffinal/websocketpnpm add @soffinal/websocketbun add @soffinal/websocketimport { WebSocket } from "@soffinal/websocket";
// Create WebSocket - no connection established yet (lazy)
const ws = new WebSocket("ws://localhost:8080");
// Connection starts automatically when you listen
ws.listen((event) => {
console.log("Event:", event);
});
// Or use async iteration
for await (const event of ws) {
if (event.type === "connected") {
ws.send("Hello, server!");
}
}
// Fire-and-forget sending (connects automatically)
ws.send("Hello, server!");
// Manual cleanup (optional)
ws.stop();
// Modern automatic cleanup
using ws2 = new WebSocket("ws://localhost:8080");
ws2.send("Auto-cleanup!"); // Cleaned up automatically at scope exitConnections are established only when needed for actual communication. No idle connections consuming resources.
- Immediate reconnection for unexpected disconnections
- Server-specified delays (respects WebSocket close code 1013)
- Configurable exponential backoff for connection failures
- No artificial delays - server handles rate limiting
Messages are automatically queued when disconnected and delivered when reconnected. No message loss.
Built-in support for JSON, binary, and custom serialization with full TypeScript type safety.
Built on @soffinal/stream for powerful event composition and filtering.
Resources are automatically cleaned up when no listeners remain. Supports modern using declarations.
Support for static URLs or async functions for authentication tokens and load balancing.
Good defaults that work for most use cases. Minimal configuration required.
Works seamlessly in Node.js, Bun, Deno, and browsers.
new WebSocket(url: string, options?: WebSocket.Options)
new WebSocket(url: () => string | Promise<string>, options?: WebSocket.Options)interface Options<ENCODER extends Encoder<any, any> | undefined = undefined> {
encoder?: ENCODER; // Custom encoder for serialization
maxConnectionTimeout?: number; // Max connection timeout (default: 10000ms)
initialRetryDelay?: number; // Initial retry delay (default: 100ms)
retryMultiplier?: number; // Retry delay multiplier (default: 2)
maxMessageQueued?: number; // Max queued messages (default: 1000)
maxIdle?: number; // Max idle time for send-only connections (default: Infinity)
}| Method | Description |
|---|---|
listen(handler) |
Listen to events (starts connection automatically) |
send(data) |
Send data or queue if disconnected (connects automatically) |
stop() |
Stop connection and clear message queue |
[Symbol.dispose]() |
Automatic cleanup for using declarations |
type Event<ENCODER> =
| { type: "connecting" }
| { type: "connected" }
| { type: "disconnected"; code?: number; reason?: string }
| { type: "message"; data: ENCODER extends WebSocket.Encoder<any, infer Data> ? Data : any }
| { type: "send"; data: ENCODER extends WebSocket.Encoder<infer Data, any> ? Data : any }
| { type: "error"; error: ErrorObject };All errors include contextual information for debugging:
type ErrorEvent<ENCODER> = {
type: "error";
error:
| { type: "url"; url: string }
| { type: "connection"; error: unknown }
| { type: "timeout"; timeout: number }
| { type: "message-encoding"; data: ENCODER extends WebSocket.Encoder<infer Data, any> ? Data : unknown }
| { type: "message-decoding"; data: string | ArrayBuffer | Uint8Array }
| { type: "send"; data: ENCODER extends WebSocket.Encoder<infer Data, any> ? Data : unknown }
| { type: "queue-overflow"; data: ENCODER extends WebSocket.Encoder<infer Data, any> ? Data : unknown };
};import { WebSocket } from "@soffinal/websocket";
const ws = new WebSocket("ws://localhost:8080");
ws.listen((event) => {
switch (event.type) {
case "connecting":
console.log("🔄 Connecting to server...");
break;
case "connected":
console.log("✅ Connected to server");
break;
case "message":
console.log("📨 Received:", event.data);
break;
case "send":
console.log("📤 Sent:", event.data);
break;
case "disconnected":
console.log("❌ Disconnected:", event.code, event.reason);
break;
case "error":
console.error("🚨 Error:", event.error);
break;
}
});
// Send messages - connection handled automatically
ws.send("Hello, server!");// Static URL
const ws1 = new WebSocket("wss://api.example.com/ws");
// Dynamic URL with authentication
const ws2 = new WebSocket(async () => {
const token = await getAuthToken();
return `wss://api.example.com/ws?token=${token}`;
});type SendMessage = { type: "chat"; message: string; userId: string };
type ReceiveMessage = { type: "response"; data: string; timestamp: number };
const ws = new WebSocket("ws://localhost:8080", {
encoder: WebSocket.getDefaultEncoder<SendMessage, ReceiveMessage>(),
});
// Type-safe sending
await ws.send({
type: "chat",
message: "Hello",
userId: "user123",
}); // ✅ Fully typed
// Type-safe receiving
ws.listen((event) => {
if (event.type === "message") {
console.log(event.data.timestamp); // ✅ TypeScript knows this exists
}
});// Type-safe MessagePack encoder
import * as msgpack from "@msgpack/msgpack";
type SendData = { action: string; payload: any };
type ReceiveData = { status: string; result: any };
const ws = new WebSocket("ws://localhost:8080", {
encoder: {
encode: (data: SendData) => msgpack.encode(data),
decode: (buffer): ReceiveData => msgpack.decode(buffer),
},
});
// Type-safe binary protocol with compression
type Command = { cmd: string; args: string[] };
type Response = { success: boolean; data: unknown };
const binaryWs = new WebSocket("ws://localhost:8080", {
encoder: {
encode: async (data: Command) => await compressAndEncrypt(data),
decode: async (data): Promise<Response> => await decryptAndDecompress(data),
},
});
// Now fully type-safe
ws.send({ action: "login", payload: { user: "john" } }); // ✅ Typed
binaryWs.send({ cmd: "exec", args: ["ls", "-la"] }); // ✅ Typedimport { filter, map } from "@soffinal/stream";
// Listen only to chat messages
ws.pipe(filter((event) => event.type === "message"))
.pipe(filter((event) => event.data.type === "chat"))
.listen((event) => {
console.log("Chat message:", event.data.message);
});
// Transform and filter connection events
ws.pipe(filter((event) => ["connected", "disconnected"].includes(event.type)))
.pipe(map((event) => ({ status: event.type, timestamp: Date.now() })))
.listen((statusEvent) => {
updateConnectionStatus(statusEvent);
});ws.listen((event) => {
if (event.type === "error") {
switch (event.error.type) {
case "url":
console.error("❌ Invalid URL:", event.error.url);
break;
case "connection":
console.error("❌ Connection failed:", event.error.error);
break;
case "timeout":
console.error("⏰ Connection timeout:", event.error.timeout, "ms");
break;
case "message-encoding":
console.error("📤 Failed to encode:", event.error.data);
break;
case "message-decoding":
console.error("📥 Failed to decode:", event.error.data);
break;
case "send":
console.error("📤 Failed to send:", event.error.data);
break;
case "queue-overflow":
console.error("📦 Message queue overflow:", event.error.data);
break;
}
}
});// Connection lifecycle based on listeners, not sending
const ws = new WebSocket("ws://localhost:8080");
// Sending keeps connection open until manually closed or if the maxIdle is set
ws.send("message 1"); // Connects and stays open
ws.send("message 2"); // Connection remains open
// Listeners control automatic cleanup
const unsubscribe1 = ws.listen(handler1);
const unsubscribe2 = ws.listen(handler2);
unsubscribe1(); // Still connected (has listeners)
unsubscribe2(); // Automatically disconnects (no more listeners)
// Manual cleanup
ws.stop(); // Immediately stop and clear queue
// Modern automatic cleanup with 'using'
using ws2 = new WebSocket("ws://localhost:8080");
ws2.send("Hello!"); // Connection stays open
// Automatically cleaned up when scope exitsThe client implements a reconnection strategy optimized for real-world scenarios:
- Normal disconnection (code 1000): No reconnection
- Server timeout (code 1013 with numeric reason): Uses server-specified delay
- Other disconnections: Immediate reconnection with exponential backoff on failures
- Philosophy: Server protection is server responsibility, client optimizes for UX
// Server can specify reconnection delay via close code 1013
ws.listen((event) => {
if (event.type === "disconnected") {
if (event.code === 1013) {
console.log("⏳ Server requested delay:", event.reason, "ms");
// Client automatically waits before reconnecting
} else {
console.log("🔄 Reconnecting immediately");
}
}
});When connection attempts fail (not disconnections), the client uses configurable exponential backoff:
- First attempt: Immediate (0ms)
- First retry:
initialRetryDelay(default: 100ms) - Second retry: 200ms (100ms × 2)
- Third retry: 400ms (200ms × 2)
- Fourth retry: 800ms (400ms × 2)
- Subsequent retries: Up to
maxConnectionTimeoutvalue
Both initialRetryDelay and retryMultiplier are configurable for different use cases.
Messages are automatically queued when disconnected and delivered when reconnected:
const ws = new WebSocket("ws://localhost:8080");
// These messages trigger connection and are queued until connected
ws.send("message 1"); // Starts connection, queues message
ws.send("message 2"); // Queued
ws.send("message 3"); // Queued
console.log(ws.queue.length); // 3 (while connecting)
// After connection established: queue is flushed automatically- @soffinal/stream: Stream-based event handling foundation
- Runtime Support: Node.js, Bun, Deno, and modern browsers
MIT License - see LICENSE file for details.
soffinal
- GitHub: @soffinal
- Email: [email protected]
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
See CHANGELOG.md for a detailed history of changes.
@soffinal/websocket - Simplifying real-time communication with automatic connection management.