zebra-spaces: send 'bye' on pagehide for listeners even on bfcache (kill closed-firefox roster zombies)
Fox 2026-06-06: "closed firefox on both phones [they] are both still
in the list" — listener-roster entries persisting indefinitely after
mobile Firefox close.
Two facts collided:
1. SERVER (proxy.unturf.com main.go aliveJanitorTick ~line 1872):
listeners are fully exempt from the heartbeat-stall reaper.
Justified fox 2026-06-04 because the page's {type:"alive"}
timer throttles hard on backgrounded mobile tabs (1Hz on
Android, paused on iOS power-save) — without the exemption,
mobile listeners lost their seat every time they tab-switched.
2. CLIENT (this file, sendByeIfRealClose): pagehide with
event.persisted=true means the page is going into bfcache
(mobile app-switch / tab-close-to-bfcache / lock screen),
so we SKIPPED the 'bye' message to keep PCs warm for resume.
Justified fox 2026-06-03 because "the phone leaving and
coming back cannot hear the music" — bye-driven SFU eviction
killed the speaker's publish + subscribe PCs.
Net: a mobile Firefox close fires pagehide(persisted=true) →
no 'bye' → server has only readTimeout (120s) + hiccup grace
(8s) to detect the dead socket → ~128s of phantom listener
seat in every other client's roster.
The bfcache justification on the CLIENT side was always
specific to speakers (they have PCs to protect). LISTENERS:
- have no publisher PC
- finalizeLeave at main.go:1089 explicitly exempts them from
evictFromSFU (their subscriber PC stays alive through the
bye)
- re-handshake fresh on pageshow via POST /subscribe (same
path as a cold join)
So for listeners, sending 'bye' on persisted=true is
roster-only cleanup: peer-left broadcast, members.delete(uuid)
on every other client, brief disappearance from the room. On
pageshow they re-handshake and reappear — same UX as a cold
rejoin, which already works.
Fix: split the bfcache rule by role. Send 'bye' on pagehide
even when persisted=true if myRole === 'listener'. Speakers
keep the original bfcache skip exactly.
Server-side backstop (60s listener-specific TTL replacing the
full exemption) ships as a separate commit in proxy.unturf.com
so 'bye' losses (carrier NAT eating the TCP shutdown, abrupt
process kill, custom Firefox close paths) still get reaped
within the minute.
Pinned by test/sendbye-fsm.test.js — extracts
sendByeIfRealClose from this page and drives 9 scenarios
covering each (role, persisted) combination plus defensive
edges (no event, ws not open, post-demote listener state).
Wired into test-all via test-sendbye target.
This commit is contained in:
parent
0ce1339f8e
commit
5e8cdadb29
3 changed files with 178 additions and 15 deletions
|
|
@ -7571,20 +7571,33 @@ refreshSpeakerList();
|
|||
* this, a closed tab dies silently → hiccup grace → 8s of trailing
|
||||
* audio. Fox 2026-06-03.
|
||||
*
|
||||
* Critical guard: pagehide ALSO fires when the page goes into the
|
||||
* back-forward cache (mobile app-switch / lock screen / minimise),
|
||||
* with event.persisted=true. We must NOT send bye in that case —
|
||||
* the page is still alive, just paused; on pageshow it resumes with
|
||||
* the same WS / SFU / mesh state. Sending bye here would force the
|
||||
* server to evict the SFU PCs, and when the phone comes back the
|
||||
* resumed audio path stays muted (Fox 2026-06-03: "now the phone
|
||||
* leaving and coming back cannot hear the music").
|
||||
* Critical guard for SPEAKERS: pagehide ALSO fires when the page
|
||||
* goes into the back-forward cache (mobile app-switch / lock screen
|
||||
* / minimise), with event.persisted=true. A speaker on bfcache
|
||||
* MUST NOT send bye — finalizeLeave would post evictFromSFU on
|
||||
* their publisher AND subscriber PCs, and when the phone comes
|
||||
* back the resumed audio path stays muted (Fox 2026-06-03: "now
|
||||
* the phone leaving and coming back cannot hear the music").
|
||||
*
|
||||
* The JS heartbeat handles the bfcache case independently — while
|
||||
* the page is bfcached, setInterval is paused, so the server's
|
||||
* aliveTTL fires after 45s if the user doesn't come back. */
|
||||
* LISTENERS take the opposite branch: send bye even on persisted.
|
||||
* finalizeLeave already exempts listeners from evictFromSFU
|
||||
* (signal-server main.go line ~1089), so a listener bye is roster-
|
||||
* only — peer-left broadcast, member entry deleted, no eviction.
|
||||
* On pageshow the listener re-handshakes (sub PC + WS join) like
|
||||
* a cold join, same path that already works. The cost is a brief
|
||||
* "rejoin" appearance in other clients' rosters; the benefit is
|
||||
* the immediate disappearance of closed-Firefox-on-phone zombies
|
||||
* who otherwise wait out the 120s readTimeout + 8s hiccup grace
|
||||
* because listeners are exempt from the server's aliveJanitor.
|
||||
* Fox 2026-06-06: "closed firefox on both phones [they] are both
|
||||
* still in the list".
|
||||
*
|
||||
* The JS heartbeat handles the speaker bfcache case independently
|
||||
* — while the page is bfcached, setInterval is paused, so the
|
||||
* server's aliveTTL fires after 45s if the user doesn't come back. */
|
||||
function sendByeIfRealClose(ev){
|
||||
if (ev && ev.persisted) return; // bfcache — page is napping, not dying
|
||||
/* speaker on bfcache: keep the PCs warm, ride out the nap */
|
||||
if (ev && ev.persisted && myRole !== 'listener') return;
|
||||
try {
|
||||
if (ws && ws.readyState === WebSocket.OPEN){
|
||||
ws.send(JSON.stringify({ type: 'bye' }));
|
||||
|
|
@ -7891,8 +7904,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
|
|||
|
||||
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
|
||||
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> · built <span class="stamp-date">2026-06-06</span><br>
|
||||
md5 <span class="stamp-md5">cf7bb5c3111551561745c6432932a659</span><br>
|
||||
sha256 <span class="stamp-sha">fe4edd4a17f457275f1b73dfa3fe61d538a1e0d1b2593efca50b1147a68b71fa</span><br>
|
||||
md5 <span class="stamp-md5">973996244966a40f7370e23bec5dcbb5</span><br>
|
||||
sha256 <span class="stamp-sha">66f3d4eebd028f7ea16ea58df624ed0d2223505ff5c99715730379757e613630</span><br>
|
||||
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
|
||||
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
|
||||
</footer>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue