From 910f92c52ae6d1337b219596e3370b76888039b9 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 2 Jun 2026 11:07:57 -0400 Subject: [PATCH] =?UTF-8?q?zebra-report:=20CallFSM=20=E2=80=94=20mirror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zebra-report/zebra-spaces.html | 89 +++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html index c455da8..fe2f1dd 100644 --- a/zebra-report/zebra-spaces.html +++ b/zebra-report/zebra-spaces.html @@ -971,6 +971,91 @@ const remoteTileSpec = { }, }; +/* ================================================================== + * CallFSM — the user's overall lifecycle in a space. Orchestrates + * (but doesn't replace) the per-leg FSMs above: a Joined call has + * exactly one SubscribeFSM and zero-to-three PublishFSMs running + * underneath; the runtime starts/stops them on entry/exit of joined. + * + * idle ──ENTER──▶ connecting ──WELCOME──▶ joined ──LEAVE──▶ leaving ──DONE──▶ idle + * ▲ │ FAILED │ ▲ + * │ ▼ │ WS_DROPPED │ + * │ idle ▼ │ + * │ reconnecting ──WELCOME──▶ joined │ + * │ │ LEAVE / FAILED │ + * │ ▼ │ + * │ leaving ─────────────────────────────┘ + * │ ▲ + * │ │ ACK + * └─────────────────────────────────── booted ◀── BOOTED ── (any live state) + * + * Role lives in ctx (host / cohost / speaker / listener). ROLE_CHANGE + * fires re-entry of joined so observers can swap mesh/sub/publish + * topology without inventing a new state per role permutation. */ +const callSpec = { + initial: 'idle', + context: { code: '', handle: '', uuid: '', role: '', bootedBy: null, lastError: null }, + states: { + idle: { + entry: (ctx) => { ctx.uuid = ''; ctx.role = ''; ctx.bootedBy = null; }, + on: { + ENTER: { + target: 'connecting', + action: (ctx, ev) => { if (ev.payload){ ctx.code = ev.payload.code || ''; ctx.handle = ev.payload.handle || ''; } }, + }, + }, + }, + connecting: { + on: { + WELCOME: { + target: 'joined', + action: (ctx, ev) => { if (ev.payload){ ctx.uuid = ev.payload.uuid || ''; ctx.role = ev.payload.role || ''; } }, + }, + FAILED: { target: 'idle', action: (ctx, ev) => { ctx.lastError = ev.payload && ev.payload.error; } }, + BOOTED: { target: 'booted', action: (ctx, ev) => { ctx.bootedBy = ev.payload && ev.payload.by; } }, + LEAVE: 'idle', + }, + }, + joined: { + on: { + /* ROLE_CHANGE re-enters joined so observers get an event to act + * on (swap mesh peers, start/stop publish, etc.) */ + ROLE_CHANGE: { + target: 'joined', + action: (ctx, ev) => { if (ev.payload && ev.payload.role) ctx.role = ev.payload.role; }, + }, + WS_DROPPED: 'reconnecting', + LEAVE: 'leaving', + BOOTED: { target: 'booted', action: (ctx, ev) => { ctx.bootedBy = ev.payload && ev.payload.by; } }, + }, + }, + reconnecting: { + /* signal-server WebSocket dropped; the SubscribeFSM + publish + * legs stay alive because WebRTC PCs are independent of the WS. + * On WELCOME (a re-issued one after the WS comes back) we land + * back in joined and ROLE_CHANGE may follow. */ + on: { + WELCOME: { + target: 'joined', + action: (ctx, ev) => { if (ev.payload && ev.payload.role) ctx.role = ev.payload.role; }, + }, + FAILED: { target: 'idle', action: (ctx, ev) => { ctx.lastError = ev.payload && ev.payload.error; } }, + LEAVE: 'leaving', + BOOTED: { target: 'booted', action: (ctx, ev) => { ctx.bootedBy = ev.payload && ev.payload.by; } }, + }, + }, + leaving: { + on: { DONE: 'idle' }, + }, + booted: { + /* user has been removed from the room. ACK transitions back to + * idle so the entry screen comes back; the room's auto-rejoin + * sessionStorage key is cleared by the runtime when entering booted. */ + on: { ACK: 'idle' }, + }, + }, +}; + /* ================================================================== * identity — ed25519 keypair, persisted in localStorage as JWK. * @@ -3039,8 +3124,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');