mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
refactor(whatsapp): reuse SDK dedupe cache (#104962)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user