JavaScript WebSocket API Complete Guide

Master real-time communication: WebSocket connections, message handling, heartbeat detection, and reconnection mechanisms

JavaScript WebSocket API Complete Guide

The WebSocket API provides full-duplex communication between browsers and servers. This article covers WebSocket usage and best practices.

Basic Concepts

Creating Connections

// Create WebSocket connection
const ws = new WebSocket('wss://example.com/socket');

// Connection with protocols
const wsWithProtocol = new WebSocket('wss://example.com/socket', ['protocol1', 'protocol2']);

// Connection state
console.log(ws.readyState);
// 0 - CONNECTING: Connecting
// 1 - OPEN: Connected
// 2 - CLOSING: Closing
// 3 - CLOSED: Closed

Basic Events

const ws = new WebSocket('wss://example.com/socket');

// Connection opened
ws.onopen = (event) => {
  console.log('Connection established');
  ws.send('Hello Server!');
};

// Message received
ws.onmessage = (event) => {
  console.log('Message received:', event.data);
};

// Connection closed
ws.onclose = (event) => {
  console.log('Connection closed:', {
    code: event.code,
    reason: event.reason,
    wasClean: event.wasClean
  });
};

// Connection error
ws.onerror = (event) => {
  console.error('WebSocket error:', event);
};

// Using addEventListener
ws.addEventListener('message', (event) => {
  console.log('Message:', event.data);
});

Sending Messages

// Send text
ws.send('Hello World');

// Send JSON
ws.send(JSON.stringify({ type: 'message', content: 'Hello' }));

// Send binary data
const buffer = new ArrayBuffer(8);
ws.send(buffer);

// Send Blob
const blob = new Blob(['data'], { type: 'application/octet-stream' });
ws.send(blob);

// Check buffer
console.log('Bytes pending:', ws.bufferedAmount);

// Set binary type
ws.binaryType = 'arraybuffer'; // or 'blob'

WebSocket Wrapper

Basic Wrapper Class

class WebSocketClient {
  constructor(url, options = {}) {
    this.url = url;
    this.options = {
      reconnect: true,
      reconnectInterval: 1000,
      maxReconnectInterval: 30000,
      reconnectDecay: 1.5,
      maxReconnectAttempts: null,
      ...options
    };

    this.ws = null;
    this.reconnectAttempts = 0;
    this.listeners = new Map();
    this.messageQueue = [];

    this.connect();
  }

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

    this.ws.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.flushMessageQueue();
      this.emit('open');
    };

    this.ws.onclose = (event) => {
      console.log('WebSocket closed:', event.code);
      this.emit('close', event);

      if (this.options.reconnect && !event.wasClean) {
        this.scheduleReconnect();
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      this.emit('error', error);
    };

    this.ws.onmessage = (event) => {
      this.handleMessage(event);
    };
  }

  handleMessage(event) {
    try {
      const data = JSON.parse(event.data);
      this.emit('message', data);

      // Dispatch by type
      if (data.type) {
        this.emit(data.type, data.payload || data);
      }
    } catch {
      // Non-JSON message
      this.emit('message', event.data);
    }
  }

  send(data) {
    const message = typeof data === 'string' ? data : JSON.stringify(data);

    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(message);
    } else {
      // Connection not ready, add to queue
      this.messageQueue.push(message);
    }
  }

  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift();
      this.ws.send(message);
    }
  }

  scheduleReconnect() {
    if (this.options.maxReconnectAttempts !== null &&
        this.reconnectAttempts >= this.options.maxReconnectAttempts) {
      console.log('Max reconnect attempts reached');
      this.emit('maxReconnectAttempts');
      return;
    }

    const delay = Math.min(
      this.options.reconnectInterval * Math.pow(this.options.reconnectDecay, this.reconnectAttempts),
      this.options.maxReconnectInterval
    );

    console.log(`Reconnecting in ${delay}ms...`);
    this.reconnectAttempts++;

    setTimeout(() => {
      console.log(`Reconnect attempt #${this.reconnectAttempts}`);
      this.connect();
    }, delay);
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event).push(callback);
  }

  off(event, callback) {
    if (this.listeners.has(event)) {
      const callbacks = this.listeners.get(event);
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    }
  }

  emit(event, data) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach(callback => callback(data));
    }
  }

  close(code = 1000, reason = '') {
    this.options.reconnect = false;
    this.ws.close(code, reason);
  }

  get readyState() {
    return this.ws?.readyState ?? WebSocket.CLOSED;
  }

  get isConnected() {
    return this.readyState === WebSocket.OPEN;
  }
}

// Usage
const client = new WebSocketClient('wss://example.com/socket');

client.on('open', () => {
  console.log('Connected');
});

client.on('message', (data) => {
  console.log('Message received:', data);
});

