// composables/useWebSocket.ts import { ref, computed, onUnmounted, getCurrentInstance, toValue } from 'vue' export interface WebSocketConfig { url: string | any clientId: string | any onMessage?: (data: any) => void onOpen?: () => void onClose?: () => void onError?: (error: Event) => void reconnectInterval?: number maxReconnectAttempts?: number fallbackPostUrl?: string // Optional: use a specific URL for the POST fallback (e.g., a proxy) } export interface WebSocketMessage { to_client: string data: any } export const useWebSocket = (config: WebSocketConfig) => { const ws = ref(null) const isConnected = ref(false) const reconnectAttempts = ref(0) const reconnectTimer = ref(null) const currentUrl = computed(() => toValue(config.url)) const currentClientId = computed(() => toValue(config.clientId)) const wsUrl = computed(() => { let baseUrl = currentUrl.value // Automatically upgrade to secure protocols if caller uses HTTPS if (typeof window !== 'undefined' && window.location.protocol === 'https:') { if (baseUrl.startsWith('ws://')) { baseUrl = baseUrl.replace('ws://', 'wss://') } else if (baseUrl.startsWith('http://')) { baseUrl = baseUrl.replace('http://', 'https://') } } const url = new URL(baseUrl) url.searchParams.set('client_id', currentClientId.value) return url.toString() }) const clearHandlers = (wsInstance: WebSocket | null) => { if (wsInstance) { wsInstance.onopen = null wsInstance.onmessage = null wsInstance.onclose = null wsInstance.onerror = null } } const connect = () => { try { // Clear any pending reconnect timer first if (reconnectTimer.value) { clearTimeout(reconnectTimer.value) reconnectTimer.value = null } // Close existing connection if any, and CLEAN HANDLERS to prevent recursion if (ws.value) { if (ws.value.readyState !== WebSocket.CLOSED) { console.log('🔌 Closing existing WebSocket before new connection...') clearHandlers(ws.value) ws.value.close() } ws.value = null } const connectionUrl = wsUrl.value console.log('🔌 Connecting to WebSocket:', connectionUrl) ws.value = new WebSocket(connectionUrl) ws.value.onopen = () => { console.log('✅ WebSocket connected:', currentClientId.value) isConnected.value = true reconnectAttempts.value = 0 config.onOpen?.() } ws.value.onmessage = (event) => { try { const data = JSON.parse(event.data) config.onMessage?.(data) } catch (error) { console.error('Error parsing WebSocket message:', error) } } ws.value.onclose = () => { console.log('❌ WebSocket closed:', currentClientId.value) isConnected.value = false config.onClose?.() // Attempt to reconnect only if we didn't just reach max attempts // The reconnectTimer is cleared in disconnect() and at start of connect() if (reconnectAttempts.value < (config.maxReconnectAttempts || 5)) { reconnectAttempts.value++ const interval = config.reconnectInterval || 3000 console.log(`âŗ Reconnecting in ${interval}ms... Attempt ${reconnectAttempts.value}`) reconnectTimer.value = setTimeout(() => { connect() }, interval) } else { console.error('Max reconnection attempts reached') } } ws.value.onerror = (error) => { console.error('âš ī¸ WebSocket error:', error) config.onError?.(error) } } catch (error) { console.error('❌ Error creating WebSocket:', error) config.onError?.(error as Event) } } const disconnect = () => { if (reconnectTimer.value) { clearTimeout(reconnectTimer.value) reconnectTimer.value = null } if (ws.value) { console.log('🔌 Manual disconnect: Cleaning handlers and closing...') clearHandlers(ws.value) ws.value.close() ws.value = null } isConnected.value = false reconnectAttempts.value = 0 } const sendMessage = (message: any) => { if (ws.value && ws.value.readyState === WebSocket.OPEN) { ws.value.send(JSON.stringify(message)) return true } console.warn('WebSocket is not connected') return false } // Send data via POST to WebSocket server const sendViaPost = async (message: WebSocketMessage) => { try { let postUrl = '' if (config.fallbackPostUrl) { // Use the explicitly provided fallback URL (likely a relative proxy path) postUrl = config.fallbackPostUrl } else { // Derive post URL from WebSocket URL (legacy behavior) let baseUrl = currentUrl.value if (baseUrl.startsWith('ws://')) { baseUrl = baseUrl.replace('ws://', 'http://') } else if (baseUrl.startsWith('wss://')) { baseUrl = baseUrl.replace('wss://', 'https://') } const urlObj = new URL(baseUrl) postUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}` } console.log('📡 POST URL:', postUrl) console.log('đŸ“Ļ POST Body:', JSON.stringify(message, null, 2)) const response = await fetch(postUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(message), }) console.log('đŸ“Ĩ POST Response status:', response.status, response.statusText) if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error') console.error('❌ POST Error response:', errorText) throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`) } const result = await response.json().catch(() => { console.log('â„šī¸ Response is not JSON, assuming success') return { success: true } }) console.log('✅ POST Response data:', result) return result } catch (error) { console.error('❌ Error sending message via POST:', error) throw error } } if (getCurrentInstance()) { onUnmounted(() => { disconnect() }) } return { ws: computed(() => ws.value), isConnected: computed(() => isConnected.value), connect, disconnect, sendMessage, sendViaPost, reconnectAttempts: computed(() => reconnectAttempts.value), } }