Skip to content

Commit 6787c2f

Browse files
authored
refactor(whatsapp): reuse SDK dedupe cache (#104962)
1 parent 02d307e commit 6787c2f

3 files changed

Lines changed: 19 additions & 74 deletions

File tree

extensions/whatsapp/src/auto-reply/monitor/group-gating.allowlist-warn.test.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,18 +203,23 @@ describe("applyGroupGating allowlist drop warning", () => {
203203
expect(warn.mock.calls[1]?.[1]).toContain("[email protected]");
204204
});
205205

206-
it("evicts old warning keys instead of growing without bound", async () => {
206+
it("bounds warning keys by least-recently-used conversations", async () => {
207207
const warn = vi.fn<WarnLogger>();
208+
const apply = (conversationId: string) =>
209+
applyGroupGating(makeParams(makeUnregisteredGroupMsg(conversationId), warn));
208210

209-
await applyGroupGating(makeParams(makeUnregisteredGroupMsg("[email protected]"), warn));
210211
for (let index = 0; index < 100; index += 1) {
211-
await applyGroupGating(makeParams(makeUnregisteredGroupMsg(`overflow-${index}@g.us`), warn));
212+
await apply(`${index}@g.us`);
212213
}
213-
await applyGroupGating(makeParams(makeUnregisteredGroupMsg("[email protected]"), warn));
214+
await apply("[email protected]");
215+
await apply("[email protected]");
216+
await apply("[email protected]");
217+
await apply("[email protected]");
218+
await apply("[email protected]");
214219

215220
expect(warn).toHaveBeenCalledTimes(102);
216-
expect(warn.mock.calls[0]?.[1]).toContain("evicted@g.us");
217-
expect(warn.mock.calls[101]?.[1]).toContain("evicted@g.us");
221+
expect(warn.mock.calls[100]?.[1]).toContain("100@g.us");
222+
expect(warn.mock.calls[101]?.[1]).toContain("1@g.us");
218223
});
219224

220225
it("does not warn when the group is registered", async () => {

extensions/whatsapp/src/auto-reply/monitor/group-gating.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Whatsapp plugin module implements group gating behavior.
22
import type { BuildMentionRegexesOptions } from "openclaw/plugin-sdk/channel-mention-gating";
33
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
4+
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
45
import { resolveWhatsAppGroupsConfigPath } from "../../group-config-path.js";
56
import {
67
getPrimaryIdentityId,
@@ -57,25 +58,17 @@ type ApplyGroupGatingParams = {
5758
};
5859

5960
const MAX_GROUP_DROP_WARNINGS = 100;
60-
const groupDropWarned = new Set<string>();
61+
const groupDropWarned = createDedupeCache({
62+
ttlMs: 0,
63+
maxSize: MAX_GROUP_DROP_WARNINGS,
64+
});
6165

6266
export function resetGroupDropWarningsForTests() {
6367
groupDropWarned.clear();
6468
}
6569

6670
function shouldWarnForGroupDrop(warnKey: string): boolean {
67-
if (groupDropWarned.has(warnKey)) {
68-
return false;
69-
}
70-
groupDropWarned.add(warnKey);
71-
while (groupDropWarned.size > MAX_GROUP_DROP_WARNINGS) {
72-
const oldest = groupDropWarned.values().next().value;
73-
if (!oldest) {
74-
break;
75-
}
76-
groupDropWarned.delete(oldest);
77-
}
78-
return true;
71+
return !groupDropWarned.check(warnKey);
7972
}
8073

8174
function isOwnerSender(

extensions/whatsapp/src/inbound/dedupe.ts

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Whatsapp plugin module implements dedupe behavior.
2+
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
23
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
34

45
export const WHATSAPP_INBOUND_DEDUPE_TTL_MS = 20 * 60_000;
@@ -10,65 +11,11 @@ const claimableInboundMessages = createClaimableDedupe({
1011
ttlMs: WHATSAPP_INBOUND_DEDUPE_TTL_MS,
1112
memoryMaxSize: RECENT_WEB_MESSAGE_MAX,
1213
});
13-
const recentOutboundMessages = createRecentMessageCache({
14+
const recentOutboundMessages = createDedupeCache({
1415
ttlMs: RECENT_OUTBOUND_MESSAGE_TTL_MS,
1516
maxSize: RECENT_OUTBOUND_MESSAGE_MAX,
1617
});
1718

18-
function createRecentMessageCache(options: { ttlMs: number; maxSize: number }) {
19-
const ttlMs = Math.max(0, options.ttlMs);
20-
const maxSize = Math.max(0, Math.floor(options.maxSize));
21-
const cache = new Map<string, number>();
22-
23-
const prune = (now: number) => {
24-
if (ttlMs > 0) {
25-
const cutoff = now - ttlMs;
26-
for (const [key, timestamp] of cache) {
27-
if (timestamp < cutoff) {
28-
cache.delete(key);
29-
}
30-
}
31-
}
32-
while (cache.size > maxSize) {
33-
const oldest = cache.keys().next().value;
34-
if (!oldest) {
35-
break;
36-
}
37-
cache.delete(oldest);
38-
}
39-
};
40-
41-
const peek = (key: string | null, now = Date.now()): boolean => {
42-
if (!key) {
43-
return false;
44-
}
45-
const timestamp = cache.get(key);
46-
if (timestamp === undefined) {
47-
return false;
48-
}
49-
if (ttlMs > 0 && now - timestamp >= ttlMs) {
50-
cache.delete(key);
51-
return false;
52-
}
53-
return true;
54-
};
55-
56-
return {
57-
check: (key: string | null, now = Date.now()): boolean => {
58-
if (!key) {
59-
return false;
60-
}
61-
const existed = peek(key, now);
62-
cache.delete(key);
63-
cache.set(key, now);
64-
prune(now);
65-
return existed;
66-
},
67-
peek,
68-
clear: () => cache.clear(),
69-
};
70-
}
71-
7219
export class WhatsAppRetryableInboundError extends Error {
7320
constructor(message: string, options?: ErrorOptions) {
7421
super(message, options);

0 commit comments

Comments
 (0)