/** * Deduplication manager — prevents processing the same event twice * within a configurable window. Matches Rust `DeduplicationManager` * which uses Redis with 5-minute TTL. */ import { DEDUP_WINDOW_MS } from './constants'; import type { Uuid } from './types/core'; interface DedupEntry { timestamp: number; } export class DedupManager { private entries: Map = new Map(); private windowMs = DEDUP_WINDOW_MS; private cleanupInterval: ReturnType | null = null; constructor() { // Periodic cleanup of expired entries (every 30s) this.cleanupInterval = setInterval(() => this.cleanup(), 30_000); } /** Check if a message is a duplicate. Returns false if new, true if duplicate. */ checkAndMark(messageId: Uuid, roomId: Uuid): boolean { const key = `${roomId}:${messageId}`; const existing = this.entries.get(key); if (existing) return true; // duplicate this.entries.set(key, { timestamp: Date.now() }); return false; // not a duplicate } /** Check if a message is already known (without marking it). */ isDuplicate(messageId: Uuid, roomId: Uuid): boolean { const key = `${roomId}:${messageId}`; return this.entries.has(key); } /** Remove expired entries. */ private cleanup(): void { const now = Date.now(); for (const [key, entry] of this.entries) { if (now - entry.timestamp > this.windowMs) { this.entries.delete(key); } } } /** Clear all dedup entries. */ clear(): void { this.entries.clear(); } /** Stop the cleanup interval. */ destroy(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.entries.clear(); } /** Get current entry count. */ get size(): number { return this.entries.size; } }