/** * Composable for WebSocket connectivity to receive live vote updates. * * Connects to the backend WS endpoint and allows subscribing to * individual vote session channels for real-time tally updates. */ export function useWebSocket() { const config = useRuntimeConfig() let ws: WebSocket | null = null let reconnectTimer: ReturnType | null = null const connected = ref(false) const lastMessage = ref(null) /** * Open a WebSocket connection to the backend live endpoint. */ function connect() { if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) { return } const wsUrl = config.public.apiBase .replace(/^http/, 'ws') .replace(/\/api\/v1$/, '/api/v1/ws/live') ws = new WebSocket(wsUrl) ws.onopen = () => { connected.value = true if (reconnectTimer) { clearTimeout(reconnectTimer) reconnectTimer = null } } ws.onclose = () => { connected.value = false reconnect() } ws.onerror = () => { connected.value = false } ws.onmessage = (event: MessageEvent) => { try { lastMessage.value = JSON.parse(event.data) } catch { lastMessage.value = event.data } } } /** * Subscribe to real-time updates for a vote session. */ function subscribe(sessionId: string) { if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: 'subscribe', session_id: sessionId })) } } /** * Unsubscribe from a vote session's updates. */ function unsubscribe(sessionId: string) { if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: 'unsubscribe', session_id: sessionId })) } } /** * Gracefully close the WebSocket connection. */ function disconnect() { if (reconnectTimer) { clearTimeout(reconnectTimer) reconnectTimer = null } if (ws) { ws.onclose = null ws.close() ws = null } connected.value = false } /** * Schedule a reconnection attempt after a delay. */ function reconnect() { if (reconnectTimer) return reconnectTimer = setTimeout(() => { reconnectTimer = null connect() }, 3000) } return { connected, lastMessage, connect, subscribe, unsubscribe, disconnect } }