Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 0 additions & 4 deletions __tests__/contexts/wave/MyStreamContext.resume.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 3 additions & 53 deletions __tests__/services/websocket/WebSocketProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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();
});
});

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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(() => {
Expand All @@ -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,
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<typeof WebSocket>)
.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();
});
});
Expand Down
68 changes: 13 additions & 55 deletions contexts/wave/MyStreamContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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<MyStreamContextType | null>(null);

Expand Down Expand Up @@ -193,76 +186,41 @@ export const MyStreamProvider: React.FC<MyStreamProviderProps> = ({
[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;
}

const now = Date.now();
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();
});
Expand Down Expand Up @@ -299,15 +257,15 @@ export const MyStreamProvider: React.FC<MyStreamProviderProps> = ({
return;
}

runBrowserResumeSync("visibilitychange");
runBrowserResumeSync();
};

const handleFocus = () => {
runBrowserResumeSync("focus");
runBrowserResumeSync();
};

const handleOnline = () => {
runBrowserResumeSync("online");
runBrowserResumeSync();
};

document.addEventListener("visibilitychange", handleVisibilityChange);
Expand Down
11 changes: 3 additions & 8 deletions hooks/useWaveIsTyping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 1 addition & 2 deletions hooks/useWaveWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ export function useWaveWebSocket(waveId: string): UseWaveWebSocketResult {
}
};

ws.onerror = (error: Event) => {
console.error("WebSocket error:", error);
ws.onerror = () => {
ws.close();
};
}
Expand Down
Loading
Loading