client.send({ type: 'subscribe', channel: 'updates' });

Heartbeat Detection

class HeartbeatWebSocket extends WebSocketClient {
  constructor(url, options = {}) {
    super(url, {
      heartbeatInterval: 30000,
      heartbeatTimeout: 5000,
      pingMessage: { type: 'ping' },
      pongMessage: 'pong',
      ...options
    });

    this.heartbeatTimer = null;
    this.pongTimer = null;

    this.on('open', () => this.startHeartbeat());
    this.on('close', () => this.stopHeartbeat());
    this.on('pong', () => this.handlePong());
  }

  startHeartbeat() {
    this.stopHeartbeat();

    this.heartbeatTimer = setInterval(() => {
      if (this.isConnected) {
        this.sendPing();
      }
    }, this.options.heartbeatInterval);
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
    if (this.pongTimer) {
      clearTimeout(this.pongTimer);
      this.pongTimer = null;
    }
  }

  sendPing() {
    this.send(this.options.pingMessage);

    // Wait for pong response
    this.pongTimer = setTimeout(() => {
      console.log('Heartbeat timeout, disconnecting');
      this.ws.close(4000, 'Heartbeat timeout');
    }, this.options.heartbeatTimeout);
  }

  handlePong() {
    if (this.pongTimer) {
      clearTimeout(this.pongTimer);
      this.pongTimer = null;
    }
  }

  handleMessage(event) {
    try {
      const data = JSON.parse(event.data);

      // Handle pong response
      if (data.type === 'pong' || data === this.options.pongMessage) {
        this.emit('pong', data);
        return;
      }

      super.handleMessage(event);
    } catch {
      super.handleMessage(event);
    }
  }
}

Practical Applications

Chat Application

class ChatClient {
  constructor(url, userId) {
    this.userId = userId;
    this.ws = new HeartbeatWebSocket(url);
    this.rooms = new Set();
    this.messageHandlers = new Map();

    this.setupEventHandlers();
  }

  setupEventHandlers() {
    this.ws.on('open', () => {
      this.authenticate();
    });

    this.ws.on('message', (data) => {
      this.handleServerMessage(data);
    });

    this.ws.on('close', () => {
      console.log('Chat connection closed');
    });
  }

  authenticate() {
    this.ws.send({
      type: 'auth',
      userId: this.userId,
      token: this.getAuthToken()
    });
  }

  getAuthToken() {
    return localStorage.getItem('chatToken');
  }

  handleServerMessage(data) {
    switch (data.type) {
      case 'auth_success':
        console.log('Authentication successful');
        this.rejoinRooms();
        break;

      case 'message':
        this.onMessage(data);
        break;

      case 'user_joined':
        this.onUserJoined(data);
        break;

      case 'user_left':
        this.onUserLeft(data);
        break;

      case 'typing':
        this.onTyping(data);
        break;

      case 'error':
        console.error('Server error:', data.message);
        break;
    }
  }

  joinRoom(roomId) {
    this.rooms.add(roomId);
    this.ws.send({
      type: 'join',
      roomId
    });
  }

  leaveRoom(roomId) {
    this.rooms.delete(roomId);
    this.ws.send({
      type: 'leave',
      roomId
    });
  }

  rejoinRooms() {
    this.rooms.forEach(roomId => {
      this.ws.send({
        type: 'join',
        roomId
      });
    });
  }

  sendMessage(roomId, content) {
    const message = {
      type: 'message',
      roomId,
      content,
      timestamp: Date.now()
    };

    this.ws.send(message);
    return message;
  }

  sendTyping(roomId) {
    this.ws.send({
      type: 'typing',
      roomId
    });
  }

  onMessage(data) {
    const handler = this.messageHandlers.get('message');
    if (handler) handler(data);
  }

  onUserJoined(data) {
    const handler = this.messageHandlers.get('userJoined');
    if (handler) handler(data);
  }

  onUserLeft(data) {
    const handler = this.messageHandlers.get('userLeft');
    if (handler) handler(data);
  }

  onTyping(data) {
    const handler = this.messageHandlers.get('typing');
    if (handler) handler(data);
  }

  on(event, handler) {
    this.messageHandlers.set(event, handler);
  }

  disconnect() {
    this.ws.close();
  }
}

// Usage
const chat = new ChatClient('wss://chat.example.com', 'user123');

chat.on('message', (data) => {
  displayMessage(data);
});

chat.on('typing', (data) => {
  showTypingIndicator(data.userId);
});

chat.joinRoom('room1');
chat.sendMessage('room1', 'Hello everyone!');

Real-time Data Subscription

class DataSubscriber {
  constructor(url) {
    this.ws = new WebSocketClient(url);
    this.subscriptions = new Map();
    this.dataHandlers = new Map();

    this.ws.on('message', (data) => {
      this.handleData(data);
    });
  }

