fix(client): always force WS reconnect on iOS foreground return#368
Conversation
iOS kills WebSocket connections during background without updating readyState. checkAndReconnect() previously trusted readyState, so if the socket appeared OPEN the function did nothing — first message sent on the dead socket was silently lost. Remove the readyState guard: always tear down the old socket and create a fresh connection. Sends during the reconnect window queue in pendingSends and flush after the welcome handshake completes. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 3 issue(s) (3 warning).
packages/client/src/connection.ts
Correct fix for the iOS stale-socket problem, but orphans live sockets without closing them and forces unnecessary reconnect churn on desktop — both worth addressing.
- 🟡 regressions (L337): On desktop browsers, every tab switch (visibilitychange → visible) now tears down a healthy WebSocket and creates a new one. The old code preserved live sockets. This adds reconnect latency, a brief disconnected flash, and extra server load on every foreground return — even when the socket is fine. The reconnect + seq replay machinery makes this safe from a correctness standpoint, but it's wasteful on non-iOS platforms. Consider guarding with a Capacitor/iOS platform check, or at minimum accepting this as a documented trade-off.
[fixable] - 🟡 bugs (L339): defuseOldWs() nulls out handlers and drops the reference but never calls ws.close(). On desktop where the socket IS still alive, this orphans an open WebSocket — the server keeps the old connection object until TCP timeout. Previously this was rare (only triggered when readyState was already broken), but now it happens on every foreground transition. The orphaned socket should be explicitly closed before being abandoned to avoid server-side connection leaks.
[fixable] - 🟡 missing_tests: MitzoConnection has zero test coverage — no test or spec files exist under packages/client/. This PR changes reconnect behavior on all platforms (not just iOS). Key scenarios that should be tested: (1) checkAndReconnect always calls doConnect even when ws.readyState is OPEN, (2) checkAndReconnect is a no-op when reconnectTimer is set, (3) sends during the reconnect window queue in pendingSends and flush after welcome, (4) defuseOldWs prevents stale onclose from clobbering the new socket.
[fixable]
| * sends that arrive during the reconnect window queue in pendingSends | ||
| * and flush after the welcome handshake completes. | ||
| */ | ||
| checkAndReconnect(): void { |
There was a problem hiding this comment.
🟡 regressions: On desktop browsers, every tab switch (visibilitychange → visible) now tears down a healthy WebSocket and creates a new one. The old code preserved live sockets. This adds reconnect latency, a brief disconnected flash, and extra server load on every foreground return — even when the socket is fine. The reconnect + seq replay machinery makes this safe from a correctness standpoint, but it's wasteful on non-iOS platforms. Consider guarding with a Capacitor/iOS platform check, or at minimum accepting this as a documented trade-off. [fixable]
| this.doConnect(); | ||
| } | ||
| if (this.reconnectTimer) return; | ||
| this.defuseOldWs(); |
There was a problem hiding this comment.
🟡 bugs: defuseOldWs() nulls out handlers and drops the reference but never calls ws.close(). On desktop where the socket IS still alive, this orphans an open WebSocket — the server keeps the old connection object until TCP timeout. Previously this was rare (only triggered when readyState was already broken), but now it happens on every foreground transition. The orphaned socket should be explicitly closed before being abandoned to avoid server-side connection leaks. [fixable]
- Add force parameter to checkAndReconnect(): iOS (via forceReconnect) always forces, desktop browser visibility events use default (false) which preserves healthy sockets - Close old socket in defuseOldWs() to prevent server-side connection leak from orphaned live sockets - Add 6 tests covering: force vs default behavior, reconnectTimer guard, pending send flush after forced reconnect, socket close Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
LGTM — no issues found.
Summary
readyStatecheckAndReconnect()trustedreadyState— if the socket appeared OPEN, it did nothingpendingSendsand flush after the welcome handshakeRoot cause
connection.ts:328-336— thereadyState !== OPENguard:iOS kills the TCP connection during background but the JS
readyStateproperty staysOPENuntil a send/recv actually fails. The first user message fires into the void.Test plan
🤖 Generated with Claude Code