zebra-spaces: wireZebraMachines orchestrator + integration tests (83 green)

Step five — composition layer. wireZebraMachines() returns a coherent
room:
  - one CallFSM
  - one SubscribeFSM
  - three PublishFSMs (mic / screen / camera)
  - lazy Map of RemoteTileFSMs created on first tileFor(kind, pubHex)
  - tileLeft(pubHex) fans LEFT to every tile keyed by that publisher

Observers wire transitions between machines but the orchestrator
itself stays pure — no WebRTC, no DOM, no fetch. The page's runtime
layers its OWN observers on top to drive real side effects, and the
test extracts the orchestrator directly.

Cascades modelled:
- CallFSM joined (from anything except reconnecting) ── starts the sub
- CallFSM reconnecting → joined does NOT re-START (sub stayed alive)
- CallFSM leaving / booted ── stops sub AND every live publish
- RemoteTileFSMs lazy: tileFor returns the same instance per key
- tileLeft sends LEFT to every kind for that pubHex

+ 11 integration tests + 1 full end-to-end scenario walking through
host publishes mic+screen / listener joins late / listener sees the
screen / host unshares / mute+prune cycle removes the tile / listener
leaves and sub stops.

Total: 83 tests passing. The pure-FSM layer + orchestrator are now
ready to be wired into the imperative call sites in the live runtime.
That's the next step — gradually replace the firefighting code paths
(sfuPublishCamera, sfuSubscribe, role transitions) by feeding events
into these machines from the existing handlers, then observing
state changes to invoke the side effects. Tests catch regressions
on the pure layer while the QA loop catches what touches the wire.
This commit is contained in:
Russell Ballestrini 2026-06-02 11:10:16 -04:00
parent a29baeee7e
commit d9a743a680
No known key found for this signature in database
2 changed files with 233 additions and 10 deletions

View file

@ -1056,6 +1056,77 @@ const callSpec = {
},
};
/* ==================================================================
* wireZebraMachines — orchestrator. Composes one CallFSM, one
* SubscribeFSM, three PublishFSMs (mic/screen/camera), and a Map of
* RemoteTileFSMs into a coherent room. Observers wire transitions
* between machines; no side effects in this layer — the page's
* runtime attaches its OWN observers on top to drive actual WebRTC
* and DOM work. That separation keeps this function fully testable
* in Node with synthetic events.
*
* Returns { call, sub, pubs, remoteTiles, tileFor, tileLeft }. */
function wireZebraMachines(){
const call = createFSM(callSpec);
const sub = createFSM(subscribeSpec);
const pubs = {
mic: createFSM(publishSpec),
screen: createFSM(publishSpec),
camera: createFSM(publishSpec),
};
pubs.mic.context.kind = 'mic';
pubs.screen.context.kind = 'screen';
pubs.camera.context.kind = 'camera';
/* RemoteTileFSMs keyed by `${kind}:${pubHex}` — lazily created. */
const remoteTiles = new Map();
function tileFor(kind, pubHex){
const key = kind + ':' + pubHex;
let t = remoteTiles.get(key);
if (!t){
t = createFSM(remoteTileSpec);
t.context.kind = kind;
t.context.pubHex = pubHex;
remoteTiles.set(key, t);
}
return t;
}
/* peer-left: any tile keyed by this pubHex (any kind) goes LEFT */
function tileLeft(pubHex){
for (const [key, t] of remoteTiles){
if (key.endsWith(':' + pubHex)) t.send('LEFT');
}
}
/* CallFSM → SubscribeFSM: subscribe whenever joined, stop when not.
* Coming back from reconnecting → joined doesn't re-START because the
* SubscribeFSM stayed alive through the signal-WS drop. */
call.observe(({ state, prev }) => {
if (state === prev) return;
if (state === 'joined' && prev !== 'reconnecting'){
sub.send('START');
}
if (state === 'leaving' || state === 'booted'){
sub.send('STOP');
}
});
/* CallFSM → PublishFSMs: tearing down the call stops every live publish */
call.observe(({ state, prev }) => {
if (state === prev) return;
if (state === 'leaving' || state === 'booted'){
for (const k of Object.keys(pubs)){
const ps = pubs[k].state;
if (ps === 'acquiring' || ps === 'negotiating' || ps === 'live'){
pubs[k].send('STOP');
}
}
}
});
return { call, sub, pubs, remoteTiles, tileFor, tileLeft };
}
/* ==================================================================
* identity — ed25519 keypair, persisted in localStorage as JWK.
*
@ -3124,8 +3195,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> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">a871b9e9438f857dfd47cbaa578c676d</span><br>
sha256 <span class="stamp-sha">bdfaeba6af0fd5765508ab816cf3bde5ab405a0864368320ba26d952e0218a72</span><br>
md5 <span class="stamp-md5">4726e6221aa4643fe259cf38b6d747c9</span><br>
sha256 <span class="stamp-sha">2b13c8c80049461809709c2ab8058f71d2bb83fe2f257a86382110e454deaa21</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>