C.W.K.
Stream
Lesson 08 of 08 · published

Practical Patterns

~13 min · browser, heartbeat, router

Level 0Poller
0 XP0/60 lessons0/10 achievements
0/120 XP to next level120 XP to go0% complete

Application-level ping/pong

RFC ping/pong (opcode 0x9/0xA) is invisible from the browser API — the browser handles it but does not let you trigger it. So you build your own. Send {type: 'ping'} every 30 seconds; expect {type: 'pong'} within a few seconds; if you do not get one, close the connection with code 4000 and let your reconnection logic take over. This catches silently-dead connections (NAT timeout, laptop lid) that the browser does not notice.

Message router by type

Once your messages have a type field, route them. A flat switch is fine for a few types; a registry (Map<type, handler>) scales better. The router is the natural place to add cross-cutting concerns: logging, validation, error handling.

Compose, don't inherit

Reconnection, heartbeat, and routing are independent concerns. Build them as small composable wrappers around a base socket, rather than one giant class. cwkPippa's adapters follow exactly this shape — narrow boundary, narrow responsibilities.

Code

Robust client with reconnect + heartbeat + router·javascript
class RobustSocket extends EventTarget {
  constructor(url) {
    super();
    this.url = url;
    this.handlers = new Map();      // type -> fn
    this.pingTimer = null;
    this.pongTimer = null;
    this._connect();
  }

  on(type, fn) { this.handlers.set(type, fn); return this; }

  send(type, data) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, data }));
    }
  }

  _connect() {
    this.ws = new WebSocket(this.url);

    this.ws.addEventListener('open', () => {
      this._startHeartbeat();
      this.dispatchEvent(new Event('connected'));
    });

    this.ws.addEventListener('message', (e) => {
      let msg;
      try { msg = JSON.parse(e.data); } catch { return; }
      if (msg.type === 'pong') return clearTimeout(this.pongTimer);
      const handler = this.handlers.get(msg.type);
      if (handler) handler(msg.data);
      else this.dispatchEvent(new MessageEvent('unhandled', { data: msg }));
    });

    this.ws.addEventListener('close', () => {
      this._stopHeartbeat();
      // Reconnection logic from previous lesson goes here.
    });
  }

  _startHeartbeat() {
    this.pingTimer = setInterval(() => {
      if (this.ws?.readyState !== WebSocket.OPEN) return;
      this.ws.send(JSON.stringify({ type: 'ping' }));
      this.pongTimer = setTimeout(() => {
        // No pong in 5s; assume dead.
        this.ws.close(4000, 'heartbeat timeout');
      }, 5_000);
    }, 30_000);
  }

  _stopHeartbeat() {
    clearInterval(this.pingTimer);
    clearTimeout(this.pongTimer);
  }
}

// Usage
const ws = new RobustSocket('wss://api.example.com/ws');
ws.on('chat.message', (m) => renderMessage(m));
ws.on('user.joined',  (u) => addUserToSidebar(u));
ws.send('chat.message', { room: 'general', text: 'hi' });

External links

Exercise

Compose RobustSocket from the previous lesson's ReconnectingWebSocket class so that you have one wrapper that handles reconnection AND heartbeat AND routing. Test by registering five message types, then unplugging your network for 60 seconds. The client should reconnect, route messages correctly, and never throw an unhandled error.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.