perf(calls): speaker set only updates when it changes; DOM fallback detaches once the fork streams
CI / Build & Quality Checks (push) Successful in 1m31s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 15s
CI / Playwright smoke (e2e) (push) Successful in 2m22s
CI / Build & Quality Checks (push) Successful in 1m31s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 15s
CI / Playwright smoke (e2e) (push) Successful in 2m22s
Every io.lotus.call_state push allocated a new Set, re-rendering the app-wide call bar for the whole call. nextSpeakerSet() returns the previous reference when membership is unchanged (pure helpers in utils/speakerSet.ts, unit-tested), and the DOM MutationObserver fallback in useCallSpeakers/useRemoteAllMuted is attached only while the fork's participant list is unavailable. Fixes #32 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { nextSpeakerSet, sameSet } from './speakerSet';
|
||||
|
||||
test('sameSet: identity, size and membership', () => {
|
||||
const a = new Set(['@a:x', '@b:x']);
|
||||
assert.equal(sameSet(a, a), true);
|
||||
assert.equal(sameSet(a, new Set(['@b:x', '@a:x'])), true);
|
||||
assert.equal(sameSet(a, new Set(['@a:x'])), false);
|
||||
assert.equal(sameSet(a, new Set(['@a:x', '@c:x'])), false);
|
||||
});
|
||||
|
||||
test('nextSpeakerSet returns the SAME reference when speakers are unchanged (#32)', () => {
|
||||
const prev = new Set(['@a:x']);
|
||||
const next = nextSpeakerSet(prev, [
|
||||
{ userId: '@a:x', speaking: true },
|
||||
{ userId: '@b:x', speaking: false },
|
||||
]);
|
||||
assert.equal(next, prev);
|
||||
});
|
||||
|
||||
test('nextSpeakerSet returns a new set when speakers change, ignoring non-user ids', () => {
|
||||
const prev = new Set(['@a:x']);
|
||||
const next = nextSpeakerSet(prev, [
|
||||
{ userId: '@a:x', speaking: false },
|
||||
{ userId: '@b:x', speaking: true },
|
||||
{ userId: 'not-a-user-id', speaking: true },
|
||||
]);
|
||||
assert.notEqual(next, prev);
|
||||
assert.deepEqual(Array.from(next), ['@b:x']);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { isUserId } from './matrix';
|
||||
|
||||
/** Minimal shape of a `io.lotus.call_state` participant entry we need here. */
|
||||
export type SpeakingParticipant = { userId: string; speaking: boolean };
|
||||
|
||||
/** True when both sets contain exactly the same members. */
|
||||
export const sameSet = <T>(a: Set<T>, b: Set<T>): boolean => {
|
||||
if (a === b) return true;
|
||||
if (a.size !== b.size) return false;
|
||||
for (const v of a) {
|
||||
if (!b.has(v)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Derives the speaking-users set from a `io.lotus.call_state` participant
|
||||
* list. Returns `prev` unchanged (same reference) when the derived set is
|
||||
* equal to it, so callers using the functional `setState` form can bail out
|
||||
* of a re-render (#32 — the fork pushes this multiple times per second).
|
||||
*/
|
||||
export const nextSpeakerSet = (
|
||||
prev: Set<string>,
|
||||
participants: readonly SpeakingParticipant[],
|
||||
): Set<string> => {
|
||||
const next = new Set<string>();
|
||||
participants.forEach((p) => {
|
||||
if (p.speaking && isUserId(p.userId)) next.add(p.userId);
|
||||
});
|
||||
return sameSet(prev, next) ? prev : next;
|
||||
};
|
||||
Reference in New Issue
Block a user