Files
cinny/src/app/hooks/useRoomMemberChange.test.ts
T
jaredandClaude Opus 4.8 1b8f554584 perf(receipts): shared member-change store instead of per-row listeners (PERF-3)
Every ReadReceiptAvatars row and every useMemberAvatar registered its own global
RoomStateEvent.Members listener — ~6 per receipt row — each firing on any
membership / display-name / avatar change in ANY room.

Add a module-level MemberChangeStore (mirroring the PERF-1 presence store) that
registers exactly ONE global Members listener and fans out to subscribers keyed
by roomId|userId. Two hooks: useRoomMemberChange (single) and
useRoomMembersChange (multi, one effect). useMemberAvatar and ReadReceiptAvatars
use them; behavior (re-render triggers) is byte-for-byte equivalent. Unsubscribe
is idempotent via a set-identity guard; the multi-hook key is order-independent.
Unit-tested (key-scoped fan-out, single shared listener, idempotent unsubscribe).

Reviewed by two passes (lifecycle/closure + behavioral equivalence) — clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:10:33 -04:00

68 lines
2.2 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import type { MatrixClient } from 'matrix-js-sdk';
import { memberChangeStore } from './useRoomMemberChange';
// Minimal fake client: records registered handlers and can emit a member change.
// The store registers exactly one RoomStateEvent.Members handler; we invoke it
// with the same (event, state, member) shape the SDK uses.
const makeFakeMx = () => {
const handlers: Array<(e: unknown, s: unknown, m: unknown) => void> = [];
const mx = {
on: (_event: unknown, h: (e: unknown, s: unknown, m: unknown) => void) => {
handlers.push(h);
},
removeListener: (_event: unknown, h: (e: unknown, s: unknown, m: unknown) => void) => {
const i = handlers.indexOf(h);
if (i >= 0) handlers.splice(i, 1);
},
};
return {
mx: mx as unknown as MatrixClient,
listenerCount: () => handlers.length,
emit: (roomId: string, userId: string) =>
handlers.forEach((h) => h({ getRoomId: () => roomId }, undefined, { userId })),
};
};
test('fans out only to the matching roomId|userId', () => {
const { mx, emit } = makeFakeMx();
let a = 0;
let b = 0;
memberChangeStore.subscribe(mx, '!fanout', '@a', () => {
a += 1;
});
memberChangeStore.subscribe(mx, '!fanout', '@b', () => {
b += 1;
});
emit('!fanout', '@a');
assert.equal(a, 1);
assert.equal(b, 0);
emit('!other', '@a'); // different room, same user -> no fire
assert.equal(a, 1);
});
test('registers exactly one client listener regardless of subscriber count', () => {
const { mx, listenerCount } = makeFakeMx();
memberChangeStore.subscribe(mx, '!one', '@a', () => {});
memberChangeStore.subscribe(mx, '!one', '@b', () => {});
memberChangeStore.subscribe(mx, '!one2', '@c', () => {});
assert.equal(listenerCount(), 1);
});
test('idempotent unsubscribe does not evict a re-subscribed key', () => {
const { mx, emit } = makeFakeMx();
const unsubA = memberChangeStore.subscribe(mx, '!idem', '@a', () => {});
unsubA();
let count = 0;
memberChangeStore.subscribe(mx, '!idem', '@a', () => {
count += 1;
});
unsubA(); // double-invoke of the first unsub must not drop the new subscriber
emit('!idem', '@a');
assert.equal(count, 1);
});