Skip to content

fix(chat): stop a reconnect reopening the event stream in the background - #1510

Merged
bmc08gt merged 1 commit into
code/cashfrom
fix/chat-background-stream
Sep 21, 2026
Merged

bmc08gt merged 1 commit into
code/cashfrom
fix/chat-background-stream

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Bugsnag 6ab0133d58142669ea781ea5 (2026.9.1, versionCode 4503) has the bidirectional event stream reopening 6.5 seconds after MainActivity#onStop() and going on to sync the feed, with the app in the background. TokenCoordinator cancelled its reserve state stream a second after the same onStop:

17:09:04.901  MainActivity#onStop()
17:09:05.550  [TokenCoordinator] Lifecycle stopped, cancelling reserve state stream
17:09:11.407  [ChatCoordinator] Network connected, re-syncing chat feed
17:09:11.410  [event-streaming] Opening bidirectional stream (attempt 1)
17:09:11.730  [FeedSyncDelegate] Feed synced: 2 chats

ChatCoordinator.onStop already does what TokenCoordinator does: it stops the heartbeat and calls eventStreamDelegate.close(). Nothing here is a deliberate background channel kept alive for notification delivery, so the reopen is a second path that gets around the teardown rather than an intended exception to it.

That path is the network-reconnect collector. It is launched on scope, which spans the logged-in session rather than the foreground, so a reconnect landing after onStop re-synced the feed and reopened the stream behind the close. It now reads a foregrounded flag maintained by onStart/onStop, checked after the debounce so the answer is the lifecycle state the reconnect would actually act in rather than the one it arrived in. A reconnect that lands while backgrounded is dropped rather than deferred: onStart already syncs and reopens, so nothing is lost by ignoring it.

The foreground churn was a second bug

The same breadcrumbs show Network connected, re-syncing chat feed firing at 17:08:54, :56, 17:09:00, :03 and :11, each one followed by Event stream already connected, skipping open. distinctUntilChanged was applied to the whole NetworkState, which carries signal strength and connection type alongside connected, and a cellular radio reports those every few seconds. Every signal wobble therefore passed through as a fresh reconnect and re-synced the feed on a connection that never dropped. Mapping to connected before de-duplicating collapses them.

Tests

ChatCoordinatorLifecycleStreamTest covers the reconnect in the background, the reconnect in the foreground as a control, and the signal wobbles. Two things about this class are worth knowing before editing it, both of which cost real time here:

  • advanceUntilIdle() never returns, because startHeartbeat keeps an unbounded timed loop scheduled. Use runCurrent() and advanceTimeBy(), as the other chat coordinator tests do.
  • Assert only after coordinator.teardown(). An assertion that throws first skips the teardown, and runTest's cleanup drain then spins on virtual time instead of reporting the failure.

The wobbles have to be spaced with advanceTimeBy rather than applied back to back. NetworkConnectivityListener.state is a conflated StateFlow, so wobbles set in the same instant collapse before the collector sees them and the churn under test never happens.

No rendering or UI code is touched.

Bugsnag 6ab0133d58142669ea781ea5 has the bidirectional event stream reopening
6.5 seconds after `MainActivity#onStop()` and going on to sync the feed, with
the app in the background. `TokenCoordinator` cancelled its reserve state
stream a second after the same `onStop`.

`ChatCoordinator.onStop` already closes the stream and stops the heartbeat, so
the reopen comes from a second path. The network-reconnect collector is
launched on `scope`, which spans the logged-in session rather than the
foreground, and a reconnect landing after `onStop` re-synced the feed and
reopened the stream behind the close. It now reads a `foregrounded` flag
maintained by `onStart`/`onStop`, checked after the debounce so the answer is
the lifecycle state the reconnect would actually act in. A reconnect that
lands while backgrounded is dropped rather than deferred, since `onStart`
already syncs and reopens.

The same breadcrumbs show "Network connected, re-syncing chat feed" firing
every few seconds in the foreground, each one skipping an already-connected
stream. `distinctUntilChanged` was applied to the whole `NetworkState`, which
carries signal strength and connection type alongside `connected`, and a
cellular radio reports those every few seconds. Every signal wobble passed
through as a fresh reconnect on a connection that never dropped. Mapping to
`connected` before de-duplicating collapses them.
@bmc08gt bmc08gt self-assigned this Sep 20, 2026
@github-actions github-actions Bot added the type: fix Bug fix label Sep 20, 2026
@bmc08gt
bmc08gt merged commit 7047205 into code/cash Sep 21, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the fix/chat-background-stream branch September 23, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant