Skip to content

Commit 4e29b2f

Browse files
fix(slack): bound conversation info cache (#101562)
1 parent 4074432 commit 4e29b2f

2 files changed

Lines changed: 76 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: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Slack plugin module implements channel type behavior.
2+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
23
import {
34
normalizeLowercaseStringOrEmpty,
45
normalizeOptionalString,
@@ -13,8 +14,27 @@ export type SlackConversationInfo = {
1314
user?: string;
1415
};
1516

17+
const SLACK_CONVERSATION_INFO_CACHE_MAX_ENTRIES = 1024;
1618
const SLACK_CONVERSATION_INFO_CACHE = new Map<string, SlackConversationInfo>();
1719

20+
function getCachedSlackConversationInfo(cacheKey: string): SlackConversationInfo | undefined {
21+
const cached = SLACK_CONVERSATION_INFO_CACHE.get(cacheKey);
22+
if (cached) {
23+
SLACK_CONVERSATION_INFO_CACHE.delete(cacheKey);
24+
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, cached);
25+
}
26+
return cached;
27+
}
28+
29+
function setCachedSlackConversationInfo(
30+
cacheKey: string,
31+
conversationInfo: SlackConversationInfo,
32+
): void {
33+
SLACK_CONVERSATION_INFO_CACHE.delete(cacheKey);
34+
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, conversationInfo);
35+
pruneMapToMaxSize(SLACK_CONVERSATION_INFO_CACHE, SLACK_CONVERSATION_INFO_CACHE_MAX_ENTRIES);
36+
}
37+
1838
export async function resolveSlackConversationInfo(params: {
1939
cfg: OpenClawConfig;
2040
accountId?: string | null;
@@ -26,7 +46,7 @@ export async function resolveSlackConversationInfo(params: {
2646
}
2747
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
2848
const cacheKey = `${account.accountId}:${channelId}`;
29-
const cached = SLACK_CONVERSATION_INFO_CACHE.get(cacheKey);
49+
const cached = getCachedSlackConversationInfo(cacheKey);
3050
if (cached) {
3151
return cached;
3252
}
@@ -42,7 +62,7 @@ export async function resolveSlackConversationInfo(params: {
4262
groupChannels.includes(`mpim:${channelIdLower}`))
4363
) {
4464
const result = { type: "group" } as const;
45-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
65+
setCachedSlackConversationInfo(cacheKey, result);
4666
return result;
4767
}
4868

@@ -59,7 +79,7 @@ export async function resolveSlackConversationInfo(params: {
5979
})
6080
) {
6181
const result = { type: "channel" } as const;
62-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
82+
setCachedSlackConversationInfo(cacheKey, result);
6383
return result;
6484
}
6585

@@ -70,7 +90,7 @@ export async function resolveSlackConversationInfo(params: {
7090
if (!token) {
7191
const result = { type: isNativeImChannel ? "dm" : "unknown" } as const;
7292
if (!isNativeImChannel) {
73-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
93+
setCachedSlackConversationInfo(cacheKey, result);
7494
}
7595
return result;
7696
}
@@ -89,20 +109,20 @@ export async function resolveSlackConversationInfo(params: {
89109
: undefined;
90110
const result: SlackConversationInfo = user ? { type: "dm", user } : { type: "dm" };
91111
if (user) {
92-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
112+
setCachedSlackConversationInfo(cacheKey, result);
93113
}
94114
return result;
95115
}
96116
const info = await client.conversations.info({ channel: channelId });
97117
const channel = info.channel as { is_im?: boolean; is_mpim?: boolean } | undefined;
98118
const type = channel?.is_im ? "dm" : channel?.is_mpim ? "group" : "channel";
99119
const result = { type } as const;
100-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
120+
setCachedSlackConversationInfo(cacheKey, result);
101121
return result;
102122
} catch {
103123
const result = { type: isNativeImChannel ? "dm" : "unknown" } as const;
104124
if (!isNativeImChannel) {
105-
SLACK_CONVERSATION_INFO_CACHE.set(cacheKey, result);
125+
setCachedSlackConversationInfo(cacheKey, result);
106126
}
107127
return result;
108128
}

0 commit comments

Comments
 (0)