agentsclimarketplace

Websocket patterns

Skill vibeeval/vibecosystem/skills/websocket-patterns

Connection management, room patterns, reconnection strategies, message buffering, and binary protocol design.From its SKILL.md

Install
npx -y skills add vibeeval/vibecosystem --skill websocket-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

6.7 KB, ~1.6k tokens by cl100k_base, as published. Nobody here has run it

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?: string): void {
    const members = this.rooms.get(room)
    if (!members) return
    const payload = JSON.stringify(data)
    for (const id of members) {
      if (id === excludeId) continue
      const client = this.clients.get(id)
      if (client?.ws.readyState === WebSocket.OPEN) {
        client.ws.send(payload)
      }
    }
  }

  getRoomSize(room: string): number {
    return this.rooms.get(room)?.size ?? 0
  }
}

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) => {
      if (event.code === 1000) return  // Normal closure, no reconnect
      this.scheduleReconnect()
    }

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data as string)
      this.emit(data.type, data)
    }

    this.ws.onerror = () => {
      this.ws?.close()
    }
  }

  private scheduleReconnect(): void {
    // Exponential backoff with jitter
    const baseDelay = Math.min(1000 * 2 ** this.reconnectAttempts, this.maxReconnectDelay)
    const jitter = baseDelay * 0.3 * Math.random()
    const delay = baseDelay + jitter

    this.reconnectAttempts++
    setTimeout(() => this.connect(), delay)
  }

  send(data: unknown): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
    } else {
      // Buffer messages while disconnected (max 100)
      if (this.messageBuffer.length < 100) {
        this.messageBuffer.push(data)
      }
    }
  }

  private flushBuffer(): void {
    const buffered = [...this.messageBuffer]
    this.messageBuffer = []
    for (const msg of buffered) {
      this.send(msg)
    }
  }

  on(event: string, handler: Function): void {
    if (!this.handlers.has(event)) this.handlers.set(event, [])
    this.handlers.get(event)!.push(handler)
  }

  private emit(event: string, ...args: unknown[]): void {
    for (const handler of this.handlers.get(event) ?? []) {
      handler(...args)
    }
  }
}

Message Protocol Design

// Typed message envelope
interface WsMessage<T = unknown> {
  type: string
  id: string          // For request-response correlation
  timestamp: number
  payload: T
}

// Request-response over WebSocket
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(new Error(`RPC timeout: ${type}`))
      }, timeoutMs)

      this.pending.set(id, { resolve, timer })
      this.ws.send(JSON.stringify(msg))
    })
  }

  handleResponse(msg: WsMessage): void {
    const entry = this.pending.get(msg.id)
    if (!entry) return
    clearTimeout(entry.timer)
    this.pending.delete(msg.id)
    entry.resolve(msg.payload)
  }
}

Checklist

  • Heartbeat/ping-pong every 30s, terminate stale connections at 45s
  • Exponential backoff with jitter for client reconnection (1s-30s)
  • Message buffer during disconnection (cap at 100 messages)
  • Auth token validation on initial handshake (not in messages)
  • Room-based broadcasting for scoped message delivery
  • Rate limit per client (max messages/second)
  • Message size limit (max 1MB per frame)
  • Graceful shutdown: close code 1001, drain messages before exit

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

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.