From fad58aa03d061a3df4e45f73f8bf937b692e62db Mon Sep 17 00:00:00 2001 From: HimanM <67066047+HimanM@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:42:24 +0530 Subject: [PATCH] fix(cart): add websocket heartbeat pings, exponential reconnect, and eliminate 5s GET polling --- frontend/src/hooks/useCart.ts | 124 +++++++++++++++++++++++++--------- 1 file changed, 92 insertions(+), 32 deletions(-) diff --git a/frontend/src/hooks/useCart.ts b/frontend/src/hooks/useCart.ts index f76f6ac..15d3bef 100644 --- a/frontend/src/hooks/useCart.ts +++ b/frontend/src/hooks/useCart.ts @@ -92,52 +92,112 @@ export function useCart(sessionId: string, recentCartStorageKey?: string) { useEffect(() => { let isActive = true; - let interval: ReturnType | null = null; - const initialFetch = window.setTimeout(() => void fetchCart(), 0); + let reconnectTimeout: ReturnType | null = null; + let pingInterval: ReturnType | null = null; + let reconnectAttempts = 0; + + // Initial fetch on session load + void fetchCart(); const wsUrl = createWsUrl(sessionId); - if (!wsUrl) { - interval = setInterval(fetchCart, 5000); - } else { - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - ws.onopen = () => { - if (isActive && sessionIdRef.current === sessionId) setReconnectingSessionId(null); + // If WebSocket is disabled, revalidate on tab focus (no continuous 5s polling spam) + if (!wsUrl) { + const handleFocus = () => { + if (isActive) void fetchCart(); + }; + window.addEventListener("focus", handleFocus); + return () => { + isActive = false; + window.removeEventListener("focus", handleFocus); }; + } + + const connectWebSocket = () => { + if (!isActive || sessionIdRef.current !== sessionId) return; - ws.onmessage = (e) => { - if (!isActive || sessionIdRef.current !== sessionId) return; - try { - const data = JSON.parse(e.data); - if ((data.type === "cart_updated" || data.type === "cart_initialized") && data.cart) { - applyCart(data.cart, undefined, data.type === "cart_updated" ? "Cart updated." : undefined); + try { + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => { + if (!isActive || sessionIdRef.current !== sessionId) return; + reconnectAttempts = 0; + setReconnectingSessionId(null); + + // Setup 25-second heartbeat ping to keep socket alive through proxies/routers + if (pingInterval) clearInterval(pingInterval); + pingInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "ping" })); + } + }, 25000); + }; + + ws.onmessage = (e) => { + if (!isActive || sessionIdRef.current !== sessionId) return; + try { + const data = JSON.parse(e.data); + if ((data.type === "cart_updated" || data.type === "cart_initialized") && data.cart) { + applyCart(data.cart, undefined, data.type === "cart_updated" ? "Cart updated." : undefined); + } + } catch { + // ignore } - } catch { - // ignore - } - }; + }; + + const handleDisconnect = () => { + if (pingInterval) clearInterval(pingInterval); + if (!isActive || sessionIdRef.current !== sessionId) return; + + setReconnectingSessionId(sessionId); + + // Exponential backoff reconnect: 1.5s, 3s, 6s, max 10s + const delay = Math.min(10000, 1500 * Math.pow(1.5, reconnectAttempts)); + reconnectAttempts += 1; - ws.onerror = () => { - if (isActive) setReconnectingSessionId(sessionId); - if (!interval) { - interval = setInterval(fetchCart, 5000); + if (reconnectTimeout) clearTimeout(reconnectTimeout); + reconnectTimeout = setTimeout(() => { + if (isActive && sessionIdRef.current === sessionId) { + void fetchCart(); + connectWebSocket(); + } + }, delay); + }; + + ws.onerror = handleDisconnect; + ws.onclose = handleDisconnect; + } catch { + if (isActive && sessionIdRef.current === sessionId) { + if (reconnectTimeout) clearTimeout(reconnectTimeout); + reconnectTimeout = setTimeout(connectWebSocket, 3000); } - }; + } + }; + + connectWebSocket(); - ws.onclose = () => { - if (isActive) setReconnectingSessionId(sessionId); - if (!interval) { - interval = setInterval(fetchCart, 5000); + // Re-verify cart state when user tabs back into the window + const handleFocus = () => { + if (isActive && sessionIdRef.current === sessionId) { + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + connectWebSocket(); } + void fetchCart(); } - } + }; + window.addEventListener("focus", handleFocus); return () => { isActive = false; - window.clearTimeout(initialFetch); - if (interval) clearInterval(interval); - wsRef.current?.close(); + if (reconnectTimeout) clearTimeout(reconnectTimeout); + if (pingInterval) clearInterval(pingInterval); + window.removeEventListener("focus", handleFocus); + if (wsRef.current) { + wsRef.current.onclose = null; + wsRef.current.onerror = null; + wsRef.current.close(); + } }; }, [applyCart, sessionId, fetchCart]);