diff --git a/extensions/line/src/send.test.ts b/extensions/line/src/send.test.ts index 4ad6d0795318..b787beaef3e8 100644 --- a/extensions/line/src/send.test.ts +++ b/extensions/line/src/send.test.ts @@ -515,6 +515,19 @@ describe("LINE send helpers", () => { expect(getProfileMock).toHaveBeenCalledTimes(1); }); + it("bounds profile cache entries across distinct users", async () => { + getProfileMock.mockImplementation(async (userId: string) => ({ + displayName: userId, + })); + + for (let index = 0; index <= 1000; index += 1) { + await sendModule.getUserProfile(`U-profile-${index}`, { cfg: LINE_TEST_CFG }); + } + await sendModule.getUserProfile("U-profile-0", { cfg: LINE_TEST_CFG }); + + expect(getProfileMock).toHaveBeenCalledTimes(1002); + }); + it("continues when loading animation is unsupported", async () => { showLoadingAnimationMock.mockRejectedValueOnce(new Error("unsupported")); diff --git a/extensions/line/src/send.ts b/extensions/line/src/send.ts index 7b223abfbe34..b80e35a11dac 100644 --- a/extensions/line/src/send.ts +++ b/extensions/line/src/send.ts @@ -1,6 +1,7 @@ // Line plugin module implements send behavior. import { messagingApi } from "@line/bot-sdk"; import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime"; +import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime"; import { logVerbose } from "openclaw/plugin-sdk/runtime-env"; @@ -29,6 +30,25 @@ const userProfileCache = new Map< { displayName: string; pictureUrl?: string; fetchedAt: number } >(); const PROFILE_CACHE_TTL_MS = 5 * 60 * 1000; +const PROFILE_CACHE_MAX_ENTRIES = 1000; + +function cacheUserProfile( + userId: string, + profile: { displayName: string; pictureUrl?: string; fetchedAt: number }, +): void { + // Refresh insertion order so overflow evicts expired entries first, then the oldest live fetch. + userProfileCache.delete(userId); + userProfileCache.set(userId, profile); + if (userProfileCache.size <= PROFILE_CACHE_MAX_ENTRIES) { + return; + } + for (const [key, cached] of userProfileCache) { + if (profile.fetchedAt - cached.fetchedAt >= PROFILE_CACHE_TTL_MS) { + userProfileCache.delete(key); + } + } + pruneMapToMaxSize(userProfileCache, PROFILE_CACHE_MAX_ENTRIES); +} interface LineSendOpts { cfg: OpenClawConfig; @@ -511,7 +531,7 @@ export async function getUserProfile( pictureUrl: profile.pictureUrl, }; - userProfileCache.set(userId, { + cacheUserProfile(userId, { ...result, fetchedAt: Date.now(), });