fix(client): detect stalled event streams and resync on foreground - #47571
Merged
Merged
Conversation
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.
Lock a phone with beta.opencode.ai open, unlock it ten minutes later, and the session is frozen at its pre-lock state until you reload. The socket died while the page was suspended, but the browser never reported that on the hung
reader.read(), soconnection.status()stayed"connected"and none of the resync paths (which key off a status transition orserver.connected) ever fired.Why the app could not notice
server.connected,iterator.next()could hang forever.: heartbeatevery 15 s, but the SSE parser drops comment lines, so nothing in the client knew the stream had gone quiet.pageLifecycleonly wiredpagehide/pageshow. Locking a phone or switching apps firesvisibilitychange, notpagehide.What changes
sequenceDiagram participant S as Server participant SSE as SSE reader participant SE as SharedEvents participant C as createClientConnection S-->>SSE: ": heartbeat" (every 15 s) SSE->>SE: onActivity() SE->>C: onActivity() C->>C: reset 45 s watchdog Note over C: device sleeps, socket dies silently C->>C: watchdog fires (or visibilitychange → visible after >20 s idle) C->>SSE: abort("Event stream stalled") C->>C: status = reconnecting → sync.invalidate() C->>S: new /api/event S-->>C: server.connected → full resyncRequestOptions.onActivity(codegen + regenerated client): the SSE reader reports every chunk, including keepalive comments.SharedEvents.subscribe({ onActivity })fans it out per subscriber.createClientConnection: any received bytes push a 45 s deadline out. A timer whose deadline passed while the page was suspended fires as soon as it resumes, so a dead socket is replaced within milliseconds of unlock.visibilitychange → visibleforces an immediate reconnect only if the stream has been silent for more than one heartbeat interval (20 s); a live desktop tab that kept receiving heartbeats is left alone.onlinealways forces one. Forced resyncs skip the 1 s backoff.stop()now marks the connection as not connected, andpageshowalways callsstart(). Previously apagehidewithout apersistedpageshowleft a dead stream reporting"connected"forever.start()runs at creation instead of inonMount; the first event is always delivered asynchronously through the flush timer, so consumers see no ordering change.