Unify the call/huddle session lifecycle across the clients (LC-613) #574

Merged
longjacksonle merged 1 commit from refactor/lc613-converge-rtc-clients into main 2026-07-21 03:05:01 +02:00

Workstream 4 of LC-596: converge call.js and voice.js "where practical". This does the session-lifecycle convergence (the ticket's headline) and leaves the media layer surface-specific by design.

The duplication this removes

call.js (1:1, one RTCPeerConnection) and voice.js (huddle, mesh or SFU) each published their own real-time-session state, and transcribe.js reconciled the two surfaces. Two concrete things were duplicated:

Two session-room globals. __lcCallRoom and __lcVoiceRoom never coexisted (you are in one real-time session at a time), had identical set-on-enter / null-on-exit lifecycles, and their only shared reader already coalesced them with ||. They become one __lcSessionRoom, so transcribe.js's resolution drops from data-lc-room || __lcCallRoom || __lcVoiceRoom to data-lc-room || __lcSessionRoom. That reconciliation was the ticket's opening complaint.

Two lifecycle event pairs. lc:call-active / lc:voice-joined -> lc:rtc-session-started; lc:call-ended / lc:voice-left -> lc:rtc-session-ended. transcribe.js's four listeners collapse to two.

The one real difference, kept via a flag not two names

A 1:1 call is sole-participant, so its end must finalize the shared transcript for everyone; a huddle end must only stop our capture, because the session continues for whoever is still on the line. Rather than keep two event names to encode that, both events carry detail.surface ('call' | 'voice') and transcribe.js branches on it. huddle_ring.js's start listener guards on surface === 'voice', so a DM call no longer reaches it - matching the old lc:voice-joined-only trigger exactly.

AC4: one fewer global (two room globals to one).

Deliberately NOT converged

Per the ticket's own "forcing one abstraction would be worse":

  • Media operations. call.js drives one RTCPeerConnection; voice.js loops a mesh or delegates to the SFU. toggleMute/toggleCamera/toggleScreen differ by exactly that single-PC vs per-peer-loop vs SFU boundary. Merging them would push mesh/SFU branching into the DM path for no benefit.
  • The bus-drain (watchBus) and control-click-dispatch machinery. These are textbook duplication and safe to extract, but doing so means a new shared-loaded JS file, and bundling that into an untested-client refactor adds blast radius for little value. Filed as LC-616 so the remaining intent is tracked, not dropped.

Verification (no JS test harness)

  • AC2/AC3 (both paths behave identically): the DM-path contract is pinned by the LC-614 server tests, which assert the WS frames. This change does not alter any frame, and they stay green.
  • No orphans: every old global/event name is gone from live code - grep finds them only in explanatory comments.
  • All four files parse (node --check).
  • An adversarial review verified each behaviour-preservation claim: the surface-branch in the ended handler, the single started attach being equivalent to the old two, the huddle-ring surface guard, the dedup global swap (DM and group room ids are disjoint, and the server excludes voice channels from huddle rings), and symmetric set/null on every enter/exit path.

just check clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5

Workstream 4 of LC-596: converge `call.js` and `voice.js` "where practical". This does the session-lifecycle convergence (the ticket's headline) and leaves the media layer surface-specific by design. ## The duplication this removes `call.js` (1:1, one `RTCPeerConnection`) and `voice.js` (huddle, mesh or SFU) each published their own real-time-session state, and `transcribe.js` reconciled the two surfaces. Two concrete things were duplicated: **Two session-room globals.** `__lcCallRoom` and `__lcVoiceRoom` never coexisted (you are in one real-time session at a time), had identical set-on-enter / null-on-exit lifecycles, and their only shared reader already coalesced them with `||`. They become one `__lcSessionRoom`, so `transcribe.js`'s resolution drops from `data-lc-room || __lcCallRoom || __lcVoiceRoom` to `data-lc-room || __lcSessionRoom`. That reconciliation was the ticket's opening complaint. **Two lifecycle event pairs.** `lc:call-active` / `lc:voice-joined` -> `lc:rtc-session-started`; `lc:call-ended` / `lc:voice-left` -> `lc:rtc-session-ended`. `transcribe.js`'s four listeners collapse to two. ## The one real difference, kept via a flag not two names A 1:1 call is sole-participant, so its end must finalize the shared transcript for everyone; a huddle end must only stop *our* capture, because the session continues for whoever is still on the line. Rather than keep two event names to encode that, both events carry `detail.surface` ('call' | 'voice') and `transcribe.js` branches on it. `huddle_ring.js`'s start listener guards on `surface === 'voice'`, so a DM call no longer reaches it - matching the old `lc:voice-joined`-only trigger exactly. AC4: one fewer global (two room globals to one). ## Deliberately NOT converged Per the ticket's own "forcing one abstraction would be worse": - **Media operations.** `call.js` drives one `RTCPeerConnection`; `voice.js` loops a mesh or delegates to the SFU. `toggleMute`/`toggleCamera`/`toggleScreen` differ by exactly that single-PC vs per-peer-loop vs SFU boundary. Merging them would push mesh/SFU branching into the DM path for no benefit. - **The bus-drain (`watchBus`) and control-click-dispatch machinery.** These *are* textbook duplication and safe to extract, but doing so means a new shared-loaded JS file, and bundling that into an untested-client refactor adds blast radius for little value. Filed as **LC-616** so the remaining intent is tracked, not dropped. ## Verification (no JS test harness) - **AC2/AC3 (both paths behave identically):** the DM-path contract is pinned by the LC-614 server tests, which assert the WS frames. This change does not alter any frame, and they stay green. - **No orphans:** every old global/event name is gone from live code - grep finds them only in explanatory comments. - All four files parse (`node --check`). - An adversarial review verified each behaviour-preservation claim: the surface-branch in the ended handler, the single started attach being equivalent to the old two, the huddle-ring surface guard, the dedup global swap (DM and group room ids are disjoint, and the server excludes voice channels from huddle rings), and symmetric set/null on every enter/exit path. `just check` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
refactor(rtc): unify the call/huddle session lifecycle across the clients (LC-613)
All checks were successful
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / Kingfisher (push) Successful in 6s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 6s
check-secrets / TruffleHog (pull_request) Successful in 8s
Check / clippy + fmt + tests (pull_request) Successful in 5m49s
Create release / Create release from merged PR (pull_request) Has been skipped
2035f1b763
Workstream 4 of LC-596, the "converge where practical" cut. call.js (1:1, single peer connection) and voice.js (huddle, mesh or SFU) duplicated the real-time-session plumbing, and transcribe.js had to reconcile the two surfaces. This unifies the parts that are genuinely one thing and leaves the parts that are genuinely two.

Converged:
- The two session-room globals `__lcCallRoom` and `__lcVoiceRoom` become one `__lcSessionRoom`. They never coexisted (you are in one real-time session at a time), had identical set-on-enter / null-on-exit lifecycles, and their only shared reader (transcribe.js) already coalesced them with `||`. That reconciliation is now gone: `data-lc-room || __lcCallRoom || __lcVoiceRoom` is `data-lc-room || __lcSessionRoom`.
- The two lifecycle event pairs become one: `lc:call-active` / `lc:voice-joined` -> `lc:rtc-session-started`, and `lc:call-ended` / `lc:voice-left` -> `lc:rtc-session-ended`. Both carry `detail.surface` ('call' | 'voice'). transcribe.js's four listeners collapse to two.

The one behaviour that genuinely differs is preserved via `detail.surface`, not by two event names: a 1:1 call is sole-participant, so its end finalizes the shared transcript for everyone; a huddle end only stops the local capture, because the session continues for whoever is still on the line. transcribe.js branches on the surface to keep both. huddle_ring.js's start listener guards on `surface === 'voice'` so a DM call no longer reaches it, matching the old `lc:voice-joined`-only trigger exactly.

Net globals: one fewer (two room globals to one), satisfying AC4.

Deliberately NOT converged, per the map's verdicts and the ticket's own "forcing one abstraction would be worse":
- The media operations. call.js drives one `RTCPeerConnection`; voice.js loops over a mesh or delegates to the SFU. `toggleMute`/`toggleCamera`/`toggleScreen` differ by exactly that single-PC vs per-peer-loop vs SFU boundary; merging them would push mesh/SFU branching into the DM path for nothing.
- The bus-drain (`watchBus`) and control-bar click-dispatch machinery. These are textbook duplication (identical but for a few string tokens) and safe to extract into a shared module, but bundling a new shared-loaded file into untested client JS adds blast radius for little value. Left as a tracked mechanical follow-up.

Verification: no JS test harness exists, so the DM-path contract is pinned by the LC-614 server tests (green: they assert the WS frames, which this does not change), every old global/event name is gone from live code (grep-confirmed; only comments reference them), all four files parse, and an adversarial review confirmed each behaviour-preservation claim - the surface-branch in the ended handler, the single started attach, the huddle-ring surface guard, the dedup global swap, and symmetric set/null on every path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhDVMViNHqUbwmfe2aXTv5
longjacksonle scheduled this pull request to auto merge when all checks succeed 2026-07-21 03:00:05 +02:00
longjacksonle deleted branch refactor/lc613-converge-rtc-clients 2026-07-21 03:05:02 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!574
No description provided.