Matrix: bound room metadata caches

This commit is contained in:
Gustavo Madeira Santana
2026-03-12 08:45:37 +00:00
parent d67a601051
commit feddcb4f82
4 changed files with 119 additions and 2 deletions

View File

@@ -152,4 +152,23 @@ describe("createDirectRoomTracker", () => {
}),
).resolves.toBe(false);
});
it("bounds joined-room membership cache size", async () => {
const client = createMockClient({ isDm: false });
const tracker = createDirectRoomTracker(client);
for (let i = 0; i <= 1024; i += 1) {
await tracker.isDirectMessage({
roomId: `!room-${i}:example.org`,
senderId: "@alice:example.org",
});
}
await tracker.isDirectMessage({
roomId: "!room-0:example.org",
senderId: "@alice:example.org",
});
expect(client.getJoinedRoomMembers).toHaveBeenCalledTimes(1026);
});
});

View File

@@ -11,6 +11,17 @@ type DirectRoomTrackerOptions = {
};
const DM_CACHE_TTL_MS = 30_000;
const MAX_TRACKED_DM_ROOMS = 1024;
function rememberBounded<T>(map: Map<string, T>, key: string, value: T): void {
map.set(key, value);
if (map.size > MAX_TRACKED_DM_ROOMS) {
const oldest = map.keys().next().value;
if (typeof oldest === "string") {
map.delete(oldest);
}
}
}
export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTrackerOptions = {}) {
const log = opts.log ?? (() => {});
@@ -55,7 +66,7 @@ export function createDirectRoomTracker(client: MatrixClient, opts: DirectRoomTr
.filter((entry): entry is string => typeof entry === "string")
.map((entry) => entry.trim())
.filter(Boolean);
joinedMembersCache.set(roomId, { members: normalized, ts: now });
rememberBounded(joinedMembersCache, roomId, { members: normalized, ts: now });
return normalized;
} catch (err) {
log(`matrix: dm member lookup failed room=${roomId} (${String(err)})`);

View File

@@ -0,0 +1,62 @@
import { describe, expect, it, vi } from "vitest";
import type { MatrixClient } from "../sdk.js";
import { createMatrixRoomInfoResolver } from "./room-info.js";
function createClientStub() {
return {
getRoomStateEvent: vi.fn(
async (
roomId: string,
eventType: string,
stateKey: string,
): Promise<Record<string, unknown>> => {
if (eventType === "m.room.name") {
return { name: `Room ${roomId}` };
}
if (eventType === "m.room.canonical_alias") {
return {
alias: `#alias-${roomId}:example.org`,
alt_aliases: [`#alt-${roomId}:example.org`],
};
}
if (eventType === "m.room.member") {
return { displayname: `Display ${roomId}:${stateKey}` };
}
return {};
},
),
} as unknown as MatrixClient & {
getRoomStateEvent: ReturnType<typeof vi.fn>;
};
}
describe("createMatrixRoomInfoResolver", () => {
it("caches room info and member display names", async () => {
const client = createClientStub();
const resolver = createMatrixRoomInfoResolver(client);
await resolver.getRoomInfo("!room:example.org");
await resolver.getRoomInfo("!room:example.org");
await resolver.getMemberDisplayName("!room:example.org", "@alice:example.org");
await resolver.getMemberDisplayName("!room:example.org", "@alice:example.org");
expect(client.getRoomStateEvent).toHaveBeenCalledTimes(3);
});
it("bounds cached room and member entries", async () => {
const client = createClientStub();
const resolver = createMatrixRoomInfoResolver(client);
for (let i = 0; i <= 1024; i += 1) {
await resolver.getRoomInfo(`!room-${i}:example.org`);
}
await resolver.getRoomInfo("!room-0:example.org");
for (let i = 0; i <= 4096; i += 1) {
await resolver.getMemberDisplayName("!room:example.org", `@user-${i}:example.org`);
}
await resolver.getMemberDisplayName("!room:example.org", "@user-0:example.org");
expect(client.getRoomStateEvent).toHaveBeenCalledTimes(6150);
});
});

View File

@@ -6,8 +6,22 @@ export type MatrixRoomInfo = {
altAliases: string[];
};
const MAX_TRACKED_ROOM_INFO = 1024;
const MAX_TRACKED_MEMBER_DISPLAY_NAMES = 4096;
function rememberBounded<T>(map: Map<string, T>, key: string, value: T, maxEntries: number): void {
map.set(key, value);
if (map.size > maxEntries) {
const oldest = map.keys().next().value;
if (typeof oldest === "string") {
map.delete(oldest);
}
}
}
export function createMatrixRoomInfoResolver(client: MatrixClient) {
const roomInfoCache = new Map<string, MatrixRoomInfo>();
const memberDisplayNameCache = new Map<string, string>();
const getRoomInfo = async (roomId: string): Promise<MatrixRoomInfo> => {
const cached = roomInfoCache.get(roomId);
@@ -40,16 +54,27 @@ export function createMatrixRoomInfoResolver(client: MatrixClient) {
// ignore
}
const info = { name, canonicalAlias, altAliases };
roomInfoCache.set(roomId, info);
rememberBounded(roomInfoCache, roomId, info, MAX_TRACKED_ROOM_INFO);
return info;
};
const getMemberDisplayName = async (roomId: string, userId: string): Promise<string> => {
const cacheKey = `${roomId}:${userId}`;
const cached = memberDisplayNameCache.get(cacheKey);
if (cached) {
return cached;
}
try {
const memberState = await client
.getRoomStateEvent(roomId, "m.room.member", userId)
.catch(() => null);
if (memberState && typeof memberState.displayname === "string") {
rememberBounded(
memberDisplayNameCache,
cacheKey,
memberState.displayname,
MAX_TRACKED_MEMBER_DISPLAY_NAMES,
);
return memberState.displayname;
}
return userId;