diff --git a/frontend/src/main.test.ts b/frontend/src/main.test.ts index 884be11..edd9376 100644 --- a/frontend/src/main.test.ts +++ b/frontend/src/main.test.ts @@ -253,6 +253,25 @@ describe("WebSocket reconnect", () => { expect(websocketInstances).toHaveLength(2); expect(websocketInstances[1].url).toContain("room=lobby"); }); + + it("does not reconnect from a socket that was intentionally replaced", async () => { + vi.useFakeTimers(); + + window.switchRoom("lobby"); + expect(websocketInstances).toHaveLength(1); + const replaced = websocketInstances[0]; + + // A room switch intentionally replaces the socket; the old socket must not + // schedule its own reconnect, otherwise history is re-pulled on a loop. + window.switchRoom("main"); + expect(websocketInstances).toHaveLength(2); + expect(replaced.onclose).toBeNull(); + + replaced.onclose?.(); + await vi.advanceTimersByTimeAsync(3000); + + expect(websocketInstances).toHaveLength(2); + }); }); describe("URL resolution", () => { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 66a5823..960dcf1 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -79,19 +79,28 @@ export function resolveManifestUrl() { } function connectWebSocket(room: string) { - if (ws) ws.close(); + // Detach the old socket's close handler before closing it. Otherwise an + // intentional reconnect (login/logout/room switch/session refresh) would + // have the replaced socket schedule yet another reconnect, and each new + // connection re-pulls the room's history — filling the panel with old + // messages on a loop. + if (ws) { + ws.onclose = null; + ws.close(); + } const socketUrl = new URL(resolveWebSocketBaseUrl()); socketUrl.searchParams.set("username", username); socketUrl.searchParams.set("room", room); - ws = new WebSocket(socketUrl); + const socket = new WebSocket(socketUrl); + ws = socket; - ws.onopen = () => { + socket.onopen = () => { console.log(`Connected to chat room: ${room}`); addMessage("system", "Connected to " + room); }; - ws.onmessage = (event) => { + socket.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.type === "typing") { showTyping(msg.username); @@ -119,7 +128,10 @@ function connectWebSocket(room: string) { addMessage(msg.type || "public", msg.text, msg.username, msg.timestamp); }; - ws.onclose = () => { + socket.onclose = () => { + // Ignore close events from sockets that were intentionally replaced. + if (ws !== socket) return; + ws = null; console.log("Chat disconnected. Reconnecting in 3s..."); setTimeout(() => connectWebSocket(room), 3000); };