Skip to content

Commit 56aa859

Browse files
fix(slack): bound conversation info cache
1 parent 1aeed5e commit 56aa859

2 files changed

Lines changed: 81 additions & 7 deletions

File tree

extensions/slack/src/channel-type.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,55 @@ describe("resolveSlackChannelType", () => {
178178
expect(conversationsInfoMock).not.toHaveBeenCalled();
179179
});
180180

181+
it("evicts least-recently-used conversation info entries after the cache limit", async () => {
182+
const cacheMaxEntries = 1024;
183+
const cfg = {
184+
channels: {
185+
slack: {
186+
botToken: "xoxb-test",
187+
},
188+
},
189+
} as never;
190+
191+
conversationsInfoMock.mockImplementation(async ({ channel }) => ({
192+
channel: {
193+
id: channel,
194+
},
195+
}));
196+
197+
for (let index = 0; index < cacheMaxEntries; index++) {
198+
await resolveSlackConversationInfo({
199+
cfg,
200+
channelId: `C${index.toString().padStart(8, "0")}`,
201+
});
202+
}
203+
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries);
204+
205+
await resolveSlackConversationInfo({
206+
cfg,
207+
channelId: "C00000000",
208+
});
209+
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries);
210+
211+
await resolveSlackConversationInfo({
212+
cfg,
213+
channelId: `C${cacheMaxEntries.toString().padStart(8, "0")}`,
214+
});
215+
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries + 1);
216+
217+
await resolveSlackConversationInfo({
218+
cfg,
219+
channelId: "C00000001",
220+
});
221+
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries + 2);
222+
223+
await resolveSlackConversationInfo({
224+
cfg,
225+
channelId: "C00000000",
226+
});
227+
expect(conversationsInfoMock).toHaveBeenCalledTimes(cacheMaxEntries + 2);
228+
});
229+
181230
it("preserves the channel-type wrapper contract", async () => {
182231
conversationsInfoMock.mockResolvedValueOnce({
183232
channel: {

extensions/slack/src/channel-type.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,33 @@ export type SlackConversationInfo = {
1313
user?: string;
1414
};
1515

16+
const SLACK_CONVERSATION_INFO_CACHE_MAX_ENTRIES = 1024;
1617
const SLACK_CONVERSATION_INFO_CACHE = new Map<string, SlackConversationInfo>();
1718

19+
function getCachedSlackConversationInfo(cacheKey: string): SlackConversationInfo | undefined {
20+
const cached = SLACK_CONVERSATION_INFO_CACHE.get(cacheKey);
21+
if (cached) {
22+
SLACK_CONVERSATION_INFO_CACHE.delete(cacheKey);
23+
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, cached);
24+
}
25+
return cached;
26+
}
27+
28+
function setCachedSlackConversationInfo(
29+
cacheKey: string,
30+
conversationInfo: SlackConversationInfo,
31+
): void {
32+
if (SLACK_CONVERSATION_INFO_CACHE.has(cacheKey)) {
33+
SLACK_CONVERSATION_INFO_CACHE.delete(cacheKey);
34+
} else if (SLACK_CONVERSATION_INFO_CACHE.size >= SLACK_CONVERSATION_INFO_CACHE_MAX_ENTRIES) {
35+
const oldest = SLACK_CONVERSATION_INFO_CACHE.keys().next().value;
36+
if (typeof oldest === "string") {
37+
SLACK_CONVERSATION_INFO_CACHE.delete(oldest);
38+
}
39+
}
40+
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, conversationInfo);
41+
}
42+
1843
export async function resolveSlackConversationInfo(params: {
1944
cfg: OpenClawConfig;
2045
accountId?: string | null;
@@ -26,7 +51,7 @@ export async function resolveSlackConversationInfo(params: {
2651
}
2752
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
2853
const cacheKey = `${account.accountId}:${channelId}`;
29-
const cached = SLACK_CONVERSATION_INFO_CACHE.get(cacheKey);
54+
const cached = getCachedSlackConversationInfo(cacheKey);
3055
if (cached) {
3156
return cached;
3257
}
@@ -42,7 +67,7 @@ export async function resolveSlackConversationInfo(params: {
4267
groupChannels.includes(`mpim:${channelIdLower}`))
4368
) {
4469
const result = { type: "group" } as const;
45-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
70+
setCachedSlackConversationInfo(cacheKey, result);
4671
return result;
4772
}
4873

@@ -59,7 +84,7 @@ export async function resolveSlackConversationInfo(params: {
5984
})
6085
) {
6186
const result = { type: "channel" } as const;
62-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
87+
setCachedSlackConversationInfo(cacheKey, result);
6388
return result;
6489
}
6590

@@ -70,7 +95,7 @@ export async function resolveSlackConversationInfo(params: {
7095
if (!token) {
7196
const result = { type: isNativeImChannel ? "dm" : "unknown" } as const;
7297
if (!isNativeImChannel) {
73-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
98+
setCachedSlackConversationInfo(cacheKey, result);
7499
}
75100
return result;
76101
}
@@ -89,20 +114,20 @@ export async function resolveSlackConversationInfo(params: {
89114
: undefined;
90115
const result: SlackConversationInfo = user ? { type: "dm", user } : { type: "dm" };
91116
if (user) {
92-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
117+
setCachedSlackConversationInfo(cacheKey, result);
93118
}
94119
return result;
95120
}
96121
const info = await client.conversations.info({ channel: channelId });
97122
const channel = info.channel as { is_im?: boolean; is_mpim?: boolean } | undefined;
98123
const type = channel?.is_im ? "dm" : channel?.is_mpim ? "group" : "channel";
99124
const result = { type } as const;
100-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
125+
setCachedSlackConversationInfo(cacheKey, result);
101126
return result;
102127
} catch {
103128
const result = { type: isNativeImChannel ? "dm" : "unknown" } as const;
104129
if (!isNativeImChannel) {
105-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
130+
setCachedSlackConversationInfo(cacheKey, result);
106131
}
107132
return result;
108133
}

0 commit comments

Comments
 (0)