Files
Moltbot/src/infra/dedupe.ts
2026-02-22 10:46:34 +01:00

80 lines
1.8 KiB
TypeScript

import { pruneMapToMaxSize } from "./map-size.js";
export type DedupeCache = {
check: (key: string | undefined | null, now?: number) => boolean;
peek: (key: string | undefined | null, now?: number) => boolean;
clear: () => void;
size: () => number;
};
type DedupeCacheOptions = {
ttlMs: number;
maxSize: number;
};
export function createDedupeCache(options: DedupeCacheOptions): DedupeCache {
const ttlMs = Math.max(0, options.ttlMs);
const maxSize = Math.max(0, Math.floor(options.maxSize));
const cache = new Map<string, number>();
const touch = (key: string, now: number) => {
cache.delete(key);
cache.set(key, now);
};
const prune = (now: number) => {
const cutoff = ttlMs > 0 ? now - ttlMs : undefined;
if (cutoff !== undefined) {
for (const [entryKey, entryTs] of cache) {
if (entryTs < cutoff) {
cache.delete(entryKey);
}
}
}
if (maxSize <= 0) {
cache.clear();
return;
}
pruneMapToMaxSize(cache, maxSize);
};
const hasUnexpired = (key: string, now: number, touchOnRead: boolean): boolean => {
const existing = cache.get(key);
if (existing === undefined) {
return false;
}
if (ttlMs > 0 && now - existing >= ttlMs) {
cache.delete(key);
return false;
}
if (touchOnRead) {
touch(key, now);
}
return true;
};
return {
check: (key, now = Date.now()) => {
if (!key) {
return false;
}
if (hasUnexpired(key, now, true)) {
return true;
}
touch(key, now);
prune(now);
return false;
},
peek: (key, now = Date.now()) => {
if (!key) {
return false;
}
return hasUnexpired(key, now, false);
},
clear: () => {
cache.clear();
},
size: () => cache.size,
};
}