Skip to content

fix(mobile): reconnect on resume instead of trusting a stale status - #3837

Closed
AI-OWEN wants to merge 1 commit into
block:mainfrom
AI-OWEN:fix/mobile-resume-stale-connection
Closed

fix(mobile): reconnect on resume instead of trusting a stale status#3837
AI-OWEN wants to merge 1 commit into
block:mainfrom
AI-OWEN:fix/mobile-resume-stale-connection

Conversation

@AI-OWEN

@AI-OWEN AI-OWEN commented Jul 31, 2026

Copy link
Copy Markdown

Summary

On mobile, the relay session could come back from a real background stint believing it was still connected while sitting on a dead socket. No events arrived until some later write surfaced the error — which is why replies would appear all at once the moment the user typed something.

onAppResumed() returned early whenever state.status was already connected, on the assumption that a still-connected status meant the socket survived the 5-second background grace window:

// If still connected, nothing to do — the socket survived the background
// grace window.
if (state.status == SessionStatus.connected) return;

That assumption does not hold once the OS suspends the process. The grace timer never fires, and a socket torn down by the platform or by carrier NAT never delivers its onDisconnected callback, so the status is stale in exactly the case where it matters.

This change tracks whether the app actually backgrounded (onAppPaused() fired) and only trusts a connected status when it did not:

  • Resume from inactive — a Control Centre swipe or a notification banner — never calls onAppPaused(), so that path keeps its live socket and gains no reconnect churn.
  • Resume after a real background stint reconnects regardless of the reported status. When the status still claims connected, the old socket is torn down first via reconnect() so the replacement isn't racing a half-open one.

Reconnecting is idempotent and replays missed events, so an occasional redundant reconnect is far cheaper than a silent stall.

Related issue

None found — searched open issues and PRs for existing reports of stale mobile sockets on resume.

Testing

Two new tests in mobile/test/shared/relay/relay_session_test.dart, covering both directions of the condition:

  • reconnects on resume even when the status still claims connected — the regression itself
  • does not rebuild the socket when the app never backgrounded — guards against trading a silent stall for reconnect churn on every Control Centre swipe

Full mobile suite on the rebased branch (not just the touched file):

dart format --output=none --set-exit-if-changed .   →  Formatted 331 files (0 changed)
flutter analyze                                     →  No issues found! (ran in 15.8s)
flutter test                                        →  +1024 ~1: All tests passed!

No UI change — behaviour only.

When iOS suspends the app, the 5-second background grace timer never
fires and a socket torn down by the platform or carrier NAT never
delivers its onDisconnected callback. onAppResumed then sees a
`connected` status, returns early, and the session sits on a dead
socket -- no events arrive until some later write surfaces the error,
which is why replies appear all at once after the user types something.

Track whether the app actually backgrounded and only trust a
`connected` status when it did not, so resuming from `inactive` (a
Control Centre swipe) still keeps its live socket. After a real
background stint, reconnect regardless of the reported status;
reconnect is idempotent and replays missed events, so a redundant
reconnect is far cheaper than a silent stall.

Signed-off-by: Owen Gallagher <70806278+AI-OWEN@users.noreply.github.com>
@AI-OWEN
AI-OWEN requested a review from a team as a code owner July 31, 2026 00:59
@Chessing234

Copy link
Copy Markdown
Contributor

this matches the laggy-replies report. can you also cover a case where resume fires while a reconnect is already in flight?

@AI-OWEN

AI-OWEN commented Aug 5, 2026

Copy link
Copy Markdown
Author

Closing this as superseded by #4372 ("fix(mobile): recover stale relay sessions"), merged on 2026-08-03.

That PR fixes the same bug in the same function, and does more of it. The comparison, for the record:

  • Here: a _wasBackgrounded bool — reconnect after any real background stint.
  • fix(mobile): recover stale relay sessions #4372: a _backgroundedAt timestamp — reconnect once the app has been backgrounded past the 5s grace window, plus a 30s WebSocket pingInterval so the sub-5s window is covered by socket liveness rather than being trusted outright.

The one place this branch went further was calling reconnect() to tear the half-open socket down before redialling, where onAppResumed on main now calls _connect() directly. That is not a real gap: _connect() already does _socket?.dispose() and gates the previous socket's callbacks behind _connectionGeneration, so the stale socket cannot deliver into the new session.

@Chessing234 — your question about resume firing while a reconnect is already in flight is answered by that same generation counter on main; the older in-flight connection has a stale generation and its onConnected/onMessage callbacks are dropped. Thanks for the review either way.

Nothing here is worth rebasing on top of the merged fix.

@AI-OWEN AI-OWEN closed this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants