Reconnect a frozen room socket on resume instead of trusting a stale 'open'#111
Merged
Conversation
An iOS background freeze suspends the JS timers, so the room socket can die server-side with no `onclose` ever firing — `status` stays a stale 'open'. On foreground we then skipped the reconnect (the guard was `!== 'open'`), and a message sent in that window hit `ws.send` on a dead socket: kept in `sent` but never `outbox`, so nothing resent it and it surfaced as "Lucid didn't respond" while the daemon never received it. Judge liveness by the inbound-frame clock, not `status`. `resumeForeground` forces a fresh dial when the socket looks dead (no inbound for >15s despite an echoing server), and `enqueue` routes a send to the outbox — reconnecting — rather than into the void whenever the socket only looks open.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #108.
The bug
An iOS background freeze suspends the JS event loop, so the room socket can die server-side with no
oncloseever firing —statusstays a stale'open'. Two things then went wrong on resume:useLucidAsk.tsx,getStatus() !== 'open'). After a freeze the status reads'open', so we skipped the reconnect and kept trusting a dead socket.ws.send()on the dead socket and was orphaned — the payload lands insentbut neveroutbox, and only a retryable daemon error resends fromsent. A dead-socket send gets no daemon response at all, so nothing resent it: it sat until the first-token timeout → "Lucid didn't respond", with the daemon never receiving it.The fix
Judge liveness by the inbound-frame clock, not
status:resumeForeground()(called onAppState → active) forces a fresh dial when the socket looks dead — no inbound frame for >15s (STALE_INBOUND_MS) despite a server we've proven echoes. A genuinely live socket echoes a keepalive every ~10s, so it can't reach 15s while alive; the check is gated onpongsSeen > 0and a socket that's been open a while, so a fresh open or a non-echoing server never false-positives.enqueue()now routes a send to theoutbox(and reconnects) instead ofws.send()whenever the socket only looks open. The freshonopenflushes the outbox onto a live socket.This makes
statusreflect reality before the user can type, which is the clean fix for the orphaned send — no "resend fromsent" (which would risk double-running an ask against the room's frame replay).Verification
tsc --noEmitclean. No behavior change on a healthy socket (the staleness check can't trigger while inbound is flowing).pongsSeenand open-duration to avoid false reconnects).Sibling issues from the same session: #107 (silent resume memory-wipe, fixed in #110) and #109 (diagnostics blind to the freeze).