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>
68 lines
2.2 KiB
TypeScript
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);
|
|
});
|