/** * SAAP WebSocket Composable - Real-time Updates * WebSocket connection for live agent status and message updates */ import { ref, reactive, onUnmounted } from 'vue' import { io, type Socket } from 'socket.io-client' // Types interface WebSocketMessage { type: 'agent_update' | 'message_update' | 'system_update' | 'error' data: any timestamp: string } interface ConnectionStatus { connected: boolean reconnecting: boolean error: string | null lastPing: string | null connectionCount: number } export const useWebSocket = () => { // WebSocket connection let socket: Socket | null = null let reconnectTimer: NodeJS.Timeout | null = null // Reactive state const connectionStatus = reactive({ connected: false, reconnecting: false, error: null, lastPing: null, connectionCount: 0 }) const messages = ref([]) const lastMessage = ref(null) // WebSocket URL (matching FastAPI WebSocket endpoint) const WS_URL = 'ws://localhost:8000/ws' const RECONNECT_DELAY = 3000 const MAX_RECONNECT_ATTEMPTS = 5 let reconnectAttempts = 0 // Event listeners const eventListeners = new Map>() // ===================================================== // CONNECTION MANAGEMENT // ===================================================== const connect = (): Promise => { return new Promise((resolve) => { try { // Close existing connection if (socket) { socket.disconnect() socket = null } console.log('🌐 Connecting to SAAP WebSocket:', WS_URL) // For FastAPI WebSocket, we use native WebSocket API const ws = new WebSocket(WS_URL) ws.onopen = (event) => { console.log('✅ WebSocket connected') connectionStatus.connected = true connectionStatus.reconnecting = false connectionStatus.error = null connectionStatus.connectionCount++ connectionStatus.lastPing = new Date().toISOString() reconnectAttempts = 0 // Send initial ping ws.send(JSON.stringify({ type: 'ping', timestamp: new Date().toISOString() })) // Emit connect event emitEvent('connect', { status: 'connected' }) resolve(true) } ws.onmessage = (event) => { try { // Handle text messages let data: any = event.data // Try to parse JSON if it's a string if (typeof data === 'string') { try { data = JSON.parse(data) } catch { // If not JSON, treat as plain message console.log('📝 WebSocket message:', data) return } } // Create WebSocket message const message: WebSocketMessage = { type: data.type || 'system_update', data: data, timestamp: new Date().toISOString() } // Store message messages.value.push(message) lastMessage.value = message // Keep only last 100 messages if (messages.value.length > 100) { messages.value = messages.value.slice(-100) } // Update last ping connectionStatus.lastPing = new Date().toISOString() // Emit specific event based on message type emitEvent(message.type, message.data) emitEvent('message', message) console.log('📨 WebSocket message received:', message.type, message.data) } catch (error) { console.error('❌ WebSocket message parse error:', error) } } ws.onclose = (event) => { console.log('🔌 WebSocket disconnected:', event.code, event.reason) connectionStatus.connected = false if (!connectionStatus.reconnecting) { handleReconnect() } emitEvent('disconnect', { code: event.code, reason: event.reason }) } ws.onerror = (error) => { console.error('❌ WebSocket error:', error) connectionStatus.error = 'Connection error' connectionStatus.connected = false emitEvent('error', { error }) resolve(false) } // Store WebSocket instance socket = ws as any } catch (error) { console.error('❌ WebSocket connection failed:', error) connectionStatus.error = error instanceof Error ? error.message : 'Unknown error' resolve(false) } }) } const disconnect = () => { if (reconnectTimer) { clearTimeout(reconnectTimer) reconnectTimer = null } if (socket) { (socket as any).close() socket = null } connectionStatus.connected = false connectionStatus.reconnecting = false console.log('🔌 WebSocket manually disconnected') } const handleReconnect = () => { if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { console.error('❌ Max reconnection attempts reached') connectionStatus.error = 'Failed to reconnect' return } connectionStatus.reconnecting = true reconnectAttempts++ console.log(`🔄 Attempting to reconnect... (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`) reconnectTimer = setTimeout(() => { connect() }, RECONNECT_DELAY) } // ===================================================== // MESSAGE SENDING // ===================================================== const send = (message: any): boolean => { if (!socket || !connectionStatus.connected) { console.warn('⚠️ Cannot send message: WebSocket not connected') return false } try { const payload = JSON.stringify({ ...message, timestamp: new Date().toISOString() }) ;(socket as any).send(payload) console.log('📤 WebSocket message sent:', message) return true } catch (error) { console.error('❌ Failed to send WebSocket message:', error) return false } } const ping = (): boolean => { return send({ type: 'ping' }) } // ===================================================== // EVENT SYSTEM // ===================================================== const on = (event: string, callback: Function) => { if (!eventListeners.has(event)) { eventListeners.set(event, new Set()) } eventListeners.get(event)!.add(callback) } const off = (event: string, callback: Function) => { const listeners = eventListeners.get(event) if (listeners) { listeners.delete(callback) } } const emitEvent = (event: string, data: any) => { const listeners = eventListeners.get(event) if (listeners) { listeners.forEach(callback => { try { callback(data) } catch (error) { console.error(`❌ Error in WebSocket event listener (${event}):`, error) } }) } } // ===================================================== // SPECIALIZED HANDLERS // ===================================================== const subscribeToAgentUpdates = (callback: (agent: any) => void) => { on('agent_update', callback) } const subscribeToMessageUpdates = (callback: (message: any) => void) => { on('message_update', callback) } const subscribeToSystemUpdates = (callback: (status: any) => void) => { on('system_update', callback) } // ===================================================== // LIFECYCLE // ===================================================== // Auto-cleanup on component unmount onUnmounted(() => { disconnect() eventListeners.clear() }) // Periodic ping to keep connection alive let pingTimer: NodeJS.Timeout | null = null const startHeartbeat = () => { if (pingTimer) clearInterval(pingTimer) pingTimer = setInterval(() => { if (connectionStatus.connected) { ping() } }, 30000) // Ping every 30 seconds } const stopHeartbeat = () => { if (pingTimer) { clearInterval(pingTimer) pingTimer = null } } return { // State connectionStatus, messages, lastMessage, // Connection Management connect, disconnect, // Messaging send, ping, // Event System on, off, // Specialized Subscriptions subscribeToAgentUpdates, subscribeToMessageUpdates, subscribeToSystemUpdates, // Heartbeat startHeartbeat, stopHeartbeat, // Configuration WS_URL, RECONNECT_DELAY, MAX_RECONNECT_ATTEMPTS } } // Export types export type { WebSocketMessage, ConnectionStatus }