refactor(whatsapp): reuse SDK dedupe cache (#104962)

This commit is contained in:
Marcus Castro
2026-07-12 14:15:53 -03:00
committed by GitHub
parent 02d307e1b8
commit 6787c2f653
3 changed files with 19 additions and 74 deletions
@@ -203,18 +203,23 @@ describe("applyGroupGating allowlist drop warning", () => {
expect(warn.mock.calls[1]?.[1]).toContain("b@g.us");
});
it("evicts old warning keys instead of growing without bound", async () => {
it("bounds warning keys by least-recently-used conversations", async () => {
const warn = vi.fn<WarnLogger>();
const apply = (conversationId: string) =>
applyGroupGating(makeParams(makeUnregisteredGroupMsg(conversationId), warn));
await applyGroupGating(makeParams(makeUnregisteredGroupMsg("evicted@g.us"), warn));
for (let index = 0; index < 100; index += 1) {
await applyGroupGating(makeParams(makeUnregisteredGroupMsg(`overflow-${index}@g.us`), warn));
await apply(`${index}@g.us`);
}
await applyGroupGating(makeParams(makeUnregisteredGroupMsg("evicted@g.us"), warn));
await apply("0@g.us");
await apply("100@g.us");
await apply("0@g.us");
await apply("1@g.us");
await apply("100@g.us");
expect(warn).toHaveBeenCalledTimes(102);
expect(warn.mock.calls[0]?.[1]).toContain("evicted@g.us");
expect(warn.mock.calls[101]?.[1]).toContain("evicted@g.us");
expect(warn.mock.calls[100]?.[1]).toContain("100@g.us");
expect(warn.mock.calls[101]?.[1]).toContain("1@g.us");
});
it("does not warn when the group is registered", async () => {
@@ -1,6 +1,7 @@
// Whatsapp plugin module implements group gating behavior.
import type { BuildMentionRegexesOptions } from "openclaw/plugin-sdk/channel-mention-gating";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
import { resolveWhatsAppGroupsConfigPath } from "../../group-config-path.js";
import {
getPrimaryIdentityId,
@@ -57,25 +58,17 @@ type ApplyGroupGatingParams = {
};
const MAX_GROUP_DROP_WARNINGS = 100;
const groupDropWarned = new Set<string>();
const groupDropWarned = createDedupeCache({
ttlMs: 0,
maxSize: MAX_GROUP_DROP_WARNINGS,
});
export function resetGroupDropWarningsForTests() {
groupDropWarned.clear();
}
function shouldWarnForGroupDrop(warnKey: string): boolean {
if (groupDropWarned.has(warnKey)) {
return false;
}
groupDropWarned.add(warnKey);
while (groupDropWarned.size > MAX_GROUP_DROP_WARNINGS) {
const oldest = groupDropWarned.values().next().value;
if (!oldest) {
break;
}
groupDropWarned.delete(oldest);
}
return true;
return !groupDropWarned.check(warnKey);
}
function isOwnerSender(
+2 -55
View File
@@ -1,4 +1,5 @@
// Whatsapp plugin module implements dedupe behavior.
import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
export const WHATSAPP_INBOUND_DEDUPE_TTL_MS = 20 * 60_000;
@@ -10,65 +11,11 @@ const claimableInboundMessages = createClaimableDedupe({
ttlMs: WHATSAPP_INBOUND_DEDUPE_TTL_MS,
memoryMaxSize: RECENT_WEB_MESSAGE_MAX,
});
const recentOutboundMessages = createRecentMessageCache({
const recentOutboundMessages = createDedupeCache({
ttlMs: RECENT_OUTBOUND_MESSAGE_TTL_MS,
maxSize: RECENT_OUTBOUND_MESSAGE_MAX,
});
function createRecentMessageCache(options: { ttlMs: number; maxSize: number }) {
const ttlMs = Math.max(0, options.ttlMs);
const maxSize = Math.max(0, Math.floor(options.maxSize));
const cache = new Map<string, number>();
const prune = (now: number) => {
if (ttlMs > 0) {
const cutoff = now - ttlMs;
for (const [key, timestamp] of cache) {
if (timestamp < cutoff) {
cache.delete(key);
}
}
}
while (cache.size > maxSize) {
const oldest = cache.keys().next().value;
if (!oldest) {
break;
}
cache.delete(oldest);
}
};
const peek = (key: string | null, now = Date.now()): boolean => {
if (!key) {
return false;
}
const timestamp = cache.get(key);
if (timestamp === undefined) {
return false;
}
if (ttlMs > 0 && now - timestamp >= ttlMs) {
cache.delete(key);
return false;
}
return true;
};
return {
check: (key: string | null, now = Date.now()): boolean => {
if (!key) {
return false;
}
const existed = peek(key, now);
cache.delete(key);
cache.set(key, now);
prune(now);
return existed;
},
peek,
clear: () => cache.clear(),
};
}
export class WhatsAppRetryableInboundError extends Error {
constructor(message: string, options?: ErrorOptions) {
super(message, options);