fix(lotus): Wave-1 audit fixes (EC1–EC6)
CI / Build embedded bundle (push) Successful in 1m0s
CI / Publish to Gitea npm registry (push) Has been skipped

- EC1: lotusQuality — track + clearTimeout the 500ms settle re-apply per room
  (was leaking a timer that fired on torn-down rooms).
- EC2/EC3: lotusQuality + lotusAudioInject drive off vm.allConnections$ instead
  of the remote-gated livekitRoomItems$ (were no-ops when alone), matching
  lotusDenoise.
- EC4: lotusDecorations resets its roster to {} on teardown so a decoration from
  a previous call can't render on a shared user in the next one.
- EC5: hoisted a stable useSyncExternalStore subscribe fn (was re-subscribing
  every tile render).
- EC6: lotusFocus only sets the spotlight when the userId field is present
  (a partial payload no longer clears the pin).

tsc clean. Needs a republish to ship.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lotus CI
2026-07-02 20:13:01 -04:00
co-authored by Claude Opus 4.8
parent 02666c0c04
commit 0ffe247929
4 changed files with 52 additions and 17 deletions
+25 -3
View File
@@ -48,6 +48,10 @@ export function startLotusQuality(vm: CallViewModel): () => void {
// Per-room LocalTrackPublished listeners, so sticky settings re-apply on
// every (re)publish.
const roomListeners = new Map<LivekitRoom, () => void>();
// Per-room settle re-apply timers, so we can cancel a pending 500ms re-apply
// when a room is removed or on teardown — otherwise it would fire against a
// torn-down room.
const settleTimers = new Map<LivekitRoom, ReturnType<typeof setTimeout>>();
let rooms: LivekitRoom[] = [];
const applyToRoom = (room: LivekitRoom): void => {
@@ -76,8 +80,12 @@ export function startLotusQuality(vm: CallViewModel): () => void {
const applyToAll = (): void => rooms.forEach(applyToRoom);
// Keep the LocalTrackPublished listeners in sync with the connected rooms.
const sub = vm.livekitRoomItems$.subscribe((items) => {
const next = items.map((i) => i.livekitRoom);
// Drive off the LOCAL participant's connection(s), not `livekitRoomItems$` —
// that stream omits rooms with no remote members (returns null for isLocal),
// so caps wouldn't apply to the local senders while you're alone. Map the
// connections to their livekit rooms exactly like `lotusDenoise.ts` does.
const sub = vm.allConnections$.subscribe((data) => {
const next = data.getConnections().map((c) => c.livekitRoom);
rooms = next;
// Remove listeners for rooms that went away.
for (const [room, off] of roomListeners) {
@@ -97,7 +105,18 @@ export function startLotusQuality(vm: CallViewModel): () => void {
// recompute so our cap wins.
const reapply = (): void => {
applyToRoom(room);
setTimeout(() => applyToRoom(room), 500);
// Store the settle timer per room and cancel any pending one, so it
// can be cleared on removal/teardown and never fires against a
// torn-down room.
const prev = settleTimers.get(room);
if (prev !== undefined) clearTimeout(prev);
settleTimers.set(
room,
setTimeout(() => {
settleTimers.delete(room);
applyToRoom(room);
}, 500),
);
};
const events = [
ParticipantEvent.LocalTrackPublished,
@@ -106,6 +125,9 @@ export function startLotusQuality(vm: CallViewModel): () => void {
for (const e of events) room.localParticipant.on(e, reapply);
roomListeners.set(room, () => {
for (const e of events) room.localParticipant.off(e, reapply);
const t = settleTimers.get(room);
if (t !== undefined) clearTimeout(t);
settleTimers.delete(room);
});
applyToRoom(room);
}