fix(hooks): detach WebSocket handlers on explicit disconnect#157
fix(hooks): detach WebSocket handlers on explicit disconnect#1576figpsolseeker wants to merge 1 commit intodcccrypto:mainfrom
Conversation
The disconnect() in usePriceStream called ws.close() without detaching the onclose handler first. Because close() is async, the onclose callback would still fire on the next event loop tick — and its body schedules a setTimeout(connect, ...) for auto-reconnect. Result: a zombie reconnect timer would wake up after the app backgrounded or the component unmounted, either calling setState on an unmounted component (React warning) or creating a duplicate WebSocket when the app foregrounded. The fix detaches all four handlers (onopen, onmessage, onerror, onclose) before calling close(). For unexpected closes (network drop while foregrounded), the onclose handler stays attached and auto-reconnect still works as intended. Adds a regression test asserting the handlers are null after unmount. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR adds explicit WebSocket event handler cleanup to the Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
__tests__/hooks/usePriceStream.test.ts (1)
34-36: Consider: Mock doesn't simulate asynconcloseinvocation.The real
WebSocket.close()firesoncloseasynchronously after the close handshake. The mock setsclosed = truesynchronously without invokingonclose. This is fine for the current test since you verify handlers are nulled beforeclose()returns, but if you want stronger proof against the race condition, you could optionally enhance the mock:close() { this.closed = true; // Simulate async onclose delivery setTimeout(() => this.onclose?.(), 0); }This would let you verify that the reconnect timer doesn't fire even when
oncloseis dispatched post-disconnect.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@__tests__/hooks/usePriceStream.test.ts` around lines 34 - 36, Mock WebSocket.close() currently sets this.closed synchronously but doesn't simulate the async onclose callback; update the mock implementation of close() in the test (the mock class's close method and its onclose handler) to mark closed and schedule an asynchronous invocation of this.onclose (e.g., via setTimeout 0) so tests exercise the real-world race where onclose fires after close handshake and ensure the reconnect timer logic in usePriceStream is still correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@__tests__/hooks/usePriceStream.test.ts`:
- Around line 34-36: Mock WebSocket.close() currently sets this.closed
synchronously but doesn't simulate the async onclose callback; update the mock
implementation of close() in the test (the mock class's close method and its
onclose handler) to mark closed and schedule an asynchronous invocation of
this.onclose (e.g., via setTimeout 0) so tests exercise the real-world race
where onclose fires after close handshake and ensure the reconnect timer logic
in usePriceStream is still correct.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 28d2f5d5-1f11-40f7-bcfd-fdedd5336af7
📒 Files selected for processing (2)
__tests__/hooks/usePriceStream.test.tssrc/hooks/usePriceStream.ts
Summary
disconnect()inusePriceStreamcalledws.close()without detaching theonclosehandler first. Becauseclose()is async, theonclosecallback still fires on the next event loop tick — and its body schedules asetTimeout(connect, ...)for auto-reconnect.onopen,onmessage,onerror,onclose) before callingclose(). For unexpected closes (network drop while foregrounded), the handler stays attached and auto-reconnect still works as designed.Reproduction (before fix)
disconnect()runs,wsRef.current = nullonclosecallback fires ~immediately afterwardreconnectTimerRef.current = setTimeout(connect, baseDelay + jitter)— a timer in the background-ed appconnect()opens a new WS even though the user isn't using the apphandleAppState('active')ALSO callsconnect()→ double connectionWhat changed
src/hooks/usePriceStream.tsonopen/onmessage/onerror/onclosebeforeclose()indisconnect()__tests__/hooks/usePriceStream.test.tsTest plan
usePriceStreamtests pass (18 existing + 1 new regression test)🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests