Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 92 additions & 32 deletions frontend/src/hooks/useCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,52 +92,112 @@ export function useCart(sessionId: string, recentCartStorageKey?: string) {

useEffect(() => {
let isActive = true;
let interval: ReturnType<typeof setInterval> | null = null;
const initialFetch = window.setTimeout(() => void fetchCart(), 0);
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
let pingInterval: ReturnType<typeof setInterval> | 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]);

Expand Down