@forgedevstack/forge-socket
v1.0.0
Published
Typed WebSocket client with auto-reconnect, heartbeat, rooms, and offline message queue. Zero dependencies.
Maintainers
Readme
@forgedevstack/forge-socket
Typed WebSocket client for the browser and Node with auto-reconnect, heartbeat, rooms, and an offline message queue. Zero runtime dependencies.
Part of the ForgeStack ecosystem.
Install
npm install @forgedevstack/forge-socketQuick Example
import { ForgeSocket } from '@forgedevstack/forge-socket';
interface Incoming {
'chat:message': { user: string; text: string };
'presence:update': { online: number };
}
interface Outgoing {
'chat:send': { text: string };
}
const socket = new ForgeSocket<Incoming, Outgoing>({
url: 'wss://example.com/ws',
});
socket.onStateChange((state, previous) => {
console.log(`${previous} -> ${state}`);
});
const unsubscribe = socket.on('chat:message', (payload) => {
console.log(`${payload.user}: ${payload.text}`);
});
socket.connect();
socket.join('general');
socket.send('chat:send', { text: 'hello' }, 'general');Options
| Option | Type | Default | Description |
|---|---|---|---|
| url | string | required | WebSocket server URL |
| webSocketImpl | WebSocketConstructor | globalThis.WebSocket | Injectable WebSocket implementation |
| protocols | string \| string[] | — | Subprotocols passed to the constructor |
| reconnect.enabled | boolean | true | Auto-reconnect on unexpected close |
| reconnect.baseDelayMs | number | 1000 | First retry delay |
| reconnect.maxDelayMs | number | 30000 | Upper bound for backoff delay |
| reconnect.backoffFactor | number | 2 | Exponential growth factor |
| reconnect.maxAttempts | number | Infinity | Retry attempt limit |
| reconnect.jitter | boolean | true | Adds random jitter to each delay |
| heartbeat.enabled | boolean | true | Periodic ping/pong keepalive |
| heartbeat.intervalMs | number | 30000 | Interval between pings |
| heartbeat.timeoutMs | number | 10000 | Pong wait before forcing reconnect |
| heartbeat.pingType | string | 'ping' | Envelope type used for pings |
| heartbeat.pongType | string | 'pong' | Envelope type recognized as pong |
| queue.enabled | boolean | true | Queue messages while not open |
| queue.maxSize | number | 100 | Oldest messages drop beyond this size |
| serializer | (envelope) => string | JSON.stringify | Custom outgoing serialization |
| deserializer | (data) => envelope | JSON.parse | Custom incoming parsing |
Methods
| Method | Returns | Description |
|---|---|---|
| connect() | void | Opens the connection |
| close(code?, reason?) | void | Closes without reconnecting |
| send(type, payload, room?) | boolean | true if sent now, false if queued or dropped |
| on(type, handler) | () => void | Subscribes; returns an unsubscribe function |
| off(type, handler) | void | Removes a handler |
| once(type, handler) | () => void | Fires the handler a single time |
| onStateChange(handler) | () => void | Observes connection state transitions |
| join(room) | void | Joins a room; rejoined automatically after reconnect |
| leave(room) | void | Leaves a room |
| state | ConnectionState | Current connection state |
| url | string | Configured URL |
Connection States
idle → connecting → open → closing → closed, with reconnecting between attempts after an unexpected close. When reconnect is enabled, the client retries with capped exponential backoff and rejoins all rooms once the connection is open again.
Wire Protocol
Every message is a JSON envelope:
{ "type": "chat:send", "payload": { "text": "hello" }, "room": "general" }Room membership uses built-in envelope types room:join and room:leave with the room name in the payload:
{ "type": "room:join", "payload": { "room": "general" }, "room": "general" }Heartbeats send { "type": "ping", "payload": null } and expect any message with type pong in response. Pong messages are consumed internally and never reach your handlers.
Node Usage
Node 22+ ships a global WebSocket, so no configuration is needed. On older Node versions, inject the ws implementation:
import WebSocket from 'ws';
import { ForgeSocket } from '@forgedevstack/forge-socket';
import type { WebSocketConstructor } from '@forgedevstack/forge-socket';
const socket = new ForgeSocket({
url: 'ws://localhost:8080',
webSocketImpl: WebSocket as unknown as WebSocketConstructor,
});License
MIT
