import { ref, computed, reactive, type Ref } from "vue"; import type { WebSocketMessage, ConnectionState, WebSocketConfig, MessageHistory, ConnectionStats, MonitoringData, ClientInfo, OnlineUser, ActivityLog, } from "../types/websocket"; export const useWebSocket = () => { // Check if we're in browser environment const isBrowser = process.client; // WebSocket connection const ws: Ref = ref(null); // Connection state - consolidated into single reactive object const connectionState = reactive({ isConnected: false, isConnecting: false, connectionStatus: "disconnected", clientId: null, staticId: null, currentRoom: null, userId: "anonymous", ipAddress: null, connectionStartTime: null, lastPingTime: null, connectionLatency: 0, connectionHealth: "poor", reconnectAttempts: 0, messagesReceived: 0, messagesSent: 0, uptime: "00:00:00", }); // Configuration const config = reactive({ wsUrl: "ws://localhost:8080/api/v1/ws", userId: "anonymous", room: "default", staticId: "", useIPBasedId: false, autoReconnect: true, heartbeatEnabled: true, maxReconnectAttempts: 10, reconnectDelay: 1000, maxReconnectDelay: 30000, heartbeatInterval: 30000, heartbeatTimeout: 5000, maxMissedHeartbeats: 3, maxMessages: 1000, messageWarningThreshold: 800, actionThrottle: 100, }); // Message history and data const messages = ref([]); const stats = ref(null); const monitoringData = ref(null); const onlineUsers = ref([]); const activityLog = ref([]); // Admin functionality const serverInfo = ref(null); const systemHealth = ref(null); // Timer references for proper cleanup let reconnectTimeout: number | null = null; let heartbeatInterval: number | null = null; let heartbeatTimeout: number | null = null; let uptimeInterval: number | null = null; let connectionStartTime: number | null = null; // Heartbeat tracking let missedHeartbeats = 0; let lastHeartbeatTime = 0; let isManualDisconnect = false; // Helper function to update connection status const updateConnectionStatus = ( status: "disconnected" | "connecting" | "connected" | "error" ) => { connectionState.connectionStatus = status; switch (status) { case "connected": connectionState.isConnected = true; connectionState.isConnecting = false; connectionState.connectionHealth = "good"; break; case "connecting": connectionState.isConnected = false; connectionState.isConnecting = true; connectionState.connectionHealth = "warning"; break; case "disconnected": case "error": connectionState.isConnected = false; connectionState.isConnecting = false; connectionState.connectionHealth = "poor"; break; } }; // Helper function to update uptime const updateUptime = () => { if (connectionStartTime && connectionState.isConnected) { const now = Date.now(); const uptime = now - connectionStartTime; const seconds = Math.floor(uptime / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); connectionState.uptime = `${hours.toString().padStart(2, "0")}:${( minutes % 60 ) .toString() .padStart(2, "0")}:${(seconds % 60).toString().padStart(2, "0")}`; } else { connectionState.uptime = "00:00:00"; } }; // Timer management const startUptimeTimer = () => { if (uptimeInterval) { clearInterval(uptimeInterval); } uptimeInterval = window.setInterval(updateUptime, 1000); }; const stopUptimeTimer = () => { if (uptimeInterval) { clearInterval(uptimeInterval); uptimeInterval = null; } }; // Message handling const getIconForMessageType = (type: string): string => { switch (type) { case "success": return "đŸŸĸ"; case "error": return "❌"; case "warning": return "âš ī¸"; case "info": return "â„šī¸"; default: return "📝"; } }; const addMessage = ( type: string, data: any, messageId?: string, icon?: string ) => { if (!isBrowser) return; const now = new Date(); const timeString = now.toLocaleTimeString("en-US", { hour12: true, hour: "numeric", minute: "2-digit", second: "2-digit", }); // Enhanced message formatting based on type let formattedData = data; let displayType = type; switch (type) { case "connection_info": displayType = "info"; formattedData = { client_id: data.clientId, connected_at: data.connectedAt, ip_address: data.ipAddress, last_ping: data.lastPingTime || "N/A", room: data.room || "default", static_id: data.staticId || "none", user_id: data.userId || "anonymous", }; break; case "heartbeat": displayType = "info"; formattedData = `â¤ī¸ Heartbeat started\nInterval: ${ config.heartbeatInterval / 1000 }s`; break; case "pong": displayType = "success"; formattedData = `🏓 Pong received\nLatency: ${connectionState.connectionLatency}ms`; break; case "message": displayType = "info"; formattedData = `📨 Message received\n${JSON.stringify(data, null, 2)}`; break; case "broadcast": displayType = "info"; formattedData = `📡 Broadcast received\n${data}`; break; case "direct_message": displayType = "info"; formattedData = `📨 Direct message from ${data.from}\n${data.message}`; break; case "room_message": displayType = "info"; formattedData = `đŸ“ĸ Room message from ${data.room}\n${data.message}`; break; case "stats": displayType = "info"; formattedData = `📊 Stats updated\n${JSON.stringify(data, null, 2)}`; break; case "monitoring": displayType = "info"; formattedData = `📈 Monitoring data\n${JSON.stringify(data, null, 2)}`; break; case "online_users": displayType = "info"; formattedData = `đŸ‘Ĩ Online users updated\nCount: ${ data.users?.length || 0 }`; break; case "server_info": displayType = "info"; formattedData = `🔧 Server info\n${JSON.stringify(data, null, 2)}`; break; case "system_health": displayType = "info"; formattedData = `💚 System health\n${JSON.stringify(data, null, 2)}`; break; default: if (typeof data === "string") { formattedData = data; } else { formattedData = JSON.stringify(data, null, 2); } } const message: MessageHistory = { timestamp: now, type: displayType, data: formattedData, messageId, size: JSON.stringify(data).length, icon: icon || getIconForMessageType(displayType), timeString: timeString, }; messages.value.unshift(message); // Keep only the last maxMessages if (messages.value.length > config.maxMessages) { messages.value = messages.value.slice(0, config.maxMessages); } // Update connection state connectionState.messagesReceived++; }; // Computed properties const connectionHealthColor = computed(() => { switch (connectionState.connectionHealth) { case "excellent": return "#4CAF50"; case "good": return "#2196F3"; case "warning": return "#FFC107"; case "poor": return "#F44336"; default: return "#9E9E9E"; } }); const connectionHealthText = computed(() => { switch (connectionState.connectionHealth) { case "excellent": return "Excellent"; case "good": return "Good"; case "warning": return "Warning"; case "poor": return "Poor"; default: return "Unknown"; } }); const isMessageLimitReached = computed(() => { return messages.value.length >= config.maxMessages; }); const shouldShowMessageWarning = computed(() => { return messages.value.length >= config.messageWarningThreshold; }); // Timer cleanup const cleanupTimers = () => { if (!isBrowser) return; if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } if (heartbeatTimeout) { clearTimeout(heartbeatTimeout); heartbeatTimeout = null; } stopUptimeTimer(); }; // Connection cleanup const cleanupConnection = () => { if (!isBrowser) return; if (ws.value) { ws.value.close(); ws.value = null; } cleanupTimers(); }; // Get close reason description const getCloseReason = (code: number): string => { switch (code) { case 1000: return "Normal closure"; case 1001: return "Going away"; case 1002: return "Protocol error"; case 1003: return "Unsupported data"; case 1004: return "Reserved"; case 1005: return "No status received"; case 1006: return "Abnormal closure"; case 1007: return "Invalid frame payload data"; case 1008: return "Policy violation"; case 1009: return "Message too big"; case 1010: return "Mandatory extension"; case 1011: return "Internal server error"; case 1012: return "Service restart"; case 1013: return "Try again later"; case 1014: return "Bad gateway"; case 1015: return "TLS handshake"; default: return "Unknown reason"; } }; // Heartbeat management const stopHeartbeat = () => { if (!isBrowser) return; if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } if (heartbeatTimeout) { clearTimeout(heartbeatTimeout); heartbeatTimeout = null; } missedHeartbeats = 0; }; const startHeartbeat = () => { if (!isBrowser || !config.heartbeatEnabled) return; stopHeartbeat(); heartbeatInterval = window.setInterval(() => { if (ws.value && ws.value.readyState === WebSocket.OPEN) { const heartbeatMessage = { type: "ping", timestamp: Date.now(), }; ws.value.send(JSON.stringify(heartbeatMessage)); lastHeartbeatTime = Date.now(); // Set timeout for heartbeat response heartbeatTimeout = window.setTimeout(() => { missedHeartbeats++; if (missedHeartbeats >= config.maxMissedHeartbeats) { addMessage( "❌ Heartbeat timeout - connection unhealthy", "warning" ); connectionState.connectionHealth = "warning"; } }, config.heartbeatTimeout); } }, config.heartbeatInterval); }; // Reconnection logic const scheduleReconnect = () => { if (!isBrowser || !config.autoReconnect) return; if (connectionState.reconnectAttempts >= config.maxReconnectAttempts) { addMessage("❌ Max reconnection attempts reached", "error"); return; } connectionState.reconnectAttempts++; const delay = Math.min( config.reconnectDelay * Math.pow(2, connectionState.reconnectAttempts - 1), config.maxReconnectDelay ); addMessage( `🔄 Reconnecting in ${delay}ms (attempt ${connectionState.reconnectAttempts}/${config.maxReconnectAttempts})`, "info" ); reconnectTimeout = window.setTimeout(() => { connect(); }, delay); }; // Message handling const handleIncomingMessage = (event: MessageEvent) => { if (!isBrowser) return; try { const data = JSON.parse(event.data); // Handle heartbeat response if (data.type === "pong") { missedHeartbeats = 0; connectionState.connectionHealth = "excellent"; const latency = Date.now() - lastHeartbeatTime; connectionState.connectionLatency = latency; return; } // Handle connection info if (data.type === "connection_info") { connectionState.clientId = data.clientId; connectionState.staticId = data.staticId; connectionState.currentRoom = data.room; connectionState.userId = data.userId; connectionState.ipAddress = data.ipAddress; return; } // Handle welcome message (server sends connection info in welcome message) if (data.type === "welcome") { console.log("Received welcome message:", data); // Map server snake_case fields to camelCase for Vue components if (data.client_id) { connectionState.clientId = data.client_id; console.log("Set clientId:", data.client_id); } if (data.static_id) { connectionState.staticId = data.static_id; console.log("Set staticId:", data.static_id); } if (data.room) { connectionState.currentRoom = data.room; console.log("Set currentRoom:", data.room); } if (data.user_id) { connectionState.userId = data.user_id; console.log("Set userId:", data.user_id); } if (data.ip_address) { connectionState.ipAddress = data.ip_address; console.log("Set ipAddress:", data.ip_address); } if (data.connected_at) { connectionState.connectionStartTime = data.connected_at * 1000; console.log("Set connectionStartTime:", data.connected_at * 1000); } // Force reactive update by triggering a change connectionState.connectionHealth = connectionState.connectionHealth === "good" ? "excellent" : "good"; console.log("Updated connectionState:", connectionState); addMessage( "đŸŸĸ Connection established successfully", "success", "Welcome message processed" ); return; } // Handle stats if (data.type === "stats") { stats.value = data; return; } // Handle monitoring data if (data.type === "monitoring") { monitoringData.value = data; return; } // Handle online users if (data.type === "online_users") { onlineUsers.value = data.users; return; } // Handle activity log if (data.type === "activity") { activityLog.value.unshift(data); if (activityLog.value.length > 100) { activityLog.value = activityLog.value.slice(0, 100); } return; } // Handle server info if (data.type === "server_info") { serverInfo.value = data; return; } // Handle system health if (data.type === "system_health") { systemHealth.value = data; return; } // Handle regular messages addMessage(data.type || "message", data, data.messageId); } catch (error) { addMessage( "❌ Failed to parse message", "error", error instanceof Error ? error.message : String(error) ); } }; // WebSocket connection methods const connect = () => { if (!isBrowser) return; // Prevent multiple connection attempts if (connectionState.isConnecting || connectionState.isConnected) { return; } // Clean up existing connection if (ws.value) { cleanupConnection(); } updateConnectionStatus("connecting"); isManualDisconnect = false; connectionStartTime = Date.now(); startUptimeTimer(); try { // Build WebSocket URL with parameters let url = config.wsUrl; const params = new URLSearchParams(); if (config.userId) { params.append("user_id", config.userId); } if (config.room) { params.append("room", config.room); } if (config.useIPBasedId) { params.append("ip_based", "true"); } else if (config.staticId) { params.append("static_id", config.staticId); } if (params.toString()) { url += "?" + params.toString(); } ws.value = new WebSocket(url); // Connection timeout const connectionTimeout = setTimeout(() => { if (ws.value && ws.value.readyState === WebSocket.CONNECTING) { ws.value.close(); updateConnectionStatus("error"); addMessage("❌ Connection timeout after 15 seconds", "error"); scheduleReconnect(); } }, 15000); ws.value.onopen = function (event) { clearTimeout(connectionTimeout); updateConnectionStatus("connected"); connectionState.reconnectAttempts = 0; connectionState.connectionStartTime = connectionStartTime; addMessage( "đŸŸĸ Connected to WebSocket server", "success", "Connection established successfully" ); // Start heartbeat if enabled if (config.heartbeatEnabled) { startHeartbeat(); } }; ws.value.onmessage = function (event) { handleIncomingMessage(event); }; ws.value.onclose = function (event) { clearTimeout(connectionTimeout); cleanupTimers(); updateConnectionStatus("disconnected"); const reason = getCloseReason(event.code); addMessage( `🔴 Disconnected: ${reason}`, "error", `Code: ${event.code}, Reason: ${event.reason || "Unknown"}` ); // Auto-reconnect logic if (!isManualDisconnect && config.autoReconnect) { scheduleReconnect(); } }; ws.value.onerror = function (error) { updateConnectionStatus("error"); addMessage("❌ WebSocket Error occurred", "error", error.toString()); }; } catch (error) { updateConnectionStatus("error"); addMessage( "❌ Failed to create WebSocket connection", "error", error instanceof Error ? error.message : String(error) ); scheduleReconnect(); } }; const disconnect = () => { if (!isBrowser) return; isManualDisconnect = true; cleanupConnection(); updateConnectionStatus("disconnected"); addMessage("🔴 Manually disconnected from WebSocket", "info"); }; // Message sending methods const sendMessage = (message: any) => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot send message: not connected", "error"); return false; } try { const messageData = { type: "message", data: message, timestamp: Date.now(), }; ws.value.send(JSON.stringify(messageData)); connectionState.messagesSent++; addMessage("📤 Message sent", "info", message); return true; } catch (error) { addMessage( "❌ Failed to send message", "error", error instanceof Error ? error.message : String(error) ); return false; } }; const broadcastMessage = (message: string) => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot broadcast message: not connected", "error"); return false; } try { const messageData = { type: "broadcast", message, timestamp: Date.now(), }; ws.value.send(JSON.stringify(messageData)); connectionState.messagesSent++; addMessage("📡 Message broadcasted", "info", message); return true; } catch (error) { addMessage( "❌ Failed to broadcast message", "error", error instanceof Error ? error.message : String(error) ); return false; } }; const sendDirectMessage = (clientId: string, message: string) => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot send direct message: not connected", "error"); return false; } try { const messageData = { type: "direct_message", clientId, message, timestamp: Date.now(), }; ws.value.send(JSON.stringify(messageData)); connectionState.messagesSent++; addMessage(`📨 Direct message sent to ${clientId}`, "info", message); return true; } catch (error) { addMessage( "❌ Failed to send direct message", "error", error instanceof Error ? error.message : String(error) ); return false; } }; const sendRoomMessage = (room: string, message: string) => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot send room message: not connected", "error"); return false; } try { const messageData = { type: "room_message", room, message, timestamp: Date.now(), }; ws.value.send(JSON.stringify(messageData)); connectionState.messagesSent++; addMessage(`đŸ“ĸ Room message sent to ${room}`, "info", message); return true; } catch (error) { addMessage( "❌ Failed to send room message", "error", error instanceof Error ? error.message : String(error) ); return false; } }; // Admin and utility methods const executeAdminCommand = async (command: string, params: any) => { if (!isBrowser || !ws.value) throw new Error("Not connected"); const message = { type: "admin_command", command, params, timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); return { success: true, message: "Command sent successfully" }; }; const getServerInfo = async () => { if (!isBrowser || !ws.value) throw new Error("Not connected"); const message = { type: "get_server_info", timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); }; const getSystemHealth = async () => { if (!isBrowser || !ws.value) throw new Error("Not connected"); const message = { type: "get_system_health", timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); }; const getOnlineUsers = () => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot get online users: not connected", "error"); return; } try { const message = { type: "get_online_users", timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); } catch (error) { addMessage( "❌ Failed to request online users", "error", error instanceof Error ? error.message : String(error) ); } }; const testConnection = () => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot test connection: not connected", "error"); return; } try { const message = { type: "ping", timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); addMessage("🏓 Connection test sent", "info"); } catch (error) { addMessage( "❌ Failed to test connection", "error", error instanceof Error ? error.message : String(error) ); } }; const sendHeartbeat = () => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot send heartbeat: not connected", "error"); return; } try { const message = { type: "ping", timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); lastHeartbeatTime = Date.now(); } catch (error) { addMessage( "❌ Failed to send heartbeat", "error", error instanceof Error ? error.message : String(error) ); } }; const executeDatabaseQuery = async (query: string) => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { throw new Error("Not connected"); } try { const message = { type: "database_query", query, timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); return { success: true, message: "Database query sent successfully" }; } catch (error) { throw new Error( `Failed to execute database query: ${ error instanceof Error ? error.message : String(error) }` ); } }; const triggerNotification = async (message: string) => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { throw new Error("Not connected"); } try { const notificationData = { type: "notification", message, timestamp: Date.now(), }; ws.value.send(JSON.stringify(notificationData)); return { success: true, message: "Notification sent successfully" }; } catch (error) { throw new Error( `Failed to trigger notification: ${ error instanceof Error ? error.message : String(error) }` ); } }; const getStats = () => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot get stats: not connected", "error"); return; } try { const message = { type: "get_stats", timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); } catch (error) { addMessage( "❌ Failed to request stats", "error", error instanceof Error ? error.message : String(error) ); } }; const getMonitoringData = () => { if (!isBrowser || !ws.value || ws.value.readyState !== WebSocket.OPEN) { addMessage("❌ Cannot get monitoring data: not connected", "error"); return; } try { const message = { type: "get_monitoring", timestamp: Date.now(), }; ws.value.send(JSON.stringify(message)); } catch (error) { addMessage( "❌ Failed to request monitoring data", "error", error instanceof Error ? error.message : String(error) ); } }; // Utility methods const clearMessages = () => { if (!isBrowser) return; messages.value = []; }; const clearActivityLog = () => { if (!isBrowser) return; activityLog.value = []; }; const getMessagesByType = (type: string) => { return messages.value.filter((msg) => msg.type === type); }; const getRecentMessages = (count: number = 10) => { return messages.value.slice(0, count); }; // Cleanup on unmount const cleanup = () => { if (!isBrowser) return; disconnect(); cleanupTimers(); }; return { // State ws, connectionState, config, messages, stats, monitoringData, onlineUsers, activityLog, // Admin state serverInfo, systemHealth, // Methods connect, disconnect, sendMessage, broadcastMessage, sendDirectMessage, sendRoomMessage, getServerInfo, getOnlineUsers, testConnection, sendHeartbeat, executeDatabaseQuery, triggerNotification, getStats, getMonitoringData, executeAdminCommand, getSystemHealth, clearMessages, clearActivityLog, getMessagesByType, getRecentMessages, cleanup, // Computed isMessageLimitReached, shouldShowMessageWarning, connectionHealthColor, connectionHealthText, }; };