/** * In-memory counter store with TTL windows for pattern detection. * Windows: 'today' resets at midnight, 'week' resets Monday 00:00, 'month' resets 1st 00:00. */ // store[window][namespace][key] = count const store = { today: new Map(), week: new Map(), month: new Map() }; function getNamespaceMap(window, namespace) { const windowMap = store[window]; if (!windowMap) return null; if (!windowMap.has(namespace)) windowMap.set(namespace, new Map()); return windowMap.get(namespace); } function increment(namespace, key, window) { const map = getNamespaceMap(window, namespace); if (!map) return; map.set(key, (map.get(key) || 0) + 1); } function get(namespace, key, window) { const map = getNamespaceMap(window, namespace); if (!map) return 0; return map.get(key) || 0; } function reset(namespace, window) { const windowMap = store[window]; if (!windowMap) return; windowMap.delete(namespace); } function getAll(namespace, window) { const map = getNamespaceMap(window, namespace); if (!map) return new Map(); return new Map(map); } // --- Scheduled resets --- function msUntilNextMidnight() { const now = new Date(); const next = new Date(now); next.setHours(24, 0, 0, 0); return next.getTime() - now.getTime(); } function msUntilNextMonday() { const now = new Date(); const day = now.getDay(); // 0=Sun const daysUntilMonday = day === 0 ? 1 : (8 - day); const next = new Date(now); next.setDate(now.getDate() + daysUntilMonday); next.setHours(0, 0, 0, 0); return next.getTime() - now.getTime(); } function msUntilNextMonth() { const now = new Date(); const next = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 0, 0, 0); return next.getTime() - now.getTime(); } // Callbacks to run on daily reset (e.g. clear firedToday in patternChecker) const dailyResetCallbacks = []; function onDailyReset(fn) { dailyResetCallbacks.push(fn); } function scheduleDailyReset() { setTimeout(() => { store.today = new Map(); for (const fn of dailyResetCallbacks) { try { fn(); } catch (_) {} } scheduleDailyReset(); }, msUntilNextMidnight()); } function scheduleWeeklyReset() { setTimeout(() => { store.week = new Map(); scheduleWeeklyReset(); }, msUntilNextMonday()); } function scheduleMonthlyReset() { setTimeout(() => { store.month = new Map(); scheduleMonthlyReset(); }, msUntilNextMonth()); } function scheduleResets() { scheduleDailyReset(); scheduleWeeklyReset(); scheduleMonthlyReset(); } // --- Cooldown store --- const cooldowns = new Map(); function setCooldown(key) { cooldowns.set(key, Date.now()); } function isOnCooldown(key, cooldownMinutes) { const last = cooldowns.get(key); if (!last) return false; return (Date.now() - last) < cooldownMinutes * 60 * 1000; } // --- Staff last-seen tracker (fallback for missing presence intent) --- const staffLastSeen = new Map(); function updateStaffLastSeen(staffId) { staffLastSeen.set(staffId, Date.now()); } function getStaffLastSeen(staffId) { return staffLastSeen.get(staffId) || null; } function isStaffRecentlyActive(staffId, withinMinutes = 60) { const last = staffLastSeen.get(staffId); if (!last) return false; return (Date.now() - last) < withinMinutes * 60 * 1000; } module.exports = { increment, get, reset, getAll, scheduleResets, onDailyReset, setCooldown, isOnCooldown, updateStaffLastSeen, getStaffLastSeen, isStaffRecentlyActive };