| name | websocket-patterns |
| description | Connection management, room patterns, reconnection strategies, message buffering, and binary protocol design. |
WebSocket Patterns
Production WebSocket patterns for real-time applications.
Connection Management
import { WebSocketServer, WebSocket } from 'ws'
interface Client {
id: string
ws: WebSocket
rooms: Set<string>
lastPing: number
metadata: Record<string, unknown>
}
class ConnectionManager {
private clients = new Map<string, Client>()
private heartbeatInterval: NodeJS.Timeout
constructor(private wss: WebSocketServer) {
this.heartbeatInterval = setInterval(() => this.checkHeartbeats(), 30_000)
wss.on('connection', (ws, req) => {
const clientId = crypto.randomUUID()
const client: Client = {
id: clientId,
ws,
rooms: new Set(),
lastPing: Date.now(),
metadata: { ip: req.socket.remoteAddress }
}
this.clients.set(clientId, client)
ws.on('pong', () => { client.lastPing = Date.now() })
ws.on('close', () => this.removeClient(clientId))
ws.on('error', () => this.removeClient(clientId))
this.send(client, { type: 'connected', clientId })
})
}
private checkHeartbeats(): void {
const staleThreshold = Date.now() - 45_000
for (const [id, client] of this.clients) {
if (client.lastPing < staleThreshold) {
client.ws.terminate()
this.removeClient(id)
} else {
client.ws.ping()
}
}
}
private removeClient(id: string): void {
const client = this.clients.get(id)
if (!client) return
for (const room of client.rooms) {
this.leaveRoom(id, room)
}
this.clients.delete(id)
}
send(client: Client, data: unknown): void {
if (client.ws.readyState === WebSocket.OPEN) {
client.ws.send(JSON.stringify(data))
}
}
destroy(): void {
clearInterval(this.heartbeatInterval)
}
}
Room Pattern
class RoomManager {
private rooms = new Map<string, Set<string>>()
joinRoom(clientId: string, room: string): void {
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set())
}
this.rooms.get(room)!.add(clientId)
}
leaveRoom(clientId: string, room: string): void {
const members = this.rooms.get(room)
if (!members) return
members.delete(clientId)
if (members.size === 0) this.rooms.delete(room)
}
broadcast(room: string, data: unknown, excludeId?: ): {
members = ..(room)
(!members)
payload = .(data)
( id members) {
(id === excludeId)
client = ..(id)
(client?.. === .) {
client..(payload)
}
}
}
(: ): {
..(room)?. ??
}
}
Client-Side Reconnection
class ReconnectingWebSocket {
private ws: WebSocket | null = null
private reconnectAttempts = 0
private maxReconnectDelay = 30_000
private messageBuffer: unknown[] = []
private handlers = new Map<string, Function[]>()
constructor(private url: string) {
this.connect()
}
private connect(): void {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
this.reconnectAttempts = 0
this.flushBuffer()
this.emit('connected')
}
this.ws.onclose = (event) => {
(event. === )
.()
}
.. = {
data = .(event. )
.(data., data)
}
.. = {
.?.()
}
}
(): {
baseDelay = .( * ** ., .)
jitter = baseDelay * * .()
delay = baseDelay + jitter
.++
( .(), delay)
}
(: ): {
(.?. === .) {
..(.(data))
} {
(.. < ) {
..(data)
}
}
}
(): {
buffered = [....]
. = []
( msg buffered) {
.(msg)
}
}
(: , : ): {
(!..(event)) ..(event, [])
..(event)!.(handler)
}
(: , ...: []): {
( handler ..(event) ?? []) {
(...args)
}
}
}
Message Protocol Design
interface WsMessage<T = unknown> {
type: string
id: string
timestamp: number
payload: T
}
class WsRpc {
private pending = new Map<string, { resolve: Function; timer: NodeJS.Timeout }>()
async request<T>(type: string, payload: unknown, timeoutMs = 5000): Promise<T> {
const id = crypto.randomUUID()
const msg: WsMessage = { type, id, timestamp: Date.now(), payload }
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(id)
reject( ())
}, timeoutMs)
..(id, { resolve, timer })
..(.(msg))
})
}
(: ): {
entry = ..(msg.)
(!entry)
(entry.)
..(msg.)
entry.(msg.)
}
}
Checklist
Anti-Patterns
- No heartbeat: zombie connections consuming resources
- Reconnecting without backoff: hammering server on outage
- Unbounded message buffer: memory leak during long disconnections
- Auth in every message: use connection-level auth, not per-message
- Broadcasting to all clients when only a room subset needs the update
- Synchronous JSON.parse in hot path: use worker threads for heavy payloads