Skip to content

Commit c615207

Browse files
committed
fix(nextcloud-talk): cap room info cache size
1 parent 60f0749 commit c615207

2 files changed

Lines changed: 63 additions & 4 deletions

File tree

extensions/nextcloud-talk/src/room-info.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
33
import { tmpdir } from "node:os";
44
import path from "node:path";
55
import { afterEach, describe, expect, it, vi } from "vitest";
6-
import { resolveNextcloudTalkRoomKind, testing } from "./room-info.js";
6+
import { ROOM_CACHE_MAX_ENTRIES, resolveNextcloudTalkRoomKind, testing } from "./room-info.js";
77

88
const fetchWithSsrFGuard = vi.hoisted(() => vi.fn());
99
const tempDirs: string[] = [];
@@ -79,6 +79,46 @@ describe("nextcloud talk room info", () => {
7979
expect(release).toHaveBeenCalledTimes(1);
8080
});
8181

82+
it("caps cached room info entries", async () => {
83+
fetchWithSsrFGuard.mockImplementation(async () => ({
84+
response: jsonResponse({
85+
ocs: {
86+
data: {
87+
type: 1,
88+
},
89+
},
90+
}),
91+
release: vi.fn(async () => {}),
92+
}));
93+
const account = {
94+
accountId: "acct-cache-cap",
95+
baseUrl: "https://nc.example.com",
96+
config: {
97+
apiUser: "bot",
98+
apiPassword: "secret",
99+
},
100+
} as never;
101+
102+
for (let index = 0; index <= ROOM_CACHE_MAX_ENTRIES; index += 1) {
103+
await resolveNextcloudTalkRoomKind({
104+
account,
105+
roomToken: `room-${index}`,
106+
});
107+
}
108+
await resolveNextcloudTalkRoomKind({ account, roomToken: "room-0" });
109+
const callsAfterOldestRetry = fetchWithSsrFGuard.mock.calls.length;
110+
await resolveNextcloudTalkRoomKind({
111+
account,
112+
roomToken: `room-${ROOM_CACHE_MAX_ENTRIES}`,
113+
});
114+
115+
expect(callsAfterOldestRetry).toBe(ROOM_CACHE_MAX_ENTRIES + 2);
116+
expect(fetchWithSsrFGuard.mock.calls).toHaveLength(callsAfterOldestRetry);
117+
expect(fetchWithSsrFGuard.mock.calls.at(-1)?.[0]).toMatchObject({
118+
url: "https://nc.example.com/ocs/v2.php/apps/spreed/api/v4/room/room-0",
119+
});
120+
});
121+
82122
it("normalizes signed decimal room type strings through the shared parser", async () => {
83123
fetchWithSsrFGuard.mockResolvedValue({
84124
response: jsonResponse({

extensions/nextcloud-talk/src/room-info.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { resolveNextcloudTalkApiCredentials } from "./api-credentials.js";
99

1010
const ROOM_CACHE_TTL_MS = 5 * 60 * 1000;
1111
const ROOM_CACHE_ERROR_TTL_MS = 30 * 1000;
12+
export const ROOM_CACHE_MAX_ENTRIES = 1000;
1213

1314
const roomCache = new Map<
1415
string,
@@ -25,6 +26,24 @@ function resolveRoomCacheKey(params: { accountId: string; roomToken: string }) {
2526
return `${params.accountId}:${params.roomToken}`;
2627
}
2728

29+
function cacheRoomInfo(
30+
key: string,
31+
value: { kind?: "direct" | "group"; fetchedAt: number; error?: string },
32+
): void {
33+
roomCache.set(key, value);
34+
trimRoomCache();
35+
}
36+
37+
function trimRoomCache(): void {
38+
while (roomCache.size > ROOM_CACHE_MAX_ENTRIES) {
39+
const oldestKey = roomCache.keys().next().value;
40+
if (oldestKey === undefined) {
41+
return;
42+
}
43+
roomCache.delete(oldestKey);
44+
}
45+
}
46+
2847
function coerceRoomType(value: unknown): number | undefined {
2948
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
3049
return value;
@@ -96,7 +115,7 @@ export async function resolveNextcloudTalkRoomKind(params: {
96115
});
97116
try {
98117
if (!response.ok) {
99-
roomCache.set(key, {
118+
cacheRoomInfo(key, {
100119
fetchedAt: Date.now(),
101120
error: `status:${response.status}`,
102121
});
@@ -111,13 +130,13 @@ export async function resolveNextcloudTalkRoomKind(params: {
111130
}>(response, "Nextcloud Talk room info failed");
112131
const type = coerceRoomType(payload.ocs?.data?.type);
113132
const kind = resolveRoomKindFromType(type);
114-
roomCache.set(key, { fetchedAt: Date.now(), kind });
133+
cacheRoomInfo(key, { fetchedAt: Date.now(), kind });
115134
return kind;
116135
} finally {
117136
await release();
118137
}
119138
} catch (err) {
120-
roomCache.set(key, {
139+
cacheRoomInfo(key, {
121140
fetchedAt: Date.now(),
122141
error: formatErrorMessage(err),
123142
});

0 commit comments

Comments
 (0)