[2.x] fix(realtime): recover desktop tabs after silent socket loss and catch up missed events#4718
Draft
ekumanov wants to merge 1 commit into
Draft
Conversation
…h up missed events WebKit suspends hidden pages on desktop Safari just like on iOS, so the WebSocket dies silently (often without `close`) while pusher-js's foreground-only activity timers cannot notice. The visibilitychange recovery from flarum#4590/flarum#4654 was gated to isIOS() by flarum#4662 and never runs there, and pusher-js's own reconnects perform no catch-up — so posts that fired while the socket was down never appear, and the open discussion silently stops live-updating (a >=2-post gap makes PostStreamState.update()'s viewingEnd() guard refuse forever). - Reconnect on visibility-restore when the connection is demonstrably unhealthy (state not 'connected', or no protocol frame within the activity window), in addition to the unconditional iOS path. Healthy desktop tabs keep receiving pongs while hidden, so plain tab switches still trigger no refetch (flarum#4662 intact). - Catch up after every effective reconnect (pusher-internal or forced): refresh the discussion list and re-sync an open DiscussionPage, capturing viewingEnd() before the refetch grows postIds. - Add PostStreamState.syncEnd() — update() without the 1-post drift bound — and capture end-ness in NewActivity before pushing event payloads, so a gap no longer permanently disables live-append. Fixes flarum#4717. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
003a0ab to
5b7ad93
Compare
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 #4717.
Full analysis: see the issue. Short version: desktop Safari suspends hidden pages exactly like iOS WebKit, so the WebSocket dies silently while the tab is backgrounded — but #4662 gated the #4590/#4654 recovery to
isIOS(), pusher-js's own reconnects perform no catch-up on any platform, and a ≥2-post gap permanently disables live-append in an open discussion becausePostStreamState.update()is guarded byviewingEnd()'s 1-post drift tolerance. Net effect observed in production: a missed reply never appears (quiet discussion), or appears ~50 minutes late only because newer replies happened to force a range load.Changes
extensions/realtime—extend/Application.tsvisibilitychangerecovery now fires forisIOS()(unchanged) or whenever the connection is demonstrably unhealthy:connection.state !== 'connected', or no protocol frame received within the activity window (65 s — server-advertisedactivity_timeout30 s + pong wait), tracked via abind_globallast-activity timestamp. Healthy desktop tabs keep receivingpusher:pongevery ≤30 s even while hidden (verified in Chrome with a 15-minute hidden-tab soak), so plain tab switches still cause no spurious refetch — the [2.x] fix(realtime): only force-reconnect on iOS visibilitychange #4662 complaint stays fixed; the zombie case (state saysconnected, socket long dead) now reconnects.watchConnection()applied to the initial Pusher instance and to every fresh instanceforceReconnect()builds: on any transition intoconnectedafter a previously established connection — pusher-js-internal recovery as well as forced rebuilds — it runscatchUp(). This replaces the one-shotonReconnectedrefresh that only the iOS path used to get.catchUp()refreshes the discussion list (as before) and re-syncs an openDiscussionPage: it capturesstream.viewingEnd()before the refetch (reliable at that point — the missed posts haven't reached the store yet), thenstore.find('discussions', id)and, if the user was at the end, loads through to the new count.extensions/realtime—extend/Discussion/NewActivity.tsviewingEnd()beforeapp.store.pushPayload(...)updatespostIds, and callstream.syncEnd()when the user was at the end. Previously, one missed event put the drift at ≥2 and every subsequentupdate()returned early — live-append was permanently dead for that page view even though events were arriving fine.framework/core—states/PostStreamState.tssyncEnd(): load through to the latest post, expanding the visible window — i.e.update()without the drift guard, for callers that have already established end-ness.update()is refactored to delegate to it and documents the caveat. No behavior change for existing callers.Testing
framework/core/js/tests/unit/forum/states/PostStreamState.test.ts: three new cases —update()appends a single new post when viewing the end;update()refuses at ≥2-post drift (locks the guard semantics the fix relies on);syncEnd()loads through regardless of drift. All 10 cases in the file pass.yarn buildandyarn check-typingspass for the extension;yarn check-typingspasses for core.Per convention, no
js/dist/dist-typingsare included.