Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Robust WebSocket Client Implementation with Auto-Reconnection and Heartbeat for Vue.js

Tech 1

Instal the WebSocket dependency:

npm install ws

Create a connection menager module using the singleton pattern:

// services/SocketService.js
class SocketService {
  constructor() {
    this.socket = null;
    this.serverUrl = null;
    this.messageHandler = null;
    this.failureCallback = null;
    this.reconnectCount = 0;
    this.maxRetries = 5;
    this.retryDelay = 3000;
    this.heartbeatTimer = null;
    this.missedPongs = 0;
    this.isIntentionalClose = false;
    this.messageQueue = [];
  }

  connect(url, authData, onReceive, onFail) {
    this.serverUrl = url;
    this.messageHandler = onReceive;
    this.failureCallback = onFail;
    this.isIntentionalClose = false;
    
    if (authData) {
      this.messageQueue.push(authData);
    }
    
    this.initializeSocket();
  }

  initializeSocket() {
    if (!('WebSocket' in window)) {
      console.error('WebSocket not supported in this browser');
      return;
    }

    try {
      this.socket = new WebSocket(this.serverUrl);
      this.attachListeners();
    } catch (err) {
      console.error('Socket initialization failed:', err);
      this.attemptReconnect();
    }
  }

  attachListeners() {
    this.socket.onopen = (evt) => this.onConnectionOpen(evt);
    this.socket.onmessage = (evt) => this.onDataReceived(evt);
    this.socket.onclose = (evt) => this.onConnectionClosed(evt);
    this.socket.onerror = (evt) => this.onConnectionError(evt);
  }

  onConnectionOpen(event) {
    console.log('Socket established');
    this.reconnectCount = 0;
    this.missedPongs = 0;
    
    // Send queued messages
    while (this.messageQueue.length > 0) {
      const payload = this.messageQueue.shift();
      this.dispatch(payload);
    }
    
    this.activateHeartbeat();
  }

  onDataReceived(event) {
    // Reset missed pongs on any message reception
    this.missedPongs = 0;
    
    let parsed;
    try {
      parsed = JSON.parse(event.data);
    } catch {
      parsed = event.data;
    }
    
    if (this.messageHandler) {
      this.messageHandler(parsed);
    }
  }

  onConnectionClosed(event) {
    console.log('Socket closed:', event.code);
    this.deactivateHeartbeat();
    
    // 1000 = Normal closure, 1001 = Going away
    if (!this.isIntentionalClose && event.code !== 1000 && event.code !== 1001) {
      this.attemptReconnect();
    }
  }

  onConnectionError(error) {
    console.error('Socket error:', error);
    if (this.failureCallback) {
      this.failureCallback(error);
    }
  }

  dispatch(data) {
    if (this.socket && this.socket.readyState === WebSocket.OPEN) {
      const packet = typeof data === 'object' ? JSON.stringify(data) : data;
      this.socket.send(packet);
      return true;
    }
    // Queue if not ready
    this.messageQueue.push(data);
    return false;
  }

  activateHeartbeat() {
    this.deactivateHeartbeat();
    
    this.heartbeatTimer = setInterval(() => {
      if (this.missedPongs > 2) {
        console.warn('Heartbeat failed - terminating connection');
        this.socket.close();
        return;
      }
      
      // Send ping frame
      const pingPacket = { 
        action: 'ping', 
        timestamp: new Date().toISOString() 
      };
      
      if (this.dispatch(pingPacket)) {
        this.missedPongs++;
      }
    }, 20000); // 20 second interval
  }

  deactivateHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  attemptReconnect() {
    if (this.reconnectCount >= this.maxRetries) {
      console.error('Maximum reconnection attempts exceeded');
      return;
    }

    this.reconnectCount++;
    const delay = this.retryDelay * this.reconnectCount; // Exponential backoff
    
    console.log(`Reconnecting in ${delay}ms... (attempt ${this.reconnectCount})`);
    
    setTimeout(() => {
      this.initializeSocket();
    }, delay);
  }

  terminate() {
    this.isIntentionalClose = true;
    this.deactivateHeartbeat();
    if (this.socket) {
      this.socket.close(1000, 'Client terminated connection');
    }
  }
}

export const socketClient = new SocketService();

Integration within Vue components:

import { socketClient } from '@/services/SocketService.js'

export default {
  mounted() {
    this.initWebSocket()
  },
  
  beforeUnmount() {
    socketClient.terminate()
  },
  
  methods: {
    initWebSocket() {
      socketClient.connect(
        'wss://api.example.com/stream',
        { 
          userId: 42, 
          token: 'auth-token-here',
          subscribe: ['notifications', 'updates']
        },
        (response) => {
          this.handleServerMessage(response)
        },
        (err) => {
          console.error('Connection failed:', err)
          this.showConnectionError()
        }
      )
    },
    
    handleServerMessage(data) {
      switch(data.type) {
        case 'notification':
          this.displayNotification(data.payload)
          break
        case 'status_update':
          this.updateStatus(data.status)
          break
        default:
          console.log('Unhandled message:', data)
      }
    },
    
    sendMessage(content) {
      socketClient.dispatch({
        type: 'chat',
        content: content,
        timestamp: Date.now()
      })
    }
  }
}
Tags: Vue.js

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.