Hwandji's picture
feat: initial HuggingFace Space deployment
4343907
raw
history blame
9.14 kB
/**
* 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 }