Skip to content

Commit 6d67b86

Browse files
committed
fix: add terminateSession() for explicit close code 4001, keep unmount graceful
Unmount uses a normal close() so the server grace period preserves session resumability on navigation/reload. A new terminateSession() method on the AblyCliTerminalHandle ref sends close(4001, "user-closed-panel") for immediate server cleanup when the user explicitly closes the panel.
1 parent 8adb318 commit 6d67b86

2 files changed

Lines changed: 93 additions & 1 deletion

File tree

packages/react-web-cli/src/AblyCliTerminal.test.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2557,3 +2557,76 @@ describe("AblyCliTerminal - Initial Command Execution", () => {
25572557
expect(hasTestCmd).toBe(true);
25582558
}, 15_000);
25592559
});
2560+
2561+
describe("AblyCliTerminal - Unmount cleanup", () => {
2562+
test("closes socket normally on unmount (no special code for resume support)", async () => {
2563+
mockClose.mockClear();
2564+
2565+
const { unmount } = render(
2566+
<AblyCliTerminal
2567+
websocketUrl="wss://test.ably.com"
2568+
ablyApiKey="test-key"
2569+
/>,
2570+
);
2571+
2572+
await act(async () => {
2573+
await new Promise((resolve) => setTimeout(resolve, 50));
2574+
});
2575+
2576+
expect(mockSocketInstance).toBeTruthy();
2577+
expect(mockSocketInstance.readyState).toBe(WebSocket.OPEN);
2578+
2579+
unmount();
2580+
2581+
// Should close without special code (allows grace period for resume)
2582+
expect(mockClose).toHaveBeenCalledWith();
2583+
});
2584+
2585+
test("does not call close if socket already closing", async () => {
2586+
mockClose.mockClear();
2587+
2588+
const { unmount } = render(
2589+
<AblyCliTerminal
2590+
websocketUrl="wss://test.ably.com"
2591+
ablyApiKey="test-key"
2592+
/>,
2593+
);
2594+
2595+
await act(async () => {
2596+
await new Promise((resolve) => setTimeout(resolve, 50));
2597+
});
2598+
2599+
mockSocketInstance.readyState = WebSocket.CLOSING;
2600+
2601+
unmount();
2602+
2603+
expect(mockClose).not.toHaveBeenCalled();
2604+
});
2605+
2606+
test("terminateSession() sends close code 4001 for immediate cleanup", async () => {
2607+
mockClose.mockClear();
2608+
const terminalRef = React.createRef<AblyCliTerminalHandle>();
2609+
2610+
render(
2611+
<AblyCliTerminal
2612+
ref={terminalRef}
2613+
websocketUrl="wss://test.ably.com"
2614+
ablyApiKey="test-key"
2615+
/>,
2616+
);
2617+
2618+
await act(async () => {
2619+
await new Promise((resolve) => setTimeout(resolve, 50));
2620+
});
2621+
2622+
expect(mockSocketInstance).toBeTruthy();
2623+
expect(mockSocketInstance.readyState).toBe(WebSocket.OPEN);
2624+
2625+
// Call terminateSession explicitly
2626+
act(() => {
2627+
terminalRef.current?.terminateSession();
2628+
});
2629+
2630+
expect(mockClose).toHaveBeenCalledWith(4001, "user-closed-panel");
2631+
});
2632+
});

packages/react-web-cli/src/AblyCliTerminal.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ export interface AblyCliTerminalHandle {
142142
setSplitPosition: (percent: number) => void;
143143
/** Read current split state. */
144144
getSplitState: () => { isSplit: boolean; splitPosition: number };
145+
/** Terminate the session immediately. Call this before unmounting when user explicitly closes the panel. */
146+
terminateSession: () => void;
145147
}
146148

147149
// Use shared debug logging
@@ -340,6 +342,23 @@ const AblyCliTerminalInner = (
340342
setSplitPosition(clamped);
341343
},
342344
getSplitState: () => ({ isSplit, splitPosition }),
345+
terminateSession: () => {
346+
debugLog(
347+
"[AblyCLITerminal] terminateSession called - closing with code 4001",
348+
);
349+
if (
350+
socketReference.current &&
351+
socketReference.current.readyState < WebSocket.CLOSING
352+
) {
353+
socketReference.current.close(4001, "user-closed-panel");
354+
}
355+
if (
356+
secondarySocketReference.current &&
357+
secondarySocketReference.current.readyState < WebSocket.CLOSING
358+
) {
359+
secondarySocketReference.current.close(4001, "user-closed-panel");
360+
}
361+
},
343362
}),
344363
[
345364
enableSplitScreen,
@@ -2382,7 +2401,7 @@ const AblyCliTerminalInner = (
23822401
socketReference.current &&
23832402
socketReference.current.readyState < WebSocket.CLOSING
23842403
) {
2385-
// close websocket
2404+
// Normal close (no 4001) so server grace period allows resume
23862405
debugLog("[AblyCLITerminal] Closing WebSocket on unmount.");
23872406
socketReference.current.close();
23882407
}

0 commit comments

Comments
 (0)