Client Guide
Experimental
This package is in an experimental phase. The API may change without following semver until it reaches a stable release.
@wooksjs/ws-client is a structured WebSocket client for browsers and Node.js. It provides RPC calls with automatic correlation, fire-and-forget messaging, push listeners with path matching, subscriptions with auto-resubscribe, and reconnection with backoff.
Zero runtime dependencies. Uses native WebSocket in browsers.
Installation
npm install @wooksjs/ws-clientFor Node.js, also install the ws package:
npm install @wooksjs/ws-client wsCreating a Client
Browser
import { createWsClient } from '@wooksjs/ws-client'
const client = createWsClient('ws://localhost:3000/ws', {
reconnect: true,
rpcTimeout: 5000,
})Node.js
import WebSocket from 'ws'
import { createWsClient } from '@wooksjs/ws-client'
const client = createWsClient('ws://localhost:3000/ws', {
_WebSocket: WebSocket as any,
rpcTimeout: 5000,
})Options
interface WsClientOptions {
protocols?: string | string[] // WebSocket subprotocols
reconnect?: boolean | WsClientReconnectOptions // Enable reconnection
rpcTimeout?: number // Timeout for call() in ms (default: 10000)
messageParser?: (raw: string) => any // Custom deserializer (default: JSON.parse)
messageSerializer?: (msg: any) => string // Custom serializer (default: JSON.stringify)
_WebSocket?: typeof WebSocket // WebSocket constructor override (Node.js)
}Sending Messages
Fire-and-forget: send()
Send a message without expecting a reply. The server handler's return value is ignored.
client.send('message', '/chat/general', { text: 'Hello!' })Wire frame: { event: "message", path: "/chat/general", data: { text: "Hello!" } }
When disconnected with reconnect enabled, messages are queued and sent when the connection reopens. Without reconnect, they are silently dropped.
RPC: call()
Send a message and wait for the server's reply. Returns a typed Promise.
const result = await client.call<{ joined: boolean }>('join', '/chat/general', { name: 'Alice' })
console.log(result.joined) // → trueWire frame: { event: "join", path: "/chat/general", data: { name: "Alice" }, id: 1 }
The client auto-generates an incrementing numeric id. The server matches the reply by this ID.
Error handling
call() rejects with WsClientError in these cases:
| Scenario | Error code |
|---|---|
| Not connected when called | 503 |
| Connection lost while waiting for reply | 503 |
client.close() called while waiting | 503 |
Timeout (rpcTimeout exceeded) | 408 |
| Server sent an error reply | Server's error code |
import { WsClientError } from '@wooksjs/ws-client'
try {
await client.call('join', '/chat/general', { name: 'Alice' })
} catch (err) {
if (err instanceof WsClientError) {
if (err.code === 409) console.log('Name already taken')
if (err.code === 408) console.log('Request timed out')
}
}Listening for Push Messages
on()
Register a handler for server-initiated push messages. Returns an unregister function.
const off = client.on('message', '/chat/general', ({ event, path, params, data }) => {
console.log(`${data.from}: ${data.text}`)
})
// Later: stop listening
off()Handler signature
interface WsClientPushEvent<T> {
event: string // Event type from server
path: string // Concrete path from server
params: Record<string, string> // Route params extracted by server (e.g. { room: 'general' })
data: T // Payload
}Route params are extracted by the server router and included in the push message. The client does not parse them.
Path matching
Exact match — O(1) lookup:
client.on('message', '/chat/general', handler)
// Matches: /chat/general
// Ignores: /chat/random, /chat/general/subWildcard suffix — prefix matching:
client.on('message', '/chat/*', handler)
// Matches: /chat/general, /chat/random, /chat/general/sub
// Ignores: /users/42Both exact and wildcard listeners fire if they match the same message. Multiple handlers for the same pattern are all called.
Subscriptions
subscribe()
Subscribe to a path with server acknowledgment and automatic resubscribe on reconnect.
const unsub = await client.subscribe('/notifications')
// Later: unsubscribe
unsub()Under the hood:
- Sends
{ event: "subscribe", path: "/notifications", id: N }(RPC) - Waits for server acknowledgment
- Tracks the subscription for auto-resubscribe after reconnect
- Returns an unsubscribe function that sends
{ event: "unsubscribe", path: "/notifications" }(fire-and-forget) and removes the tracking
You still need on() to handle the push messages that arrive on the subscribed path.
Lifecycle Events
All lifecycle handlers return an unregister function. Multiple handlers can be registered for each event.
onOpen
Fires when the connection opens, including after reconnect.
const off = client.onOpen(() => {
console.log('Connected!')
})After reconnect: queued messages are flushed first, then subscriptions are resubscribed, then onOpen fires.
onClose
Fires on every close, including before reconnect attempts.
client.onClose((code, reason) => {
console.log(`Disconnected: ${code} ${reason}`)
})onError
Fires on WebSocket error events.
client.onError((error) => {
console.error('WebSocket error:', error)
})onReconnect
Fires before each reconnection attempt, after the backoff delay.
client.onReconnect((attempt) => {
console.log(`Reconnecting... attempt ${attempt}`)
})Closing
client.close()Closes the WebSocket with code 1000. Permanently disables reconnection. Rejects all pending RPCs with WsClientError(503, 'Connection closed'). Clears the message queue.
Reconnection
Enable reconnection to automatically recover from dropped connections:
const client = createWsClient(url, {
reconnect: true, // uses defaults
})
// Or with custom options:
const client = createWsClient(url, {
reconnect: {
enabled: true,
maxRetries: 10, // default: Infinity
baseDelay: 1000, // ms, default: 1000
maxDelay: 30000, // ms, default: 30000
backoff: 'exponential', // 'exponential' | 'linear', default: 'exponential'
},
})Backoff
- Exponential (default):
min(baseDelay × 2^attempt, maxDelay)→ 1s, 2s, 4s, 8s, 16s, 30s, 30s, ... - Linear:
min(baseDelay × (attempt + 1), maxDelay)→ 1s, 2s, 3s, 4s, ...
What happens on unexpected close
- All pending RPCs are rejected (code 503, "Connection lost")
onClosehandlers fire- After backoff delay:
onReconnecthandlers fire, new WebSocket is created - On successful open: queued messages are flushed, subscriptions are resubscribed,
onOpenfires
Re-joining rooms after reconnect
Reconnection restores the WebSocket connection and resubscribes subscribe() calls, but it does not automatically re-join rooms. Handle this in onOpen:
client.onOpen(() => {
// Re-join the room after reconnect
client.call('join', `/chat/${currentRoom}`, { name: myName })
})Complete Example
import { createWsClient, WsClientError } from '@wooksjs/ws-client'
const client = createWsClient('ws://localhost:3000/ws', {
reconnect: true,
rpcTimeout: 5000,
})
// Wait for connection
await new Promise<void>(resolve => client.onOpen(resolve))
// Join a room (RPC)
try {
await client.call('join', '/chat/general', { name: 'Alice' })
} catch (err) {
if (err instanceof WsClientError && err.code === 409) {
console.log('Name taken, try another')
}
throw err
}
// Listen for messages
client.on('message', '/chat/general', ({ data }) => {
console.log(`${data.from}: ${data.text}`)
})
client.on('system', '/chat/general', ({ data }) => {
console.log(`[system] ${data.text}`)
})
// Send a message (fire-and-forget)
client.send('message', '/chat/general', { text: 'Hello everyone!' })
// Leave and close
await client.call('leave', '/chat/general')
client.close()