Spaces:
Sleeping
Sleeping
| /** | |
| * 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<ConnectionStatus>({ | |
| connected: false, | |
| reconnecting: false, | |
| error: null, | |
| lastPing: null, | |
| connectionCount: 0 | |
| }) | |
| const messages = ref<WebSocketMessage[]>([]) | |
| const lastMessage = ref<WebSocketMessage | null>(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<string, Set<Function>>() | |
| // ===================================================== | |
| // CONNECTION MANAGEMENT | |
| // ===================================================== | |
| const connect = (): Promise<boolean> => { | |
| 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 } |