  subscribe(channel, handler) {
    if (!this.subscriptions.has(channel)) {
      this.subscriptions.set(channel, new Set());
      this.ws.send({
        type: 'subscribe',
        channel
      });
    }

    this.subscriptions.get(channel).add(handler);

    // Return unsubscribe function
    return () => this.unsubscribe(channel, handler);
  }

  unsubscribe(channel, handler) {
    if (this.subscriptions.has(channel)) {
      const handlers = this.subscriptions.get(channel);
      handlers.delete(handler);

      if (handlers.size === 0) {
        this.subscriptions.delete(channel);
        this.ws.send({
          type: 'unsubscribe',
          channel
        });
      }
    }
  }

  handleData(data) {
    const { channel, payload } = data;

    if (this.subscriptions.has(channel)) {
      this.subscriptions.get(channel).forEach(handler => {
        handler(payload);
      });
    }
  }
}

// Usage
const subscriber = new DataSubscriber('wss://data.example.com');

// Subscribe to stock data
const unsubscribe = subscriber.subscribe('stocks:AAPL', (data) => {
  console.log('AAPL price:', data.price);
  updateStockChart(data);
});

// Subscribe to news
subscriber.subscribe('news:tech', (data) => {
  displayNews(data);
});

// Unsubscribe
// unsubscribe();

Collaborative Editing

class CollaborativeEditor {
  constructor(url, documentId) {
    this.documentId = documentId;
    this.ws = new WebSocketClient(url);
    this.version = 0;
    this.pendingChanges = [];
    this.onRemoteChange = null;

    this.setupConnection();
  }

  setupConnection() {
    this.ws.on('open', () => {
      this.joinDocument();
    });

    this.ws.on('message', (data) => {
      this.handleMessage(data);
    });
  }

  joinDocument() {
    this.ws.send({
      type: 'join',
      documentId: this.documentId
    });
  }

  handleMessage(data) {
    switch (data.type) {
      case 'init':
        // Initialize document state
        this.version = data.version;
        this.onRemoteChange?.(data.content, 'init');
        break;

      case 'change':
        // Handle remote change
        if (data.version === this.version + 1) {
          this.version = data.version;
          this.applyRemoteChange(data.change);
        } else {
          // Version conflict, need sync
          this.requestSync();
        }
        break;

      case 'ack':
        // Change acknowledged
        const pending = this.pendingChanges.shift();
        if (pending) {
          this.version = data.version;
        }
        break;

      case 'cursor':
        // Other users' cursor positions
        this.onCursorUpdate?.(data.userId, data.position);
        break;
    }
  }

  sendChange(change) {
    const message = {
      type: 'change',
      documentId: this.documentId,
      version: this.version,
      change
    };

    this.pendingChanges.push(change);
    this.ws.send(message);
  }

  sendCursor(position) {
    this.ws.send({
      type: 'cursor',
      documentId: this.documentId,
      position
    });
  }

  applyRemoteChange(change) {
    // Operational Transformation
    const transformedChange = this.transformChange(change);
    this.onRemoteChange?.(transformedChange, 'remote');
  }

  transformChange(change) {
    // Simplified operational transformation
    // Real implementation needs full OT algorithm
    return change;
  }

  requestSync() {
    this.ws.send({
      type: 'sync',
      documentId: this.documentId
    });
  }
}

// Usage
const editor = new CollaborativeEditor('wss://collab.example.com', 'doc123');

editor.onRemoteChange = (change, source) => {
  if (source !== 'local') {
    applyChangeToEditor(change);
  }
};

editor.onCursorUpdate = (userId, position) => {
  showRemoteCursor(userId, position);
};

// On local edit
function onLocalEdit(change) {
  editor.sendChange(change);
}

Best Practices Summary

WebSocket Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Connection Management                             │
│   ├── Implement auto-reconnection                  │
│   ├── Use heartbeat for keep-alive                 │
│   ├── Handle connection states properly            │
│   └── Close connections gracefully                 │
│                                                     │
│   Message Handling                                  │
│   ├── Use JSON formatted messages                  │
│   ├── Implement message queue                      │
│   ├── Handle message acknowledgment                │
│   └── Consider message compression                 │
│                                                     │
│   Security                                          │
│   ├── Use wss:// encrypted connections            │
│   ├── Implement authentication                     │
│   ├── Validate message sources                     │
│   └── Prevent message injection                    │
│                                                     │
└─────────────────────────────────────────────────────┘
Close CodeMeaningScenario
1000Normal closeActive disconnect
1001Going awayPage close
1006Abnormal closeNetwork disconnect
1011Server errorServer exception

Master the WebSocket API to build real-time, efficient web applications.