Skip to content

Commit 89d6552

Browse files
committed
refactor: dedupe extension runtime caches
1 parent f095bbd commit 89d6552

8 files changed

Lines changed: 89 additions & 195 deletions

File tree

extensions/discord/src/components-registry.ts

Lines changed: 39 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
1+
import { resolveGlobalMap } from "openclaw/plugin-sdk/text-runtime";
12
import type { DiscordComponentEntry, DiscordModalEntry } from "./components.js";
23

34
const DEFAULT_COMPONENT_TTL_MS = 30 * 60 * 1000;
45
const DISCORD_COMPONENT_ENTRIES_KEY = Symbol.for("openclaw.discord.componentEntries");
56
const DISCORD_MODAL_ENTRIES_KEY = Symbol.for("openclaw.discord.modalEntries");
67

7-
function resolveGlobalMap<TKey, TValue>(key: symbol): Map<TKey, TValue> {
8-
const globalStore = globalThis as Record<PropertyKey, unknown>;
9-
if (globalStore[key] instanceof Map) {
10-
return globalStore[key] as Map<TKey, TValue>;
11-
}
12-
const created = new Map<TKey, TValue>();
13-
globalStore[key] = created;
14-
return created;
15-
}
16-
178
const componentEntries = resolveGlobalMap<string, DiscordComponentEntry>(
189
DISCORD_COMPONENT_ENTRIES_KEY,
1910
);
@@ -33,68 +24,66 @@ function normalizeEntryTimestamps<T extends { createdAt?: number; expiresAt?: nu
3324
return { ...entry, createdAt, expiresAt };
3425
}
3526

