Skip to content

[2.x] fix(realtime): only force-reconnect on iOS visibilitychange#4662

Merged
imorland merged 1 commit into
2.xfrom
im/realtime-ios-only-force-reconnect
May 13, 2026
Merged

[2.x] fix(realtime): only force-reconnect on iOS visibilitychange#4662
imorland merged 1 commit into
2.xfrom
im/realtime-ios-only-force-reconnect

Conversation

@imorland

Copy link
Copy Markdown
Member

Summary

#4654 added a visibilitychange listener 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 firing close. 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 unnecessary GET /api/discussions request and a full IndexPage refresh.

Fix

Gate the visibilitychange-triggered forceReconnect() on isIOS(). The pageshow(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 existing isSafariMobile() core helper, because isSafariMobile() 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 the MacIntel + maxTouchPoints > 1 quirk (iPadOS reports navigator.platform === 'MacIntel' but desktop Macs have maxTouchPoints === 0).

Test plan

Regression from #4654.

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.
@imorland imorland requested a review from a team as a code owner May 13, 2026 21:36
@imorland imorland added this to the 2.0.0-rc.2 milestone May 13, 2026
@imorland imorland merged commit 1932e3e into 2.x May 13, 2026
26 of 37 checks passed
@imorland imorland deleted the im/realtime-ios-only-force-reconnect branch May 13, 2026 22:03
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant