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>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user