36-
export function registerDiscordComponentEntries(params: {
37-
entries: DiscordComponentEntry[];
38-
modals: DiscordModalEntry[];
39-
ttlMs?: number;
40-
messageId?: string;
41-
}): void {
42-
const now = Date.now();
43-
const ttlMs = params.ttlMs ?? DEFAULT_COMPONENT_TTL_MS;
44-
for (const entry of params.entries) {
27+
function registerEntries<
28+
T extends { id: string; messageId?: string; createdAt?: number; expiresAt?: number },
29+
>(
30+
entries: T[],
31+
store: Map<string, T>,
32+
params: { now: number; ttlMs: number; messageId?: string },
33+
): void {
34+
for (const entry of entries) {
4535
const normalized = normalizeEntryTimestamps(
4636
{ ...entry, messageId: params.messageId ?? entry.messageId },
47-
now,
48-
ttlMs,
37+
params.now,
38+
params.ttlMs,
4939
);
50-
componentEntries.set(entry.id, normalized);
51-
}
52-
for (const modal of params.modals) {
53-
const normalized = normalizeEntryTimestamps(
54-
{ ...modal, messageId: params.messageId ?? modal.messageId },
55-
now,
56-
ttlMs,
57-
);
58-
modalEntries.set(modal.id, normalized);
40+
store.set(entry.id, normalized);
5941
}
6042
}
6143

62-
export function resolveDiscordComponentEntry(params: {
63-
id: string;
64-
consume?: boolean;
65-
}): DiscordComponentEntry | null {
66-
const entry = componentEntries.get(params.id);
44+
function resolveEntry<T extends { expiresAt?: number }>(
45+
store: Map<string, T>,
46+
params: { id: string; consume?: boolean },
47+
): T | null {
48+
const entry = store.get(params.id);
6749
if (!entry) {
6850
return null;
6951
}
7052
const now = Date.now();
7153
if (isExpired(entry, now)) {
72-
componentEntries.delete(params.id);
54+
store.delete(params.id);
7355
return null;
7456
}
7557
if (params.consume !== false) {
76-
componentEntries.delete(params.id);
58+
store.delete(params.id);
7759
}
7860
return entry;
7961
}
8062

63+
export function registerDiscordComponentEntries(params: {
64+
entries: DiscordComponentEntry[];
65+
modals: DiscordModalEntry[];
66+
ttlMs?: number;
67+
messageId?: string;
68+
}): void {
69+
const now = Date.now();
70+
const ttlMs = params.ttlMs ?? DEFAULT_COMPONENT_TTL_MS;
71+
registerEntries(params.entries, componentEntries, { now, ttlMs, messageId: params.messageId });
72+
registerEntries(params.modals, modalEntries, { now, ttlMs, messageId: params.messageId });
73+
}
74+
75+
export function resolveDiscordComponentEntry(params: {
76+
id: string;
77+
consume?: boolean;
78+
}): DiscordComponentEntry | null {
79+
return resolveEntry(componentEntries, params);
80+
}
81+
8182
export function resolveDiscordModalEntry(params: {
8283
id: string;
8384
consume?: boolean;
8485
}): DiscordModalEntry | null {
85-
const entry = modalEntries.get(params.id);
86-
if (!entry) {
87-
return null;
88-
}
89-
const now = Date.now();
90-
if (isExpired(entry, now)) {
91-
modalEntries.delete(params.id);
92-
return null;
93-
}
94-
if (params.consume !== false) {
95-
modalEntries.delete(params.id);
96-
}
97-
return entry;
86+
return resolveEntry(modalEntries, params);
9887
}
9988

10089
export function clearDiscordComponentEntries(): void {

extensions/feishu/src/thread-bindings.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,15 @@ type FeishuThreadBindingsState = {
5252
};
5353

5454
const FEISHU_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.feishuThreadBindingsState");
55-
let state: FeishuThreadBindingsState | undefined;
55+
const state = resolveGlobalSingleton<FeishuThreadBindingsState>(
56+
FEISHU_THREAD_BINDINGS_STATE_KEY,
57+
() => ({
58+
managersByAccountId: new Map(),
59+
bindingsByAccountConversation: new Map(),
60+
}),
61+
);
5662

5763
function getState(): FeishuThreadBindingsState {
58-
state ??= resolveGlobalSingleton<FeishuThreadBindingsState>(
59-
FEISHU_THREAD_BINDINGS_STATE_KEY,
60-
() => ({
61-
managersByAccountId: new Map(),
62-
bindingsByAccountConversation: new Map(),
63-
}),
64-
);
6564
return state;
6665
}
6766

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,23 @@
1-
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
2-
3-
type CacheEntry = {
4-
timestamps: Map<string, number>;
5-
};
6-
7-
const sentMessages = new Map<string, CacheEntry>();
1+
import { createScopedExpiringIdCache } from "openclaw/plugin-sdk/text-runtime";
82

9-
function cleanupExpired(entry: CacheEntry): void {
10-
const now = Date.now();
11-
for (const [msgId, timestamp] of entry.timestamps) {
12-
if (now - timestamp > TTL_MS) {
13-
entry.timestamps.delete(msgId);
14-
}
15-
}
16-
}
3+
const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
4+
const sentMessageCache = createScopedExpiringIdCache<string, string>({
5+
store: new Map<string, Map<string, number>>(),
6+
ttlMs: TTL_MS,
7+
cleanupThreshold: 200,
8+
});
179

1810
export function recordMSTeamsSentMessage(conversationId: string, messageId: string): void {
1911
if (!conversationId || !messageId) {
2012
return;
2113
}
22-
let entry = sentMessages.get(conversationId);
23-
if (!entry) {
24-
entry = { timestamps: new Map() };
25-
sentMessages.set(conversationId, entry);
26-
}
27-
entry.timestamps.set(messageId, Date.now());
28-
if (entry.timestamps.size > 200) {
29-
cleanupExpired(entry);
30-
}
14+
sentMessageCache.record(conversationId, messageId);
3115
}
3216

3317
export function wasMSTeamsMessageSent(conversationId: string, messageId: string): boolean {
34-
const entry = sentMessages.get(conversationId);
35-
if (!entry) {
36-
return false;
37-
}
38-
cleanupExpired(entry);
39-
return entry.timestamps.has(messageId);
18+
return sentMessageCache.has(conversationId, messageId);
4019
}
4120

4221
export function clearMSTeamsSentMessageCache(): void {
43-
sentMessages.clear();
22+
sentMessageCache.clear();
4423
}
Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resolveGlobalMap } from "openclaw/plugin-sdk/text-runtime";
1+
import { resolveGlobalDedupeCache } from "openclaw/plugin-sdk/infra-runtime";
22

33
/**
44
* In-memory cache of Slack threads the bot has participated in.
@@ -14,34 +14,15 @@ const MAX_ENTRIES = 5000;
1414
* auto-reply gating does not diverge between prepare/dispatch call paths.
1515
*/
1616
const SLACK_THREAD_PARTICIPATION_KEY = Symbol.for("openclaw.slackThreadParticipation");
17-
18-
let threadParticipation: Map<string, number> | undefined;
19-
20-
function getThreadParticipation(): Map<string, number> {
21-
threadParticipation ??= resolveGlobalMap<string, number>(SLACK_THREAD_PARTICIPATION_KEY);
22-
return threadParticipation;
23-
}
17+
const threadParticipation = resolveGlobalDedupeCache(SLACK_THREAD_PARTICIPATION_KEY, {
18+
ttlMs: TTL_MS,
19+
maxSize: MAX_ENTRIES,
20+
});
2421

2522
function makeKey(accountId: string, channelId: string, threadTs: string): string {
2623
return `${accountId}:${channelId}:${threadTs}`;
2724
}
2825

29-
function evictExpired(): void {
30-
const now = Date.now();
31-
for (const [key, timestamp] of getThreadParticipation()) {
32-
if (now - timestamp > TTL_MS) {
33-
getThreadParticipation().delete(key);
34-
}
35-
}
36-
}
37-
38-
function evictOldest(): void {
39-
const oldest = getThreadParticipation().keys().next().value;
40-
if (oldest) {
41-
getThreadParticipation().delete(oldest);
42-
}
43-
}
44-
4526
export function recordSlackThreadParticipation(
4627
accountId: string,
4728
channelId: string,
@@ -50,14 +31,7 @@ export function recordSlackThreadParticipation(
5031
if (!accountId || !channelId || !threadTs) {
5132
return;
5233
}
53-
const threadParticipation = getThreadParticipation();
54-
if (threadParticipation.size >= MAX_ENTRIES) {
55-
evictExpired();
56-
}
57-
if (threadParticipation.size >= MAX_ENTRIES) {
58-
evictOldest();
59-
}
60-
threadParticipation.set(makeKey(accountId, channelId, threadTs), Date.now());
34+
threadParticipation.check(makeKey(accountId, channelId, threadTs));
6135
}
6236

6337
export function hasSlackThreadParticipation(
@@ -68,19 +42,9 @@ export function hasSlackThreadParticipation(
6842
if (!accountId || !channelId || !threadTs) {
6943
return false;
7044
}
71-
const key = makeKey(accountId, channelId, threadTs);
72-
const threadParticipation = getThreadParticipation();
73-
const timestamp = threadParticipation.get(key);
74-
if (timestamp == null) {
75-
return false;
76-
}
77-
if (Date.now() - timestamp > TTL_MS) {
78-
threadParticipation.delete(key);
79-
return false;
80-
}
81-
return true;
45+
return threadParticipation.peek(makeKey(accountId, channelId, threadTs));
8246
}
8347

8448
export function clearSlackThreadParticipationCache(): void {
85-
getThreadParticipation().clear();
49+
threadParticipation.clear();
8650
}

extensions/telegram/src/draft-stream.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,11 @@ type TelegramSendMessageDraft = (
2727
* lanes do not accidentally reuse draft ids when code-split entries coexist.
2828
*/
2929
const TELEGRAM_DRAFT_STREAM_STATE_KEY = Symbol.for("openclaw.telegramDraftStreamState");
30-
31-
let draftStreamState: { nextDraftId: number } | undefined;
32-
33-
function getDraftStreamState(): { nextDraftId: number } {
34-
draftStreamState ??= resolveGlobalSingleton(TELEGRAM_DRAFT_STREAM_STATE_KEY, () => ({
35-
nextDraftId: 0,
36-
}));
37-
return draftStreamState;
38-
}
30+
const draftStreamState = resolveGlobalSingleton(TELEGRAM_DRAFT_STREAM_STATE_KEY, () => ({
31+
nextDraftId: 0,
32+
}));
3933

4034
function allocateTelegramDraftId(): number {
41-
const draftStreamState = getDraftStreamState();
4235
draftStreamState.nextDraftId =
4336
draftStreamState.nextDraftId >= TELEGRAM_DRAFT_ID_MAX ? 1 : draftStreamState.nextDraftId + 1;
4437
return draftStreamState.nextDraftId;
@@ -460,6 +453,6 @@ export function createTelegramDraftStream(params: {
460453

461454
export const __testing = {
462455
resetTelegramDraftStreamForTests() {
463-
getDraftStreamState().nextDraftId = 0;
456+
draftStreamState.nextDraftId = 0;
464457
},
465458
};

0 commit comments

Comments
 (0)