[2.x] fix(realtime): only force-reconnect on iOS visibilitychange#4662
Merged
Conversation
PR #4654 added a `visibilitychange` listener that forces a fresh Pusher instance and refreshes the visible discussion list whenever the tab has been hidden for >5s. That was needed on iOS Safari, where backgrounding the page silently drops the WebSocket without firing `close`. On every other platform the WebSocket survives tab backgrounding fine — but the visibilitychange handler still fired, causing an unnecessary `GET /api/discussions` request and full list re-render every time the user switched away and back. Gate the visibilitychange-triggered `forceReconnect()` on `isIOS()` so the workaround only runs on the platform that actually needs it. The `pageshow(persisted=true)` path stays unconditional — bfcache restoration only fires on browsers that bfcached the page, and the WebSocket was definitely torn down by then regardless of platform. `isIOS()` is broader than the existing `isSafariMobile()` core utility because all iOS browsers use WebKit and share the same backgrounding pathology — iOS Chrome (`CriOS`) and iOS Firefox (`FxiOS`) are excluded by `isSafariMobile()` but still need this workaround. Regression from #4654.
ekumanov
added a commit
to ekumanov/framework
that referenced
this pull request
Jun 12, 2026
…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>
ekumanov
added a commit
to ekumanov/framework
that referenced
this pull request
Jun 18, 2026
…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 Opus 4.8 <noreply@anthropic.com>
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.
Summary
#4654 added a
visibilitychangelistener that constructs a fresh Pusher instance and refreshes the visible discussion list whenever the tab has been hidden for more than 5 seconds. That workaround was needed on iOS Safari, where backgrounding silently drops the WebSocket without firingclose. On every other platform the WebSocket survives tab backgrounding fine — but the listener still fired unconditionally, so on desktop Firefox/Chrome (and Android) every "switch to another tab, come back" cycle triggered an unnecessaryGET /api/discussionsrequest and a full IndexPage refresh.Fix
Gate the
visibilitychange-triggeredforceReconnect()onisIOS(). Thepageshow(persisted=true)path stays unconditional — bfcache restore only happens on browsers that bfcached the page, and at that point the WebSocket was definitely torn down regardless of platform.A new
isIOS()utility is added rather than reusing the existingisSafariMobile()core helper, becauseisSafariMobile()excludes iOS Chrome (CriOS) and iOS Firefox (FxiOS) — both of which use WebKit on iOS and share the same backgrounding pathology this workaround was written for.isIOS()also detects iPadOS 13+ via theMacIntel+maxTouchPoints > 1quirk (iPadOS reportsnavigator.platform === 'MacIntel'but desktop Macs havemaxTouchPoints === 0).Test plan
GET /api/discussionsrequest fires. Discussion list does not visibly refresh. (Was happening before this fix.)lives: 2kill-switch #4597.pageshow(persisted=true)path is unchanged.Regression from #4654.