-
Notifications
You must be signed in to change notification settings - Fork 0
[FTF-198] Send close code on react-web-cli panel unmount #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -142,6 +142,8 @@ export interface AblyCliTerminalHandle { | |||||||||||||
| setSplitPosition: (percent: number) => void; | ||||||||||||||
| /** Read current split state. */ | ||||||||||||||
| getSplitState: () => { isSplit: boolean; splitPosition: number }; | ||||||||||||||
| /** Terminate the session immediately. Call this before unmounting when user explicitly closes the panel. */ | ||||||||||||||
| terminateSession: () => void; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Use shared debug logging | ||||||||||||||
|
|
@@ -340,6 +342,23 @@ const AblyCliTerminalInner = ( | |||||||||||||
| setSplitPosition(clamped); | ||||||||||||||
| }, | ||||||||||||||
| getSplitState: () => ({ isSplit, splitPosition }), | ||||||||||||||
| terminateSession: () => { | ||||||||||||||
| debugLog( | ||||||||||||||
| "[AblyCLITerminal] terminateSession called - closing with code 4001", | ||||||||||||||
| ); | ||||||||||||||
| if ( | ||||||||||||||
| socketReference.current && | ||||||||||||||
| socketReference.current.readyState < WebSocket.CLOSING | ||||||||||||||
| ) { | ||||||||||||||
| socketReference.current.close(4001, "user-closed-primary"); | ||||||||||||||
| } | ||||||||||||||
| if ( | ||||||||||||||
| secondarySocketReference.current && | ||||||||||||||
| secondarySocketReference.current.readyState < WebSocket.CLOSING | ||||||||||||||
| ) { | ||||||||||||||
| secondarySocketReference.current.close(4001, "user-closed-secondary"); | ||||||||||||||
| } | ||||||||||||||
| }, | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. terminateSession leaves stale sessionId in session storageMedium Severity
|
||||||||||||||
| }), | ||||||||||||||
| [ | ||||||||||||||
| enableSplitScreen, | ||||||||||||||
|
|
@@ -2378,16 +2397,15 @@ const AblyCliTerminalInner = ( | |||||||||||||
| term.current.dispose(); | ||||||||||||||
| term.current = null; | ||||||||||||||
| } | ||||||||||||||
| grCancelReconnect(); | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unmount sets stale
|
||||||||||||||
| if ( | ||||||||||||||
| socketReference.current && | ||||||||||||||
| socketReference.current.readyState < WebSocket.CLOSING | ||||||||||||||
| ) { | ||||||||||||||
| // close websocket | ||||||||||||||
| debugLog("[AblyCLITerminal] Closing WebSocket on unmount."); | ||||||||||||||
|
||||||||||||||
| debugLog("[AblyCLITerminal] Closing WebSocket on unmount."); | |
| debugLog("[AblyCLITerminal] Closing WebSocket on unmount."); | |
| // Prevent auto-reconnect and handler side effects after unmount | |
| grCancelReconnect(); | |
| socketReference.current.onclose = null; | |
| socketReference.current.onerror = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai what do you think about above review comment, can you explain a bit more
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sacOO7 Great question! Let me break down Copilot's concern and the broader context here.
Two Different Scenarios
Based on the code, there are two distinct ways the WebSocket gets closed:
-
Explicit termination (via
terminateSession()method):- Uses code
4001and reason"user-closed-panel" - Signals to the server that the session should be terminated immediately
- Intended for when the user actively closes the panel
- Uses code
-
Component unmount (cleanup in
useEffect):- Currently uses normal close (
code 1000, no reason) - Allows the server to keep the session alive with a grace period for potential resumption
- Happens when the component is unmounted (e.g., page navigation, React re-render)
- Currently uses normal close (
The Problem Copilot Identified
When the component unmounts and calls socketReference.current.close() without arguments, it produces a CloseEvent with code 1000 (normal closure) and an empty reason.
Looking at your handleWebSocketClose function (around line 1645), it checks for specific reasons to avoid reconnecting:
const userClosedTerminal =
event.reason === "user-closed-primary" ||
event.reason === "user-closed-secondary" ||
event.reason === "manual-reconnect";Since the unmount close has no reason and code 1000 is not in your NON_RECOVERABLE_CLOSE_CODES set, the close event could potentially fall through to the reconnection logic. This means:
- Reconnection timers might be scheduled
- The component is already unmounted, so any callbacks would reference stale DOM/refs
- Resource leak and potential errors
Current Mitigation
I notice your unmount cleanup does call grResetState() at the end:
grResetState(); // Ensure global state is clean
clearConnectionTimeout();Key question: Does grResetState() properly cancel any pending reconnection timers? If yes, this concern is mitigated. If not, there's a potential issue.
Recommended Solutions
Option 1: Add a specific close reason for unmount to prevent reconnection logic from firing:
// Close with a reason that handleWebSocketClose recognizes as non-reconnectable
socketReference.current.close(1000, "component-unmount");Then update handleWebSocketClose to recognize it:
const userClosedTerminal =
event.reason === "user-closed-primary" ||
event.reason === "user-closed-secondary" ||
event.reason === "manual-reconnect" ||
event.reason === "component-unmount";Option 2: Explicitly cancel reconnection and remove event listeners before closing:
grCancelReconnect(); // Cancel any pending reconnect timers
if (socketReference.current) {
// Note: Since you use addEventListener, you'd need to remove listeners
// rather than set onclose = null
socketReference.current.close();
}
grResetState();The design intent (normal close to allow resume) is good, but you need to ensure the close handler doesn't trigger unwanted reconnection attempts after unmount. Let me know if you'd like me to search for the implementation of grResetState to verify it cancels timers properly!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@denissellu can you take a look
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These new tests render
AblyCliTerminalwithablyApiKey, but the component props interface in this package expectssignedConfig+signature(and there is noablyApiKeyprop). This will fail type-checking and likely won’t exercise the real auth/connection path. Update the tests to use the existingrenderTerminalhelper/fixtures (DEFAULT_SIGNED_CONFIG/DEFAULT_SIGNATURE/TEST_WEBSOCKET_URL) so they compile and match the component’s supported API.