Practical WebSocket Client with Auto-Reconnection and Real-Time Message Push for Vue 3

import EventBus from "@/utils/EventBus";
import { WS_ERROR_EVENT } from "@/config/constants";
import { getAuthToken } from "@/utils/auth";

const WS_BASE_URL = import.meta.env.VITE_WS_BASE_URL;

class WebSocketClient {
  private enableAutoReconnect: boolean = true;
  public reconnectAttempts: number = 0;
  private wsInstance: WebSocket | undefined;
  private heartbeatInterval: number = 30;
  private reconnectTimer: NodeJS.Timeout | null = null;
  private heartbeatTimer: NodeJS.Timeout | null = null;
  private reconnectDelay: number = 10;
  private maxReconnectAttempts: number = 10;
  private missedHeartbeats: number = 0;
  private shouldBroadcastError: boolean = false;
  private pendingEventCallbacks: string[] = [];

  constructor(private wsPath: string = "", private protocols: string[] = []) {
    this.initializeConnection();
  }

  private initializeConnection(): void {
    if (this.wsInstance) {
      this.wsInstance.close(1000, "Reinitializing connection");
      this.wsInstance = undefined;
    }

    if (typeof window.WebSocket !== "function") {
      console.error("WebSocket is not supported by your browser");
      return;
    }

    const authToken = getAuthToken();
    const fullUrl = `${WS_BASE_URL}${this.wsPath}${authToken ? `?token=${authToken}` : ""}`;
    this.wsInstance = new WebSocket(fullUrl, this.protocols);

    this.wsInstance.onopen = () => this.handleOpen();
    this.wsInstance.onclose = () => this.handleClose();
    this.wsInstance.onerror = () => this.handleError();
    this.wsInstance.onmessage = (event) => this.handleIncomingMessage(event);
  }

  private handleOpen(): void {
    console.log("WebSocket connection established");
    this.reconnectAttempts = 0;
    if (this.enableAutoReconnect) this.startHeartbeat();
  }

  private handleError(): void {
    console.error("WebSocket connection error occurred");
    this.terminateConnection();
  }

  private handleClose(): void {
    console.log("WebSocket connection closed");
    this.terminateConnection();
  }

