From 18f30b598dea406c4032746ad85511a7fd963553 Mon Sep 17 00:00:00 2001 From: Simo Date: Fri, 17 Apr 2026 14:02:26 +0300 Subject: [PATCH] wip Signed-off-by: Simo --- .../wave/MyStreamContext.resume.test.tsx | 4 - .../websocket/WebSocketProvider.test.tsx | 56 +--- contexts/wave/MyStreamContext.tsx | 68 +--- hooks/useWaveIsTyping.ts | 11 +- hooks/useWaveWebSocket.ts | 3 +- services/websocket/WebSocketProvider.tsx | 207 +++++------- services/websocket/useWebSocketHealth.ts | 294 ++++++------------ services/websocket/webSocketDebug.ts | 23 -- 8 files changed, 185 insertions(+), 481 deletions(-) delete mode 100644 services/websocket/webSocketDebug.ts diff --git a/__tests__/contexts/wave/MyStreamContext.resume.test.tsx b/__tests__/contexts/wave/MyStreamContext.resume.test.tsx index 2d0f69ddb2..527344b07e 100644 --- a/__tests__/contexts/wave/MyStreamContext.resume.test.tsx +++ b/__tests__/contexts/wave/MyStreamContext.resume.test.tsx @@ -32,10 +32,6 @@ jest.mock("@/services/websocket/useWebSocketMessage", () => ({ useWebsocketStatus: jest.fn(() => "disconnected"), })); -jest.mock("@/services/websocket/webSocketDebug", () => ({ - logWebSocketDebug: jest.fn(), -})); - jest.mock("@/contexts/wave/hooks/useActiveWaveManager", () => ({ useActiveWaveManager: jest.fn(() => ({ activeWaveId: "wave-1", diff --git a/__tests__/services/websocket/WebSocketProvider.test.tsx b/__tests__/services/websocket/WebSocketProvider.test.tsx index 747822b9e8..430a4ebc0b 100644 --- a/__tests__/services/websocket/WebSocketProvider.test.tsx +++ b/__tests__/services/websocket/WebSocketProvider.test.tsx @@ -320,9 +320,6 @@ describe("WebSocketProvider", () => { }); it("handles malformed messages gracefully", () => { - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); const wrapper = createWrapper({ url: "ws://test" }); const { result } = renderHook(() => React.useContext(WebSocketContext)!, { wrapper, @@ -346,18 +343,10 @@ describe("WebSocketProvider", () => { } }); - expect(consoleSpy).toHaveBeenCalledWith( - "Failed to parse WebSocket message:", - expect.any(SyntaxError) - ); - - consoleSpy.mockRestore(); + expect(result.current.status).toBe(WebSocketStatus.CONNECTED); }); it("handles subscriber callback errors gracefully", () => { - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); const wrapper = createWrapper({ url: "ws://test" }); const { result } = renderHook(() => React.useContext(WebSocketContext)!, { wrapper, @@ -386,13 +375,7 @@ describe("WebSocketProvider", () => { ws.triggerMessage({ type: WsMessageType.DROP_UPDATE, data: { id: 1 } }); }); - expect(consoleSpy).toHaveBeenCalledWith( - "Error in subscriber callback:", - expect.any(Error) - ); expect(faultyCallback).toHaveBeenCalledWith({ id: 1 }); - - consoleSpy.mockRestore(); }); }); @@ -460,9 +443,6 @@ describe("WebSocketProvider", () => { }); it("stops reconnecting after max attempts", () => { - const consoleSpy = jest - .spyOn(console, "warn") - .mockImplementation(() => {}); mockGetAuthJwt.mockReturnValue("fresh-token"); const wrapper = createWrapper({ @@ -522,11 +502,6 @@ describe("WebSocketProvider", () => { }); expect(global.WebSocket).toHaveBeenCalledTimes(3); - expect(consoleSpy).toHaveBeenCalledWith( - "WebSocket reconnect failed after 2 attempts" - ); - - consoleSpy.mockRestore(); }); it("does not reconnect when no fresh token is available", () => { @@ -616,9 +591,6 @@ describe("WebSocketProvider", () => { describe("Error Handling - Security & Edge Cases", () => { it("handles WebSocket constructor errors", () => { jest.useFakeTimers(); - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); // Mock WebSocket constructor to throw (global as any).WebSocket = jest.fn(() => { @@ -635,10 +607,6 @@ describe("WebSocketProvider", () => { }); expect(result.current.status).toBe(WebSocketStatus.DISCONNECTED); - expect(consoleSpy).toHaveBeenCalledWith( - "Failed to connect to WebSocket:", - expect.any(Error) - ); // Should attempt reconnect even for constructor errors act(() => { @@ -647,15 +615,10 @@ describe("WebSocketProvider", () => { expect(global.WebSocket).toHaveBeenCalledTimes(2); - consoleSpy.mockRestore(); jest.useRealTimers(); }); it("handles WebSocket error events", () => { - const consoleSpy = jest - .spyOn(console, "error") - .mockImplementation(() => {}); - const wrapper = createWrapper({ url: "ws://test" }); const { result } = renderHook(() => React.useContext(WebSocketContext)!, { wrapper, @@ -672,12 +635,7 @@ describe("WebSocketProvider", () => { ws.triggerError(); }); - expect(consoleSpy).toHaveBeenCalledWith( - "WebSocket error:", - expect.any(Event) - ); - - consoleSpy.mockRestore(); + expect(result.current.status).toBe(WebSocketStatus.CONNECTING); }); it("closes existing connection before creating new one and ignores stale close events", () => { @@ -1063,9 +1021,6 @@ describe("WebSocketProvider", () => { it("respects max reconnect attempts configuration", () => { jest.useFakeTimers(); - const consoleSpy = jest - .spyOn(console, "warn") - .mockImplementation(() => {}); mockGetAuthJwt.mockReturnValue("fresh-token"); const maxAttempts = 3; @@ -1106,24 +1061,19 @@ describe("WebSocketProvider", () => { }); } - // One final failure to trigger the max attempts warning + // One final failure after max attempts should not schedule another reconnect. const lastWs = (global.WebSocket as jest.MockedFunction) .mock.results[maxAttempts]?.value as MockWebSocket; act(() => { lastWs.triggerClose(1006); }); - // Advance time to trigger the check for exceeding max attempts act(() => { jest.advanceTimersByTime(1000); }); expect(global.WebSocket).toHaveBeenCalledTimes(maxAttempts + 1); // Initial + maxAttempts reconnects - expect(consoleSpy).toHaveBeenCalledWith( - `WebSocket reconnect failed after ${maxAttempts} attempts` - ); - consoleSpy.mockRestore(); jest.useRealTimers(); }); }); diff --git a/contexts/wave/MyStreamContext.tsx b/contexts/wave/MyStreamContext.tsx index fcd4c6cb1e..a290f86542 100644 --- a/contexts/wave/MyStreamContext.tsx +++ b/contexts/wave/MyStreamContext.tsx @@ -8,7 +8,6 @@ import useCapacitor from "@/hooks/useCapacitor"; import useDmWavesList from "@/hooks/useDmWavesList"; import useWavesList from "@/hooks/useWavesList"; import { WebSocketStatus } from "@/services/websocket/WebSocketTypes"; -import { logWebSocketDebug } from "@/services/websocket/webSocketDebug"; import { useWebsocketStatus } from "@/services/websocket/useWebSocketMessage"; import type { ReactNode } from "react"; import React, { @@ -97,12 +96,6 @@ interface MyStreamProviderProps { const BROWSER_RESUME_SYNC_COOLDOWN_MS = 1000; -type StreamSyncSource = - | "browser-resume" - | "websocket-connected" - | "capacitor-resume"; -type BrowserResumeSource = "visibilitychange" | "focus" | "online"; - // Create the context const MyStreamContext = createContext(null); @@ -193,30 +186,16 @@ export const MyStreamProvider: React.FC = ({ [registerWave, setActiveWave] ); - const syncActiveWaveAndRefetch = useEffectEvent( - (source: StreamSyncSource) => { - logWebSocketDebug("Running stream sync", { - source, - hasActiveWave: Boolean(activeWaveId), - }); - - if (activeWaveId) { - registerWave(activeWaveId, true); - } - refetchAllMainWaves(); - refetchAllDmWaves(); + const syncActiveWaveAndRefetch = useEffectEvent(() => { + if (activeWaveId) { + registerWave(activeWaveId, true); } - ); + refetchAllMainWaves(); + refetchAllDmWaves(); + }); - const runBrowserResumeSync = useEffectEvent((source: BrowserResumeSource) => { + const runBrowserResumeSync = useEffectEvent(() => { if (document.visibilityState !== "visible") { - logWebSocketDebug( - "Browser resume sync skipped because document is hidden", - { - source, - visibilityState: document.visibilityState, - } - ); return; } @@ -224,45 +203,24 @@ export const MyStreamProvider: React.FC = ({ const sinceLastBrowserResumeSyncMs = now - lastBrowserResumeSyncAtRef.current; if (sinceLastBrowserResumeSyncMs < BROWSER_RESUME_SYNC_COOLDOWN_MS) { - logWebSocketDebug("Browser resume sync suppressed by cooldown", { - source, - sinceLastBrowserResumeSyncMs, - cooldownMs: BROWSER_RESUME_SYNC_COOLDOWN_MS, - }); return; } lastBrowserResumeSyncAtRef.current = now; - logWebSocketDebug("Browser resume sync triggered", { - source, - hasActiveWave: Boolean(activeWaveId), - }); - syncActiveWaveAndRefetch("browser-resume"); + syncActiveWaveAndRefetch(); }); const handleConnectedWebSocket = useEffectEvent(() => { - logWebSocketDebug("WebSocket connected; triggering stream sync", { - hasActiveWave: Boolean(activeWaveId), - isCapacitor, - }); - - syncActiveWaveAndRefetch("websocket-connected"); + syncActiveWaveAndRefetch(); if (isCapacitor) { - logWebSocketDebug( - "Resetting new drop counts after websocket reconnect on capacitor" - ); resetAllMainWavesNewDropsCount(); resetAllDmWavesNewDropsCount(); } }); const handleCapacitorResume = useEffectEvent(() => { - logWebSocketDebug("Capacitor app resumed; triggering stream sync", { - hasActiveWave: Boolean(activeWaveId), - }); - - syncActiveWaveAndRefetch("capacitor-resume"); + syncActiveWaveAndRefetch(); resetAllMainWavesNewDropsCount(); resetAllDmWavesNewDropsCount(); }); @@ -299,15 +257,15 @@ export const MyStreamProvider: React.FC = ({ return; } - runBrowserResumeSync("visibilitychange"); + runBrowserResumeSync(); }; const handleFocus = () => { - runBrowserResumeSync("focus"); + runBrowserResumeSync(); }; const handleOnline = () => { - runBrowserResumeSync("online"); + runBrowserResumeSync(); }; document.addEventListener("visibilitychange", handleVisibilityChange); diff --git a/hooks/useWaveIsTyping.ts b/hooks/useWaveIsTyping.ts index 37eb2bf435..b8be1b52e6 100644 --- a/hooks/useWaveIsTyping.ts +++ b/hooks/useWaveIsTyping.ts @@ -2,12 +2,8 @@ import { useEffect, useRef, useState } from "react"; import { useWaveWebSocket } from "./useWaveWebSocket"; -import type { - WsDropUpdateMessage, - WsTypingMessage} from "@/helpers/Types"; -import { - WsMessageType -} from "@/helpers/Types"; +import type { WsDropUpdateMessage, WsTypingMessage } from "@/helpers/Types"; +import { WsMessageType } from "@/helpers/Types"; import type { ApiProfileMin } from "@/generated/models/ApiProfileMin"; /* ------------------------------------------------------------------ */ /* Types */ @@ -85,8 +81,7 @@ export function useWaveIsTyping( let msg: WsTypingMessage | WsDropUpdateMessage; try { msg = JSON.parse(event.data); - } catch (err) { - console.error("Bad WebSocket JSON", err); + } catch { return; } if (msg.type === WsMessageType.DROP_UPDATE) { diff --git a/hooks/useWaveWebSocket.ts b/hooks/useWaveWebSocket.ts index 437e690f96..a0747aced9 100644 --- a/hooks/useWaveWebSocket.ts +++ b/hooks/useWaveWebSocket.ts @@ -78,8 +78,7 @@ export function useWaveWebSocket(waveId: string): UseWaveWebSocketResult { } }; - ws.onerror = (error: Event) => { - console.error("WebSocket error:", error); + ws.onerror = () => { ws.close(); }; } diff --git a/services/websocket/WebSocketProvider.tsx b/services/websocket/WebSocketProvider.tsx index 791afec149..1e46f67876 100644 --- a/services/websocket/WebSocketProvider.tsx +++ b/services/websocket/WebSocketProvider.tsx @@ -12,10 +12,8 @@ import type { WebSocketMessage, } from "./WebSocketTypes"; import { WebSocketStatus } from "./WebSocketTypes"; -import type { WsMessageType } from "@/helpers/Types"; import { asNonEmptyString } from "@/lib/text/nonEmptyString"; import { getAuthJwt } from "../auth/auth.utils"; -import { logWebSocketDebug } from "./webSocketDebug"; // Default values for reconnection const DEFAULT_RECONNECT_DELAY = 2000; // Start with 2 seconds @@ -106,33 +104,35 @@ export function WebSocketProvider({ * Parse and route incoming WebSocket messages */ const handleMessage = useCallback((event: MessageEvent) => { + if (typeof event.data !== "string") { + return; + } + + let parsed: unknown; try { - if (typeof event.data !== "string") { - return; - } + parsed = JSON.parse(event.data); + } catch { + return; + } - // Parse the message - const parsed: unknown = JSON.parse(event.data); - const message = normalizeIncomingMessage(parsed); - if (!message) { - return; - } + const message = normalizeIncomingMessage(parsed); + if (!message) { + return; + } - // Get subscribers for this message type - const subscribers = subscribersRef.current.get(message.type); + // Get subscribers for this message type + const subscribers = subscribersRef.current.get(message.type); - // If there are subscribers, notify them with the message data - if (subscribers) { - subscribers.forEach((callback) => { - try { - callback(message.data); - } catch (error) { - console.error("Error in subscriber callback:", error); - } - }); + if (!subscribers) { + return; + } + + for (const subscriber of subscribers) { + try { + subscriber(message.data); + } catch { + // Keep one subscriber failure from blocking the remaining handlers. } - } catch (error) { - console.error("Failed to parse WebSocket message:", error); } }, []); @@ -149,74 +149,48 @@ export function WebSocketProvider({ /** * Attempt reconnection with exponential backoff */ - const attemptReconnect = useCallback(() => { - // Clear any existing timer - clearReconnectTimer(); + const attemptReconnect = useCallback( + (connectSocket: (token?: string) => void) => { + // Clear any existing timer + clearReconnectTimer(); - // Check if we've exceeded max attempts - const maxAttempts = - config.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS; - if (reconnectAttemptsRef.current >= maxAttempts) { - logWebSocketDebug("Reconnect attempts exhausted", { - attempts: reconnectAttemptsRef.current, - maxAttempts, - }); - console.warn( - `WebSocket reconnect failed after ${reconnectAttemptsRef.current} attempts` - ); - return; - } + // Check if we've exceeded max attempts + const maxAttempts = + config.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS; + if (reconnectAttemptsRef.current >= maxAttempts) { + return; + } - // Calculate delay based on attempts - const baseDelay = config.reconnectDelay ?? DEFAULT_RECONNECT_DELAY; - const delay = calculateReconnectDelay( - reconnectAttemptsRef.current, - baseDelay, - MAX_RECONNECT_DELAY - ); - const nextAttempt = reconnectAttemptsRef.current + 1; - - logWebSocketDebug("Scheduling reconnect attempt", { - attempt: nextAttempt, - delayMs: delay, - maxAttempts, - hasToken: Boolean(reconnectTokenRef.current), - }); - - // Schedule reconnection - reconnectTimerRef.current = setTimeout(() => { - reconnectAttemptsRef.current += 1; - logWebSocketDebug("Running scheduled reconnect attempt", { - attempt: reconnectAttemptsRef.current, - hasToken: Boolean(reconnectTokenRef.current), - }); - // Attempt reconnection with the stored token - connect(reconnectTokenRef.current); - }, delay); - }, [config.maxReconnectAttempts, config.reconnectDelay]); + // Calculate delay based on attempts + const baseDelay = config.reconnectDelay ?? DEFAULT_RECONNECT_DELAY; + const delay = calculateReconnectDelay( + reconnectAttemptsRef.current, + baseDelay, + MAX_RECONNECT_DELAY + ); + // Schedule reconnection + reconnectTimerRef.current = setTimeout(() => { + reconnectAttemptsRef.current += 1; + // Attempt reconnection with the stored token + connectSocket(reconnectTokenRef.current); + }, delay); + }, + [clearReconnectTimer, config.maxReconnectAttempts, config.reconnectDelay] + ); /** * Connect to WebSocket server */ const connect = useCallback( - (token?: string) => { + function connectSocket(token?: string) { // Store token for potential reconnection reconnectTokenRef.current = token; // Reset manual disconnect flag isManualDisconnectRef.current = false; - logWebSocketDebug("connect() requested", { - hasToken: Boolean(token), - reconnectAttempt: reconnectAttemptsRef.current, - replacingExistingSocket: Boolean(wsRef.current), - }); - // Close existing connection if any if (wsRef.current) { - logWebSocketDebug("Closing existing websocket before replacement", { - readyState: wsRef.current.readyState, - }); wsRef.current.close(); wsRef.current = null; } @@ -234,11 +208,6 @@ export function WebSocketProvider({ } try { - logWebSocketDebug("Opening websocket connection", { - hasToken: Boolean(token), - reconnectAttempt: reconnectAttemptsRef.current, - }); - // Create new WebSocket connection const ws = new WebSocket(url); @@ -250,10 +219,6 @@ export function WebSocketProvider({ setStatus(WebSocketStatus.CONNECTED); - logWebSocketDebug("WebSocket opened", { - reconnectAttempt: reconnectAttemptsRef.current, - }); - // Reset reconnect attempts on successful connection reconnectAttemptsRef.current = 0; }; @@ -281,25 +246,13 @@ export function WebSocketProvider({ ? (getAuthJwt() ?? reconnectTokenRef.current) : undefined; - logWebSocketDebug("WebSocket closed", { - code: event.code, - reason: event.reason || null, - wasClean: event.wasClean, - manualDisconnect: isManualDisconnectRef.current, - willReconnect: Boolean(freshToken), - }); - // Only attempt reconnect for unexpected closure (not code 1000) // and if this wasn't a manual disconnect if (shouldReconnect) { // Get fresh token before reconnecting if (freshToken) { reconnectTokenRef.current = freshToken; // Update stored token - attemptReconnect(); - } else { - logWebSocketDebug( - "Skipping reconnect after close because auth token is missing" - ); + attemptReconnect(connectSocket); } } else { // Reset reconnect attempts for intentional disconnects @@ -307,31 +260,13 @@ export function WebSocketProvider({ } }; - ws.onerror = (error) => { - if (wsRef.current !== ws) { - return; - } - - logWebSocketDebug("WebSocket error event received", { - readyState: ws.readyState, - reconnectAttempt: reconnectAttemptsRef.current, - }); - console.error("WebSocket error:", error); - // State will be updated by onclose handler - }; - // Store the WebSocket reference wsRef.current = ws; - } catch (error) { - logWebSocketDebug("WebSocket construction failed", { - hasToken: Boolean(token), - reconnectAttempt: reconnectAttemptsRef.current, - }); - console.error("Failed to connect to WebSocket:", error); + } catch { setStatus(WebSocketStatus.DISCONNECTED); // Schedule reconnect even for connection errors - attemptReconnect(); + attemptReconnect(connectSocket); } }, [config.url, handleMessage, clearReconnectTimer, attemptReconnect] @@ -344,10 +279,6 @@ export function WebSocketProvider({ // Set flag to indicate this is intentional isManualDisconnectRef.current = true; - logWebSocketDebug("disconnect() requested", { - hasSocket: Boolean(wsRef.current), - }); - // Clear any pending reconnect clearReconnectTimer(); @@ -366,19 +297,19 @@ export function WebSocketProvider({ * Subscribe to a specific message type */ const subscribe = useCallback( - (messageType: string, callback: MessageCallback) => { + (messageType: string, subscriber: MessageCallback) => { // Get or create subscriber set for this message type if (!subscribersRef.current.has(messageType)) { subscribersRef.current.set(messageType, new Set()); } - // Add the callback to subscribers + // Add the subscriber handler const subscribers = subscribersRef.current.get(messageType)!; - subscribers.add(callback as MessageCallback); + subscribers.add(subscriber as MessageCallback); // Return unsubscribe function return () => { - subscribers.delete(callback as MessageCallback); + subscribers.delete(subscriber as MessageCallback); // Clean up empty subscriber sets if (subscribers.size === 0) { @@ -394,14 +325,20 @@ export function WebSocketProvider({ * @param messageType - Type identifier for the message * @param data - Payload for the message */ - const send = useCallback((messageType: WsMessageType, data: T) => { - const ws = wsRef.current; - if (!ws || ws.readyState !== WebSocket.OPEN) { - return; - } - const message: WebSocketMessage = { type: messageType, ...data }; - ws.send(JSON.stringify(message)); - }, []); + const send: WebSocketContextValue["send"] = useCallback( + (messageType, data) => { + const ws = wsRef.current; + if (ws?.readyState !== WebSocket.OPEN) { + return; + } + const message: WebSocketMessage = { + type: messageType, + ...data, + }; + ws.send(JSON.stringify(message)); + }, + [] + ); // Clean up on unmount useEffect(() => { diff --git a/services/websocket/useWebSocketHealth.ts b/services/websocket/useWebSocketHealth.ts index 95addfbb5d..1d933ff4a0 100644 --- a/services/websocket/useWebSocketHealth.ts +++ b/services/websocket/useWebSocketHealth.ts @@ -4,7 +4,6 @@ import { useCallback, useEffect, useRef } from "react"; import { getAuthJwt, WALLET_AUTH_COOKIE } from "../auth/auth.utils"; import { useWebSocket } from "./useWebSocket"; import { WebSocketStatus } from "./WebSocketTypes"; -import { logWebSocketDebug } from "./webSocketDebug"; const AUTH_BROADCAST_CHANNEL = "auth-token-updates"; const AUTH_BROADCAST_MESSAGE = "auth-token-changed"; @@ -34,13 +33,6 @@ interface CookieStoreWithEvents { } type HealthCheckAction = "none" | "connect" | "disconnect"; -type HealthCheckSource = - | "status-effect" - | "resume" - | "cookie-change" - | "broadcast-channel" - | "interval"; -type ResumeEventSource = "visibilitychange" | "focus"; const isAuthCookieChange = (event: CookieChangeEventLike): boolean => { const matchChanged = event.changed?.some( @@ -74,192 +66,104 @@ export function useWebSocketHealth() { // Keep ref updated with current WebSocket state webSocketStateRef.current = webSocketState; - const performHealthCheckForSource = useCallback( - ( - source: HealthCheckSource - ): { - action: HealthCheckAction; - token: string | null; - reason: string | null; - } => { - const currentToken = getAuthJwt(); - const previousToken = lastTokenRef.current; - const tokenChanged = currentToken !== previousToken; - lastTokenRef.current = currentToken; - - const { - status: currentStatus, - connect: currentConnect, - disconnect: currentDisconnect, - } = webSocketStateRef.current; - - let action: HealthCheckAction = "none"; - let reason: string | null = null; - - if (!currentToken && currentStatus !== WebSocketStatus.DISCONNECTED) { - currentDisconnect(); - action = "disconnect"; - reason = "missing-token"; - } else if ( - currentToken && - currentStatus === WebSocketStatus.DISCONNECTED - ) { - currentConnect(currentToken); - action = "connect"; - reason = "connect-while-disconnected"; - } else if ( - currentToken && - currentStatus !== WebSocketStatus.DISCONNECTED && - currentToken !== previousToken - ) { - currentConnect(currentToken); - action = "connect"; - reason = "token-changed"; - } + const performHealthCheck = useCallback((): { + action: HealthCheckAction; + token: string | null; + reason: string | null; + } => { + const currentToken = getAuthJwt(); + const previousToken = lastTokenRef.current; + const tokenChanged = currentToken !== previousToken; + lastTokenRef.current = currentToken; + + const { + status: currentStatus, + connect: currentConnect, + disconnect: currentDisconnect, + } = webSocketStateRef.current; + + let action: HealthCheckAction = "none"; + let reason: string | null = null; + + if (!currentToken && currentStatus !== WebSocketStatus.DISCONNECTED) { + currentDisconnect(); + action = "disconnect"; + reason = "missing-token"; + } else if (currentToken && currentStatus === WebSocketStatus.DISCONNECTED) { + currentConnect(currentToken); + action = "connect"; + reason = "connect-while-disconnected"; + } else if ( + currentToken && + currentStatus !== WebSocketStatus.DISCONNECTED && + currentToken !== previousToken + ) { + currentConnect(currentToken); + action = "connect"; + reason = "token-changed"; + } - if ( - source === "resume" || - source === "status-effect" || - action !== "none" - ) { - logWebSocketDebug("Health check completed", { - source, - action, - reason, - status: currentStatus, - hasToken: Boolean(currentToken), - tokenChanged, - }); - } + if (tokenChanged) { + broadcastChannelRef.current?.postMessage({ + type: AUTH_BROADCAST_MESSAGE, + }); + } - if (tokenChanged) { - broadcastChannelRef.current?.postMessage({ - type: AUTH_BROADCAST_MESSAGE, - }); - } + return { + action, + token: currentToken, + reason, + }; + }, []); - return { - action, - token: currentToken, - reason, - }; - }, - [] - ); + const performResumeHealthCheck = useCallback(() => { + if ( + typeof document === "undefined" || + document.visibilityState !== "visible" + ) { + return; + } - const performResumeHealthCheck = useCallback( - (source: ResumeEventSource) => { - if ( - typeof document === "undefined" || - document.visibilityState !== "visible" - ) { - logWebSocketDebug( - "Resume health check skipped because document is hidden", - { - source, - visibilityState: - typeof document === "undefined" - ? "undefined" - : document.visibilityState, - } - ); - return; - } + const now = Date.now(); + const hiddenAt = hiddenAtRef.current; + const hiddenDurationMs = hiddenAt === null ? null : now - hiddenAt; + // Clear the hidden marker for this resume attempt even if we dedupe it. + hiddenAtRef.current = null; - const now = Date.now(); - const hiddenAt = hiddenAtRef.current; - const hiddenDurationMs = hiddenAt === null ? null : now - hiddenAt; - // Clear the hidden marker for this resume attempt even if we dedupe it. - hiddenAtRef.current = null; - - const sinceLastResumeCheckMs = now - lastResumeCheckAtRef.current; - if (sinceLastResumeCheckMs < RESUME_EVENT_DEDUPE_WINDOW_MS) { - logWebSocketDebug("Resume health check deduped", { - source, - sinceLastResumeCheckMs, - hiddenDurationMs, - }); - return; - } - lastResumeCheckAtRef.current = now; + const sinceLastResumeCheckMs = now - lastResumeCheckAtRef.current; + if (sinceLastResumeCheckMs < RESUME_EVENT_DEDUPE_WINDOW_MS) { + return; + } + lastResumeCheckAtRef.current = now; - logWebSocketDebug("Running resume health check", { - source, - hiddenDurationMs, - status: webSocketStateRef.current.status, - }); + const { action, token: currentToken } = performHealthCheck(); - const { - action, - token: currentToken, - reason: healthCheckReason, - } = performHealthCheckForSource("resume"); - - // Avoid a second reconnect when the health check already replaced the socket. - if (action === "connect") { - logWebSocketDebug("Resume reconnect already handled by health check", { - source, - hiddenDurationMs, - reason: healthCheckReason, - }); - return; - } - - if (!currentToken || hiddenDurationMs === null) { - logWebSocketDebug("Resume reconnect skipped", { - source, - hiddenDurationMs, - reason: currentToken ? "missing-hidden-marker" : "missing-token", - }); - return; - } + // Avoid a second reconnect when the health check already replaced the socket. + if (action === "connect") { + return; + } - if (hiddenDurationMs < RESUME_RECONNECT_HIDDEN_DURATION_MS) { - logWebSocketDebug( - "Resume reconnect skipped because hidden duration is below threshold", - { - source, - hiddenDurationMs, - thresholdMs: RESUME_RECONNECT_HIDDEN_DURATION_MS, - } - ); - return; - } + if (!currentToken || hiddenDurationMs === null) { + return; + } - const { status: currentStatus, connect: currentConnect } = - webSocketStateRef.current; - if ( - currentStatus === WebSocketStatus.CONNECTED || - currentStatus === WebSocketStatus.CONNECTING - ) { - logWebSocketDebug( - "Forcing websocket reconnect after long hidden interval", - { - source, - hiddenDurationMs, - status: currentStatus, - thresholdMs: RESUME_RECONNECT_HIDDEN_DURATION_MS, - } - ); - currentConnect(currentToken); - return; - } + if (hiddenDurationMs < RESUME_RECONNECT_HIDDEN_DURATION_MS) { + return; + } - logWebSocketDebug( - "Resume reconnect skipped because socket is disconnected", - { - source, - hiddenDurationMs, - status: currentStatus, - } - ); - }, - [performHealthCheckForSource] - ); + const { status: currentStatus, connect: currentConnect } = + webSocketStateRef.current; + if ( + currentStatus === WebSocketStatus.CONNECTED || + currentStatus === WebSocketStatus.CONNECTING + ) { + currentConnect(currentToken); + } + }, [performHealthCheck]); useEffect(() => { - performHealthCheckForSource("status-effect"); - }, [performHealthCheckForSource, webSocketState.status]); + performHealthCheck(); + }, [performHealthCheck, webSocketState.status]); useEffect(() => { if (typeof document === "undefined") { @@ -269,26 +173,14 @@ export function useWebSocketHealth() { const handleVisibilityChange = () => { if (document.visibilityState === "hidden") { hiddenAtRef.current = Date.now(); - logWebSocketDebug("Document became hidden", { - hiddenAt: hiddenAtRef.current, - }); return; } - logWebSocketDebug("Document became visible", { - hiddenDurationMs: - hiddenAtRef.current === null - ? null - : Date.now() - hiddenAtRef.current, - }); - performResumeHealthCheck("visibilitychange"); + performResumeHealthCheck(); }; const handleFocus = () => { - logWebSocketDebug("Window focus event received", { - visibilityState: document.visibilityState, - }); - performResumeHealthCheck("focus"); + performResumeHealthCheck(); }; document.addEventListener("visibilitychange", handleVisibilityChange); @@ -319,7 +211,7 @@ export function useWebSocketHealth() { const handleCookieChange = (event: CookieChangeEventLike) => { if (isAuthCookieChange(event)) { - performHealthCheckForSource("cookie-change"); + performHealthCheck(); } }; @@ -358,7 +250,7 @@ export function useWebSocketHealth() { (event.data as { type?: string | undefined })?.type === AUTH_BROADCAST_MESSAGE ) { - performHealthCheckForSource("broadcast-channel"); + performHealthCheck(); } }; channel.addEventListener("message", handleMessage as EventListener); @@ -388,12 +280,12 @@ export function useWebSocketHealth() { removeCookieListener?.(); closeBroadcastChannel?.(); }; - }, [performHealthCheckForSource]); + }, [performHealthCheck]); useEffect(() => { const healthCheck = window.setInterval(() => { - performHealthCheckForSource("interval"); + performHealthCheck(); }, HEALTH_CHECK_INTERVAL_MS); return () => window.clearInterval(healthCheck); - }, [performHealthCheckForSource]); + }, [performHealthCheck]); } diff --git a/services/websocket/webSocketDebug.ts b/services/websocket/webSocketDebug.ts deleted file mode 100644 index 70c1627ffc..0000000000 --- a/services/websocket/webSocketDebug.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { publicEnv } from "@/config/env"; - -const WEBSOCKET_DEBUG_PREFIX = "[WebSocketResumeDebug]"; - -const isWebSocketDebugEnabled = (): boolean => { - return publicEnv.NODE_ENV === "development"; -}; - -export const logWebSocketDebug = ( - message: string, - context?: Record -): void => { - if (!isWebSocketDebugEnabled()) { - return; - } - - if (context) { - console.warn(`${WEBSOCKET_DEBUG_PREFIX} ${message}`, context); - return; - } - - console.warn(`${WEBSOCKET_DEBUG_PREFIX} ${message}`); -};