This commit is contained in:
2026-04-20 18:05:36 +00:00
parent d73422555d
commit 33b1f276c6
26 changed files with 598 additions and 183 deletions

View File

@@ -83,12 +83,15 @@ function onWeeklyReset(fn) {
const firedThresholds = new Map();
// key -> window type used for threshold clearing ("today" | "week" | "month")
const firedThresholdWindows = new Map();
// key -> last-seen timestamp; drives periodic sweep for keys that outlive their window reset.
const firedThresholdLastSeen = new Map();
function clearFiredThresholdsForWindow(windowType) {
for (const [key, mappedWindowType] of firedThresholdWindows.entries()) {
if (mappedWindowType === windowType) {
firedThresholds.delete(key);
firedThresholdWindows.delete(key);
firedThresholdLastSeen.delete(key);
}
}
}
@@ -98,6 +101,7 @@ function shouldFireThreshold(key, ageMs, thresholdsMs, windowType) {
if (!['today', 'week', 'month'].includes(windowType)) return null;
firedThresholdWindows.set(key, windowType);
firedThresholdLastSeen.set(key, Date.now());
const firedForKey = firedThresholds.get(key) || new Set();
const sortedThresholds = [...thresholdsMs].sort((a, b) => a - b);
@@ -148,18 +152,49 @@ function clearEscalating(key) {
escalatingCooldowns.delete(key);
}
const ESCALATING_COOLDOWN_TTL_MS = 48 * 60 * 60 * 1000;
const ESCALATING_CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
const SWEEP_TTL_MS = 48 * 60 * 60 * 1000;
const SWEEP_INTERVAL_MS = 6 * 60 * 60 * 1000;
function cleanupStaleEscalatingCooldowns() {
const cutoff = Date.now() - ESCALATING_COOLDOWN_TTL_MS;
function cleanupStaleEscalatingCooldowns(now = Date.now()) {
const cutoff = now - SWEEP_TTL_MS;
for (const [key, state] of escalatingCooldowns.entries()) {
const lastUsed = state.lastUsed || state.lastFireAtMs || state.startedAtMs || 0;
if (lastUsed < cutoff) escalatingCooldowns.delete(key);
}
}
setInterval(cleanupStaleEscalatingCooldowns, ESCALATING_CLEANUP_INTERVAL_MS).unref?.();
// Sweep every per-Map timestamp-bearing entry older than SWEEP_TTL_MS.
// firedThresholds/firedThresholdWindows are cleared by windowType-resets;
// this sweep covers keys whose window never resets under load.
function sweepPatternStore(now = Date.now()) {
const cutoff = now - SWEEP_TTL_MS;
for (const [key, ts] of cooldowns.entries()) {
if (ts < cutoff) cooldowns.delete(key);
}
for (const [key, ts] of staffLastSeen.entries()) {
if (ts < cutoff) staffLastSeen.delete(key);
}
cleanupStaleEscalatingCooldowns(now);
for (const [key, ts] of firedThresholdLastSeen.entries()) {
if (ts < cutoff) {
firedThresholds.delete(key);
firedThresholdWindows.delete(key);
firedThresholdLastSeen.delete(key);
}
}
}
/**
* Register the module's sweep on the given trackInterval function.
* Called once from the ready handler. Interval is unref'd so it never
* blocks shutdown; trackInterval ensures handleShutdown clears it.
*/
function startSweeps(trackInterval) {
const handle = setInterval(() => sweepPatternStore(), SWEEP_INTERVAL_MS);
if (typeof handle.unref === 'function') handle.unref();
if (typeof trackInterval === 'function') trackInterval(handle);
return handle;
}
function scheduleDailyReset() {
setTimeout(() => {
@@ -243,5 +278,9 @@ module.exports = {
isOnCooldown,
updateStaffLastSeen,
getStaffLastSeen,
isStaffRecentlyActive
isStaffRecentlyActive,
startSweeps,
sweepPatternStore,
// test-only exports
_internals: { cooldowns, staffLastSeen, escalatingCooldowns, firedThresholds, firedThresholdWindows, firedThresholdLastSeen, SWEEP_TTL_MS }
};