  private startHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
    }

    this.heartbeatTimer = setInterval(() => {
      if (this.isConnectionOpen()) {
        this.send("HEARTBEAT", { type: "PING" });
        this.missedHeartbeats++;

        if (this.missedHeartbeats > 5) {
          clearInterval(this.heartbeatTimer!);
          EventBus.emit(WS_ERROR_EVENT, "Heartbeat timeout: server unresponsive");
          this.terminateConnection();
        }
      } else {
        this.terminateConnection();
      }
    }, this.heartbeatInterval * 1000);
  }

  private formatMessage(eventType: string, payload: any): string {
    const requestId = `${eventType}_${Math.floor(Math.random() * 10000)}`;
    return JSON.stringify({
      event: eventType,
      payload: {
        requestId,
        ...payload
      }
    });
  }

  public send<T = any>(eventType: string, payload?: T, errorHandler?: (msg: string) => void): Promise<{ event: string; requestId: string }> {
    return new Promise((resolve, reject) => {
      if (!this.isConnectionOpen()) {
        console.error("WebSocket is not in connected state");
        this.terminateConnection();
        reject();
        return;
      }

      const message = this.formatMessage(eventType, payload || {});
      const parsedMsg = JSON.parse(message);
      this.wsInstance?.send(message);

      if (eventType !== "HEARTBEAT") {
        console.log("Sent WebSocket message:", message);
      } else {
        this.missedHeartbeats++;
      }

      if (errorHandler && typeof errorHandler === "function") {
        if (!EventBus.hasListener(parsedMsg.payload.requestId)) {
          EventBus.on(parsedMsg.payload.requestId, errorHandler);
          if (!this.pendingEventCallbacks.includes(parsedMsg.payload.requestId)) {
            this.pendingEventCallbacks.push(parsedMsg.payload.requestId);
          }
        }
      } else if (errorHandler) {
        console.error("Third argument to send() must be an error handler function");
      }

      resolve({
        event: eventType,
        requestId: parsedMsg.payload.requestId
      });
    });
  }

  public isConnectionOpen(): boolean {
    const isOpen = this.wsInstance?.readyState === WebSocket.OPEN;
    if (isOpen) this.reconnectAttempts = 0;
    return isOpen;
  }

  private terminateConnection(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }

    if (this.enableAutoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
      this.scheduleReconnect();
    } else if (this.reconnectAttempts >= this.maxReconnectAttempts && !this.shouldBroadcastError) {
      console.error(`Failed to reconnect after ${this.maxReconnectAttempts} attempts`);
      EventBus.emit(WS_ERROR_EVENT, "Connection lost permanently");
      this.shouldBroadcastError = true;
    }
  }

  private scheduleReconnect(): void {
    if (!this.reconnectTimer) {
      this.reconnectTimer = setTimeout(() => {
        this.reconnectAttempts++;
        console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
        this.initializeConnection();
        clearTimeout(this.reconnectTimer!);
        this.reconnectTimer = null;
      }, this.reconnectDelay * 1000);
    }
  }

  public disconnect(): void {
    this.enableAutoReconnect = false;
    this.reconnectAttempts = this.maxReconnectAttempts + 1;
    if (this.isConnectionOpen()) {
      this.wsInstance?.close(1000, "Client initiated disconnect");
    }
    this.terminateConnection();
    console.log("WebSocket connection closed by client");
  }

  private handleIncomingMessage(event: MessageEvent): void {
    const { code, payload, requestId, event: eventType, message } = JSON.parse(event.data);

    if (eventType === "HEARTBEAT") {
      this.missedHeartbeats = 0;
      this.reconnectAttempts = 0;
      return;
    }

    if (code === 200) {
      this.cleanupRequestHandler(requestId);
      EventBus.emit(eventType, payload);
      return payload;
    } else {
      EventBus.emit(requestId, message || "Request failed");
      this.cleanupRequestHandler(requestId);
      return null;
    }
  }

  private cleanupRequestHandler(requestId: string, delay: boolean = true): void {
    const timer = setTimeout(() => {
      if (EventBus.hasListener(requestId) && this.pendingEventCallbacks.includes(requestId)) {
        EventBus.off(requestId);
        this.pendingEventCallbacks = this.pendingEventCallbacks.filter(id => id !== requestId);
      }
      clearTimeout(timer);
    }, delay ? 300 : 0);
  }
}

export default WebSocketClient;

Vue 3 Integration

1. Register as Global Property

import { createApp } from 'vue';
import App from './App.vue';
import WebSocketClient from '@/utils/WebSocketClient';

const app = createApp(App);
const wsClient = new WebSocketClient();
app.config.globalProperties.$wsClient = wsClient;

app.mount('#app');

2. Use in Components

<script setup lang="ts">
import { getCurrentInstance, reactive } from 'vue';
import EventBus from '@/utils/EventBus';

const { proxy } = getCurrentInstance() as any;
const wsClient = proxy.$wsClient;

interface Message {
  id: string;
  sender: string;
  content: string;
  timestamp: number;
}

const chatState = reactive({
  currentUserId: 'user_123',
  messages: [] as Message[]
});

// Send a user message
const SEND_USER_MSG = 'SEND_USER_MESSAGE';
wsClient.send(SEND_USER_MSG, {
  recipientId: 'user_456',
  content: 'Hello from Vue 3!',
  type: 'text'
});

// Listen for incoming user messages
EventBus.on(SEND_USER_MSG, (data: Message) => {
  chatState.messages.push(data);
});

// Send system message with error handling
const SEND_SYSTEM_ALERT = 'SEND_SYSTEM_ALERT';
wsClient.send(SEND_SYSTEM_ALERT, {
  type: 'payment',
  amount: 19.99
}, (errorMsg: string) => {
  console.error('System alert failed:', errorMsg);
  // Display error to user
});

// Listen for system message responses
EventBus.on(SEND_SYSTEM_ALERT, (data: any) => {
  console.log('System alert confirmed:', data);
});
</script>

3. Manual Disconnection

import { getCurrentInstance } from 'vue';

const { proxy } = getCurrentInstance() as any;
proxy.$wsClient.disconnect();

Tags: WebSocket Vue 3 TypeScript Real-Time Messaging Auto-Reconnection

Posted on Fri, 08 May 2026 01:50:18 +0000 by johnnyboy16