From 59b61d047ddb9db0e326100495e124dd0675f582 Mon Sep 17 00:00:00 2001 From: Denis Sellu Date: Fri, 9 Jan 2026 16:45:13 +0100 Subject: [PATCH] fix: send close code 4001 on unmount to signal intentional disconnect --- .../src/AblyCliTerminal.test.tsx | 45 +++++++++++++++++++ .../react-web-cli/src/AblyCliTerminal.tsx | 7 +-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/react-web-cli/src/AblyCliTerminal.test.tsx b/packages/react-web-cli/src/AblyCliTerminal.test.tsx index 90141195..48bd116c 100644 --- a/packages/react-web-cli/src/AblyCliTerminal.test.tsx +++ b/packages/react-web-cli/src/AblyCliTerminal.test.tsx @@ -2467,3 +2467,48 @@ describe("AblyCliTerminal - Initial Command Execution", () => { expect(hasTestCmd).toBe(true); }, 15_000); }); + +describe("AblyCliTerminal - Unmount cleanup", () => { + test("sends close code 4001 (user-closed-panel) on unmount", async () => { + mockClose.mockClear(); + + const { unmount } = render( + , + ); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + }); + + expect(mockSocketInstance).toBeTruthy(); + expect(mockSocketInstance.readyState).toBe(WebSocket.OPEN); + + unmount(); + + expect(mockClose).toHaveBeenCalledWith(4001, "user-closed-panel"); + }); + + test("does not call close if socket already closing", async () => { + mockClose.mockClear(); + + const { unmount } = render( + , + ); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + }); + + mockSocketInstance.readyState = WebSocket.CLOSING; + + unmount(); + + expect(mockClose).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/react-web-cli/src/AblyCliTerminal.tsx b/packages/react-web-cli/src/AblyCliTerminal.tsx index bf6c00cf..1ce5781c 100644 --- a/packages/react-web-cli/src/AblyCliTerminal.tsx +++ b/packages/react-web-cli/src/AblyCliTerminal.tsx @@ -2419,9 +2419,10 @@ const AblyCliTerminalInner = ( socketReference.current && socketReference.current.readyState < WebSocket.CLOSING ) { - // close websocket + // close websocket with code 4001 to signal intentional close + // This tells the server to cleanup immediately (no grace period) debugLog("[AblyCLITerminal] Closing WebSocket on unmount."); - socketReference.current.close(); + socketReference.current.close(4001, "user-closed-panel"); } grResetState(); // Ensure global state is clean clearConnectionTimeout(); // Clear any pending connection timeout @@ -3559,7 +3560,7 @@ const AblyCliTerminalInner = ( debugLog( "[AblyCLITerminal] Closing secondary WebSocket on terminal cleanup.", ); - secondarySocketReference.current.close(); + secondarySocketReference.current.close(4001, "user-closed-panel"); secondarySocketReference.current = null; }