zebra-spaces: stop subscribe→close→subscribe loop — listeners stay connected
Root cause of the 'late listener can't see screens' symptom: my recent
'rebuild sub PC on failure' commit (6a72e77) triggered on connection
state 'closed' as well as 'failed', AND the guard `if (sfuSubPC === pc)`
only worked because we ASSUMED Chrome would fire the state change
asynchronously. Chrome fires it SYNCHRONOUSLY during pc.close(), at
which point sfuSubPC still points at the closing pc — guard passes,
rebuild fires. Then sfuUnsubscribe closes the new PC, which triggers
another rebuild. SFU log showed listeners cycling
subscribe → 40s → close → subscribe forever.
Fix:
1. onconnectionstatechange now only rebuilds on 'failed' (the actually-
terminal state). 'closed' = sfuUnsubscribe(), 'disconnected' =
transient and WebRTC may recover on its own.
2. sfuUnsubscribe nulls sfuSubPC BEFORE pc.close(), so even if the
handler fired synchronously the === guard would correctly fail.
3. visibilitychange handler also tightened to only fire on 'failed' —
same reasoning.
The rebuild path for actual ICE failures still works (state goes
'connected' → 'disconnected' → 'failed' → rebuild).
83 FSM tests still green.
This commit is contained in:
parent
81b237eaa7
commit
aa36b84a47
1 changed files with 32 additions and 20 deletions
|
|
@ -2095,15 +2095,19 @@ async function sfuSubscribe(){
|
|||
* sfuUnsubscribe + sfuSubscribe. The auto-rejoin code that follows
|
||||
* a hard refresh handles the WS side; this handles the SFU side. */
|
||||
pc.onconnectionstatechange = () => {
|
||||
const s = pc.connectionState;
|
||||
if (s === 'failed' || s === 'closed'){
|
||||
logLine('err', 'sfu sub PC ' + s + ' — rebuilding');
|
||||
if (sfuSubPC === pc){
|
||||
sfuUnsubscribe().then(() => {
|
||||
if (wantConnected && roomID) sfuSubscribe().catch(e => logLine('err','sfu re-subscribe: '+e.message));
|
||||
});
|
||||
}
|
||||
}
|
||||
/* 'failed' is terminal ICE failure (we should rebuild).
|
||||
* 'closed' is OUR OWN sfuUnsubscribe() — never rebuild on that
|
||||
* (Chrome fires the state change synchronously before sfuSubPC is
|
||||
* null'd, which used to trigger a subscribe→close→subscribe loop
|
||||
* every time anyone deliberately tore down the sub).
|
||||
* 'disconnected' is transient — let WebRTC try to recover before
|
||||
* we yank the rug. */
|
||||
if (pc.connectionState !== 'failed') return;
|
||||
if (sfuSubPC !== pc) return;
|
||||
logLine('err', 'sfu sub PC failed — rebuilding');
|
||||
sfuUnsubscribe().then(() => {
|
||||
if (wantConnected && roomID) sfuSubscribe().catch(e => logLine('err','sfu re-subscribe: '+e.message));
|
||||
});
|
||||
};
|
||||
/* SSE: server pushes renegotiation offers when publisher set changes.
|
||||
* We answer each via POST /answer. ping events are keepalive only.
|
||||
|
|
@ -2142,7 +2146,15 @@ async function sfuSubscribe(){
|
|||
|
||||
async function sfuUnsubscribe(){
|
||||
if (sfuSubEvents){ try { sfuSubEvents.close(); } catch(_){} sfuSubEvents = null; }
|
||||
if (sfuSubPC){ try { sfuSubPC.close(); } catch(_){} sfuSubPC = null; sfuSubPeerID = null; }
|
||||
/* null out sfuSubPC FIRST, then close — Chrome fires the
|
||||
* connectionstatechange handler SYNCHRONOUSLY during pc.close(), and
|
||||
* the handler's `if (sfuSubPC === pc)` guard depends on the global
|
||||
* already being null. Otherwise the handler thinks the close was a
|
||||
* 'failed' rebuild trigger and we get a subscribe/close loop. */
|
||||
const oldPC = sfuSubPC;
|
||||
sfuSubPC = null;
|
||||
sfuSubPeerID = null;
|
||||
if (oldPC){ try { oldPC.close(); } catch(_){} }
|
||||
sfuStreamsByPubHex.clear();
|
||||
}
|
||||
|
||||
|
|
@ -3246,14 +3258,14 @@ refreshMicList();
|
|||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
if (!wantConnected || !sfuSubPC) return;
|
||||
const s = sfuSubPC.connectionState;
|
||||
if (s === 'failed' || s === 'closed' || s === 'disconnected'){
|
||||
logLine('err', 'visibility back, sub PC ' + s + ' — rebuilding');
|
||||
const pc = sfuSubPC;
|
||||
sfuUnsubscribe().then(() => {
|
||||
if (wantConnected && roomID) sfuSubscribe().catch(e => logLine('err','sfu re-subscribe: '+e.message));
|
||||
});
|
||||
}
|
||||
/* Only rebuild on terminal failure. 'disconnected' is transient
|
||||
* (WebRTC tries to recover); 'closed' never happens here because
|
||||
* sfuUnsubscribe() nulls sfuSubPC before pc.close(). */
|
||||
if (sfuSubPC.connectionState !== 'failed') return;
|
||||
logLine('err', 'visibility back, sub PC failed — rebuilding');
|
||||
sfuUnsubscribe().then(() => {
|
||||
if (wantConnected && roomID) sfuSubscribe().catch(e => logLine('err','sfu re-subscribe: '+e.message));
|
||||
});
|
||||
});
|
||||
|
||||
/* ==================================================================
|
||||
|
|
@ -3348,8 +3360,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-02</span><br>
|
||||
md5 <span class="stamp-md5">e7c6d045cf7204add40ff3ae905eab0f</span><br>
|
||||
sha256 <span class="stamp-sha">76761abd84bcfee8d479b69c8d4a68f90bf8133b15c7b2b99b817732f31d2deb</span><br>
|
||||
md5 <span class="stamp-md5">e889a0a868e23e2be985f28db80ff031</span><br>
|
||||
sha256 <span class="stamp-sha">e2d9440bd38fc3081eaafe68d972d3ac8a856ace24da919c6eb6b44ec9a0a7d2</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