import { browser } from '$app/environment'; let ws = null; let reconnectTimeout = null; let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 10; const BASE_RECONNECT_DELAY = 1000; // 1 second const MAX_RECONNECT_DELAY = 30000; // 30 seconds // Dynamically detect WebSocket URL from browser location // This ensures production uses wss:// and the correct host function getWebSocketURL() { if (!browser) { return import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}/ws`; } const WS_URL = getWebSocketURL(); /** * Calculate exponential backoff delay with jitter * @returns {number} Delay in milliseconds */ function getReconnectDelay() { // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s, 30s... const exponentialDelay = BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts); const cappedDelay = Math.min(exponentialDelay, MAX_RECONNECT_DELAY); // Add random jitter (±20%) to prevent thundering herd const jitter = cappedDelay * 0.2 * (Math.random() - 0.5); return Math.floor(cappedDelay + jitter); } export function connectWebSocket(onMessage) { if (ws?.readyState === WebSocket.OPEN) return; // WebSocket doesn't support withCredentials, but cookies are sent automatically // on same-origin requests ws = new WebSocket(`${WS_URL}/stream`); ws.onopen = () => { console.log('WebSocket connected'); reconnectAttempts = 0; // Reset on successful connection ws?.send(JSON.stringify({ type: 'subscribe' })); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); onMessage(data); } catch (error) { console.error('Failed to parse WebSocket message:', error); } }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; ws.onclose = () => { console.log('WebSocket disconnected'); if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { console.error('Max reconnect attempts reached, giving up'); return; } const delay = getReconnectDelay(); reconnectAttempts++; console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`); reconnectTimeout = setTimeout(() => { connectWebSocket(onMessage); }, delay); }; } export function disconnectWebSocket() { if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } if (ws) { ws.close(); ws = null; } reconnectAttempts = 0; // Reset attempts on explicit disconnect } /** * Reset reconnect attempts counter (call this to allow reconnection after max attempts) */ export function resetReconnectAttempts() { reconnectAttempts = 0; } export function sendMessage(message) { if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(message)); } }