fix(lotus): call_state skips standalone mode; dedupe before throttle

startLotusCallState gated only on the URL flag, so a standalone (non-widget)
load built the whole per-member pipeline and stringified it at up to 8 Hz
for a host that doesn't exist. Add the same `if (!widget)` guard the sibling
modules use. Also move distinctUntilChanged ahead of throttleTime so an
unchanged value no longer spends a throttle window (first half of #20).

Fixes #31

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Lotus CI
2026-09-12 11:34:08 -04:00
co-authored by Claude Opus 5
parent 37c9348ee8
commit fd957badff
+21 -6
View File
@@ -6,9 +6,15 @@ Please see LICENSE in the repository root for full details.
*/ */
import { combineLatest, of, type Subscription } from "rxjs"; import { combineLatest, of, type Subscription } from "rxjs";
import { distinctUntilChanged, map, switchMap, throttleTime } from "rxjs/operators"; import {
distinctUntilChanged,
map,
switchMap,
throttleTime,
} from "rxjs/operators";
import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { type CallViewModel } from "../state/CallViewModel/CallViewModel";
import { widget } from "../widget";
import { LotusWidgetActions, lotusFlag, lotusSendToHost } from "./lotusWidget"; import { LotusWidgetActions, lotusFlag, lotusSendToHost } from "./lotusWidget";
interface ParticipantState { interface ParticipantState {
@@ -32,6 +38,10 @@ interface ParticipantState {
*/ */
export function startLotusCallState(vm: CallViewModel): () => void { export function startLotusCallState(vm: CallViewModel): () => void {
if (!lotusFlag("lotusCallState")) return () => undefined; if (!lotusFlag("lotusCallState")) return () => undefined;
// [lotus] Standalone (non-widget) mode has no host to send state to;
// skip building the whole stream pipeline, mirroring lotusFocus.ts /
// lotusDecorations.ts.
if (!widget) return () => undefined;
const sub: Subscription = vm.userMedia$ const sub: Subscription = vm.userMedia$
.pipe( .pipe(
@@ -46,7 +56,11 @@ export function startLotusCallState(vm: CallViewModel): () => void {
m.videoEnabled$, m.videoEnabled$,
]).pipe( ]).pipe(
map( map(
([speaking, audioEnabled, videoEnabled]): ParticipantState => ({ ([
speaking,
audioEnabled,
videoEnabled,
]): ParticipantState => ({
id: m.id, id: m.id,
userId: m.userId, userId: m.userId,
speaking, speaking,
@@ -58,11 +72,12 @@ export function startLotusCallState(vm: CallViewModel): () => void {
), ),
), ),
), ),
// `speaking` flips rapidly; cap the send rate and drop no-op repeats. // `speaking` flips rapidly; drop no-op repeats BEFORE throttling so
// 250ms is plenty for speaking rings / mute badges and keeps the // the throttle window isn't spent re-emitting an unchanged value, then
// request/response widget traffic modest. // cap the send rate. 250ms is plenty for speaking rings / mute badges
throttleTime(250, undefined, { leading: true, trailing: true }), // and keeps the request/response widget traffic modest.
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
throttleTime(250, undefined, { leading: true, trailing: true }),
) )
.subscribe((participants) => { .subscribe((participants) => {
lotusSendToHost(LotusWidgetActions.CallState, { participants }); lotusSendToHost(LotusWidgetActions.CallState, { participants });