import { createHash } from "node:crypto"; import { formatIMessageChatTarget } from "../targets.js"; type SelfChatCacheKeyParts = { accountId: string; sender: string; isGroup: boolean; chatId?: number; }; export type SelfChatLookup = SelfChatCacheKeyParts & { text?: string; createdAt?: number; }; export type SelfChatCache = { remember: (lookup: SelfChatLookup) => void; has: (lookup: SelfChatLookup) => boolean; }; const SELF_CHAT_TTL_MS = 10_000; const MAX_SELF_CHAT_CACHE_ENTRIES = 512; const CLEANUP_MIN_INTERVAL_MS = 1_000; function normalizeText(text: string | undefined): string | null { if (!text) { return null; } const normalized = text.replace(/\r\n?/g, "\n").trim(); return normalized ? normalized : null; } function isUsableTimestamp(createdAt: number | undefined): createdAt is number { return typeof createdAt === "number" && Number.isFinite(createdAt); } function digestText(text: string): string { return createHash("sha256").update(text).digest("hex"); } function buildScope(parts: SelfChatCacheKeyParts): string { if (!parts.isGroup) { return `${parts.accountId}:imessage:${parts.sender}`; } const chatTarget = formatIMessageChatTarget(parts.chatId) || "chat_id:unknown"; return `${parts.accountId}:${chatTarget}:imessage:${parts.sender}`; } class DefaultSelfChatCache implements SelfChatCache { private cache = new Map(); private lastCleanupAt = 0; private buildKey(lookup: SelfChatLookup): string | null { const text = normalizeText(lookup.text); if (!text || !isUsableTimestamp(lookup.createdAt)) { return null; } return `${buildScope(lookup)}:${lookup.createdAt}:${digestText(text)}`; } remember(lookup: SelfChatLookup): void { const key = this.buildKey(lookup); if (!key) { return; } this.cache.set(key, Date.now()); this.maybeCleanup(); } has(lookup: SelfChatLookup): boolean { this.maybeCleanup(); const key = this.buildKey(lookup); if (!key) { return false; } const timestamp = this.cache.get(key); return typeof timestamp === "number" && Date.now() - timestamp <= SELF_CHAT_TTL_MS; } private maybeCleanup(): void { const now = Date.now(); if (now - this.lastCleanupAt < CLEANUP_MIN_INTERVAL_MS) { return; } this.lastCleanupAt = now; for (const [key, timestamp] of this.cache.entries()) { if (now - timestamp > SELF_CHAT_TTL_MS) { this.cache.delete(key); } } while (this.cache.size > MAX_SELF_CHAT_CACHE_ENTRIES) { const oldestKey = this.cache.keys().next().value; if (typeof oldestKey !== "string") { break; } this.cache.delete(oldestKey); } } } export function createSelfChatCache(): SelfChatCache { return new DefaultSelfChatCache(); }