Robust WebSocket Client Implementation with Auto-Reconnection and Heartbeat for Vue.js
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()
})
}
}
}