perf(notifications): memoize getNotificationType per push-rules object and room

Fixes #77

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-12 20:28:41 -04:00
co-authored by Claude Opus 5
parent e2e9f75e40
commit d280f66d5c
2 changed files with 76 additions and 4 deletions
+39 -1
View File
@@ -24,6 +24,7 @@ import {
getOrphanParents,
isMutedRule,
findMutedRule,
getNotificationType,
isNotificationEvent,
isVerificationFlowEvent,
unreadIsOnlyVerification,
@@ -46,7 +47,7 @@ import {
getMentionContent,
getAllVersionsRoomCreator,
} from './room';
import { RoomType, StateEvent, RoomToParents } from '../../types/matrix/room';
import { RoomType, StateEvent, RoomToParents, NotificationType } from '../../types/matrix/room';
// --- Mock helpers ---------------------------------------------------------
@@ -341,6 +342,43 @@ test('findMutedRule finds rule by id that is also muted', () => {
assert.equal(findMutedRule(rules, '!missing'), undefined);
});
// --- getNotificationType (memoization, #77) -------------------------------
test('getNotificationType memoizes per push-rules content object and room id', () => {
let scanCalls = 0;
const overrideRules = [{ rule_id: '!muted', actions: [], conditions: [{ kind: 'event_match' }] }];
const pushRulesContent = { global: { override: overrideRules } };
const mx = {
getRoomPushRule: () => {
scanCalls += 1;
return undefined;
},
getAccountData: () => ({ getContent: () => pushRulesContent }),
} as never;
assert.equal(getNotificationType(mx, '!muted'), NotificationType.Mute);
assert.equal(getNotificationType(mx, '!muted'), NotificationType.Mute);
assert.equal(getNotificationType(mx, '!muted'), NotificationType.Mute);
// Repeated calls with the same (content object, roomId) pair must hit the
// cache instead of re-running getRoomPushRule + the override scan.
assert.equal(scanCalls, 1);
// A different room id under the same push-rules content is a separate cache
// entry and still gets computed.
assert.equal(getNotificationType(mx, '!other'), NotificationType.Default);
assert.equal(scanCalls, 2);
// A new push-rules content object (simulating an `m.push_rules` account-data
// update) invalidates the cache and forces a recompute.
const newContent = { global: { override: overrideRules } };
const mx2 = {
getRoomPushRule: mx.getRoomPushRule,
getAccountData: () => ({ getContent: () => newContent }),
} as never;
assert.equal(getNotificationType(mx2, '!muted'), NotificationType.Mute);
assert.equal(scanCalls, 3);
});
// --- isNotificationEvent --------------------------------------------------
test('isNotificationEvent accepts message/sticker but rejects member, redacted, edits', () => {