Skip to content

Commit 4c79e1c

Browse files
committed
fix(channels): consolidate bounded metadata caches
1 parent e842a83 commit 4c79e1c

5 files changed

Lines changed: 62 additions & 25 deletions

File tree

extensions/discord/src/monitor/message-channel-info.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Discord plugin module implements message channel info behavior.
2+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
23
import {
34
asDateTimestampMs,
45
resolveExpiresAtMsFromDurationMs,
@@ -26,7 +27,7 @@ type DiscordMessageWithChannelId = Message & {
2627

2728
const DISCORD_CHANNEL_INFO_CACHE_TTL_MS = 5 * 60 * 1000;
2829
const DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS = 30 * 1000;
29-
export const DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES = 1000;
30+
const DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES = 1000;
3031
const DISCORD_CHANNEL_INFO_CACHE = new Map<
3132
string,
3233
{ value: DiscordChannelInfo | null; expiresAt: number }
@@ -49,17 +50,7 @@ function cacheDiscordChannelInfo(
4950
const expiresAt = resolveDiscordChannelInfoCacheExpiresAt(ttlMs, nowMs);
5051
if (expiresAt !== undefined) {
5152
DISCORD_CHANNEL_INFO_CACHE.set(channelId, { value, expiresAt });
52-
trimDiscordChannelInfoCache();
53-
}
54-
}
55-
56-
function trimDiscordChannelInfoCache(): void {
57-
while (DISCORD_CHANNEL_INFO_CACHE.size > DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES) {
58-
const oldestKey = DISCORD_CHANNEL_INFO_CACHE.keys().next().value;
59-
if (oldestKey === undefined) {
60-
return;
61-
}
62-
DISCORD_CHANNEL_INFO_CACHE.delete(oldestKey);
53+
pruneMapToMaxSize(DISCORD_CHANNEL_INFO_CACHE, DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES);
6354
}
6455
}
6556

extensions/discord/src/monitor/message-utils.test.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ vi.mock("openclaw/plugin-sdk/runtime-env", async () => {
4747
});
4848

4949
let resetDiscordChannelInfoCacheForTest: typeof import("./message-utils.js").resetDiscordChannelInfoCacheForTest;
50-
let DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES: typeof import("./message-utils.js").DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES;
5150
let resolveDiscordChannelInfo: typeof import("./message-utils.js").resolveDiscordChannelInfo;
5251
let resolveDiscordMessageChannelId: typeof import("./message-utils.js").resolveDiscordMessageChannelId;
5352
let resolveDiscordMessageText: typeof import("./message-utils.js").resolveDiscordMessageText;
@@ -57,7 +56,6 @@ let resolveReferencedReplyMediaList: typeof import("./message-utils.js").resolve
5756

5857
beforeAll(async () => {
5958
({
60-
DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES,
6159
resetDiscordChannelInfoCacheForTest,
6260
resolveDiscordChannelInfo,
6361
resolveDiscordMessageChannelId,
@@ -1228,23 +1226,21 @@ describe("resolveDiscordChannelInfo", () => {
12281226
});
12291227

12301228
it("caps cached channel info entries", async () => {
1229+
const cacheEntryLimit = 1000;
12311230
const fetchChannel = vi.fn(async (channelId: string) => ({
12321231
type: ChannelType.GuildText,
12331232
name: `name-${channelId}`,
12341233
}));
12351234
const client = { fetchChannel } as unknown as Client;
12361235

1237-
for (let index = 0; index <= DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES; index += 1) {
1236+
for (let index = 0; index <= cacheEntryLimit; index += 1) {
12381237
await resolveDiscordChannelInfo(client, `channel-${index}`);
12391238
}
12401239
await resolveDiscordChannelInfo(client, "channel-0");
1241-
await resolveDiscordChannelInfo(client, `channel-${DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES}`);
1240+
await resolveDiscordChannelInfo(client, `channel-${cacheEntryLimit}`);
12421241

1243-
expect(fetchChannel).toHaveBeenCalledTimes(DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES + 2);
1244-
expect(fetchChannel).toHaveBeenNthCalledWith(
1245-
DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES + 2,
1246-
"channel-0",
1247-
);
1242+
expect(fetchChannel).toHaveBeenCalledTimes(cacheEntryLimit + 2);
1243+
expect(fetchChannel).toHaveBeenNthCalledWith(cacheEntryLimit + 2, "channel-0");
12481244
});
12491245

12501246
it("negative-caches missing channels", async () => {

extensions/discord/src/monitor/message-utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Discord helper module supports message utils behavior.
22
export {
3-
DISCORD_CHANNEL_INFO_CACHE_MAX_ENTRIES,
43
resetDiscordChannelInfoCacheForTest,
54
resolveDiscordChannelInfo,
65
resolveDiscordMessageChannelId,

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,47 @@ describe("nextcloud talk room info", () => {
7979
expect(release).toHaveBeenCalledTimes(1);
8080
});
8181

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

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Nextcloud Talk plugin module implements room info behavior.
2+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
23
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
34
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
45
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
@@ -9,6 +10,7 @@ import { resolveNextcloudTalkApiCredentials } from "./api-credentials.js";
910

1011
const ROOM_CACHE_TTL_MS = 5 * 60 * 1000;
1112
const ROOM_CACHE_ERROR_TTL_MS = 30 * 1000;
13+
const ROOM_CACHE_MAX_ENTRIES = 1000;
1214

1315
const roomCache = new Map<
1416
string,
@@ -25,6 +27,14 @@ function resolveRoomCacheKey(params: { accountId: string; roomToken: string }) {
2527
return `${params.accountId}:${params.roomToken}`;
2628
}
2729

30+
function cacheRoomInfo(
31+
key: string,
32+
value: { kind?: "direct" | "group"; fetchedAt: number; error?: string },
33+
): void {
34+
roomCache.set(key, value);
35+
pruneMapToMaxSize(roomCache, ROOM_CACHE_MAX_ENTRIES);
36+
}
37+
2838
function coerceRoomType(value: unknown): number | undefined {
2939
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) {
3040
return value;
@@ -96,7 +106,7 @@ export async function resolveNextcloudTalkRoomKind(params: {
96106
});
97107
try {
98108
if (!response.ok) {
99-
roomCache.set(key, {
109+
cacheRoomInfo(key, {
100110
fetchedAt: Date.now(),
101111
error: `status:${response.status}`,
102112
});
@@ -111,13 +121,13 @@ export async function resolveNextcloudTalkRoomKind(params: {
111121
}>(response, "Nextcloud Talk room info failed");
112122
const type = coerceRoomType(payload.ocs?.data?.type);
113123
const kind = resolveRoomKindFromType(type);
114-
roomCache.set(key, { fetchedAt: Date.now(), kind });
124+
cacheRoomInfo(key, { fetchedAt: Date.now(), kind });
115125
return kind;
116126
} finally {
117127
await release();
118128
}
119129
} catch (err) {
120-
roomCache.set(key, {
130+
cacheRoomInfo(key, {
121131
fetchedAt: Date.now(),
122132
error: formatErrorMessage(err),
123133
});

0 commit comments

Comments
 (0)