| |
| |
| |
| |
|
|
| import { ref, reactive, onUnmounted } from 'vue' |
| import { io, type Socket } from 'socket.io-client' |
|
|
| |
| 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 = () => { |
| |
| let socket: Socket | null = null |
| let reconnectTimer: NodeJS.Timeout | null = null |
| |
| |
| const connectionStatus = reactive<ConnectionStatus>({ |
| connected: false, |
| reconnecting: false, |
| error: null, |
| lastPing: null, |
| connectionCount: 0 |
| }) |
| |
| const messages = ref<WebSocketMessage[]>([]) |
| const lastMessage = ref<WebSocketMessage | null>(null) |
| |
| |
| const WS_URL = 'ws://localhost:8000/ws' |
| const RECONNECT_DELAY = 3000 |
| const MAX_RECONNECT_ATTEMPTS = 5 |
| let reconnectAttempts = 0 |
| |
| |
| const eventListeners = new Map<string, Set<Function>>() |
| |
| |
| |
| |
| |
| const connect = (): Promise<boolean> => { |
| return new Promise((resolve) => { |
| try { |
| |
| if (socket) { |
| socket.disconnect() |
| socket = null |
| } |
| |
| console.log('🌐 Connecting to SAAP WebSocket:', WS_URL) |
| |
| |
| 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 |
| |
| |
| ws.send(JSON.stringify({ type: 'ping', timestamp: new Date().toISOString() })) |
| |
| |
| emitEvent('connect', { status: 'connected' }) |
| resolve(true) |
| } |
| |
| ws.onmessage = (event) => { |
| try { |
| |
| let data: any = event.data |
| |
| |
| if (typeof data === 'string') { |
| try { |
| data = JSON.parse(data) |
| } catch { |
| |
| console.log('📝 WebSocket message:', data) |
| return |
| } |
| } |
| |
| |
| const message: WebSocketMessage = { |
| type: data.type || 'system_update', |
| data: data, |
| timestamp: new Date().toISOString() |
| } |
| |
| |
| messages.value.push(message) |
| lastMessage.value = message |
| |
| |
| if (messages.value.length > 100) { |
| messages.value = messages.value.slice(-100) |
| } |
| |
| |
| connectionStatus.lastPing = new Date().toISOString() |
| |
| |
| 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) |
| } |
| |
| |
| 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) |
| } |
| |
| |
| |
| |
| |
| 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' }) |
| } |
| |
| |
| |
| |
| |
| 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) |
| } |
| }) |
| } |
| } |
| |
| |
| |
| |
| |
| 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) |
| } |
| |
| |
| |
| |
| |
| |
| onUnmounted(() => { |
| disconnect() |
| eventListeners.clear() |
| }) |
| |
| |
| let pingTimer: NodeJS.Timeout | null = null |
| |
| const startHeartbeat = () => { |
| if (pingTimer) clearInterval(pingTimer) |
| |
| pingTimer = setInterval(() => { |
| if (connectionStatus.connected) { |
| ping() |
| } |
| }, 30000) |
| } |
| |
| const stopHeartbeat = () => { |
| if (pingTimer) { |
| clearInterval(pingTimer) |
| pingTimer = null |
| } |
| } |
| |
| return { |
| |
| connectionStatus, |
| messages, |
| lastMessage, |
| |
| |
| connect, |
| disconnect, |
| |
| |
| send, |
| ping, |
| |
| |
| on, |
| off, |
| |
| |
| subscribeToAgentUpdates, |
| subscribeToMessageUpdates, |
| subscribeToSystemUpdates, |
| |
| |
| startHeartbeat, |
| stopHeartbeat, |
| |
| |
| WS_URL, |
| RECONNECT_DELAY, |
| MAX_RECONNECT_ATTEMPTS |
| } |
| } |
|
|
| |
| export type { WebSocketMessage, ConnectionStatus } |