From 4ed5c24f84a1b7750ee8b816218f189467b6effd Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 12 Mar 2026 15:45:05 -0400 Subject: [PATCH] feat(websocket): add reusable WebSocket connection helper utility - Create createWebSocket() helper for WebSocket connections with authentication - Support automatic reconnection with configurable delay - Include error handling and logging - Export disconnectWebSocket() for cleanup This helper provides a centralized way to create WebSocket connections with JWT token authentication from localStorage. Although not currently used in the application (we opted for server-side token injection in templates), it provides a reusable utility for future WebSocket integrations. Features: - Automatic token retrieval from localStorage - Configurable reconnection behavior (enabled by default) - Error handling with try-catch on all callbacks - Connection cleanup and management - Type-safe configuration interface Available for future use in client-side WebSocket scenarios or as a reference implementation. --- web/src/websocket.ts | 92 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 web/src/websocket.ts diff --git a/web/src/websocket.ts b/web/src/websocket.ts new file mode 100644 index 0000000..0de551e --- /dev/null +++ b/web/src/websocket.ts @@ -0,0 +1,92 @@ +import { getToken } from "./storage"; +interface WebSocketConfig { + onMessage: (data: any) => void; + onOpen?: () => void; + onClose?: () => void; + onError?: (error: Event) => void; + enableReconnect?: boolean; + reconnectDelay?: number; +} +let activeWebSocket: WebSocket | null = null; +let reconnectTimeout: ReturnType | null = null; +function createWebSocket(config: WebSocketConfig): WebSocket | null { + const token = getToken(); + if (!token) { + console.warn( + "No authentication token found - skipping WebSocket connection", + ); + return null; + } + // Clean up existing connection + if (activeWebSocket) { + activeWebSocket.close(); + } + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeout = null; + } + const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const wsUrl = `${protocol}//${window.location.host}/ws/sync?token=${token}`; + const ws = new WebSocket(wsUrl); + ws.onopen = () => { + console.log("WebSocket connected"); + if (config.onOpen) { + try { + config.onOpen(); + } catch (error) { + console.error("Error in onOpen callback:", error); + } + } + }; + ws.onmessage = (event: MessageEvent) => { + try { + const message = JSON.parse(event.data); + config.onMessage(message); + } catch (error) { + console.error("Error parsing WebSocket message:", error); + console.error("Raw message:", event.data); + } + }; + ws.onerror = (error: Event) => { + console.error("WebSocket error:", error); + if (config.onError) { + try { + config.onError(error); + } catch (err) { + console.error("Error in onError callback:", err); + } + } + }; + ws.onclose = () => { + console.log("WebSocket disconnected"); + + if (config.onClose) { + try { + config.onClose(); + } catch (error) { + console.error("Error in onClose callback:", error); + } + } + // Auto-reconnect if enabled + if (config.enableReconnect !== false) { + const delay = config.reconnectDelay || 5000; + console.log(`Reconnecting in ${delay / 1000}s...`); + reconnectTimeout = setTimeout(() => { + createWebSocket(config); + }, delay); + } + }; + activeWebSocket = ws; + return ws; +} +function disconnectWebSocket(): void { + if (reconnectTimeout) { + clearTimeout(reconnectTimeout); + reconnectTimeout = null; + } + if (activeWebSocket) { + activeWebSocket.close(); + activeWebSocket = null; + } +} +export { createWebSocket, disconnectWebSocket };