// Copyright (c) 2026 Gordon Bolton. MIT License. const { logToFile } = require('./logger'); class MemoryCache { constructor() { this.store = new Map(); } get(key) { const entry = this.store.get(key); if (!entry) return null; if (Date.now() > entry.expiresAt) { this.store.delete(key); return null; } return entry.value; } set(key, value, ttlMs) { this.store.set(key, { value, expiresAt: Date.now() + ttlMs }); } invalidate(key) { this.store.delete(key); } clear() { this.store.clear(); } getStats() { const now = Date.now(); const entries = []; let totalSize = 0; for (const [key, entry] of this.store.entries()) { // Maps must be converted before JSON.stringify (which renders them as "{}") const serializable = entry.value instanceof Map ? Object.fromEntries(entry.value) : entry.value; const json = JSON.stringify(serializable); const sizeBytes = Buffer.byteLength(json, 'utf8'); totalSize += sizeBytes; const ttlRemaining = Math.max(0, entry.expiresAt - now); const expired = now > entry.expiresAt; let itemCount = null; if (entry.value instanceof Map) { itemCount = entry.value.size; } else if (Array.isArray(entry.value)) { itemCount = entry.value.length; } else if (entry.value && typeof entry.value === 'object') { if (Array.isArray(entry.value.records)) itemCount = entry.value.records.length; else if (Array.isArray(entry.value.slots)) itemCount = entry.value.slots.length; } entries.push({ key, sizeBytes, itemCount, ttlRemainingMs: ttlRemaining, expired }); } return { entryCount: this.store.size, totalSizeBytes: totalSize, entries }; } } const cache = new MemoryCache(); // Webhook metrics for polling optimization // These are stored separately from regular cache entries const webhookMetrics = { // Per-instance metrics: key = instance URL, value = { lastWebhookTimestamp, eventsReceived, pollsSkipped } instances: new Map(), // Global metrics lastGlobalWebhookTimestamp: null, totalWebhookEventsReceived: 0 }; function getWebhookMetrics(instanceUrl) { if (!instanceUrl) return null; return webhookMetrics.instances.get(instanceUrl) || { lastWebhookTimestamp: null, eventsReceived: 0, pollsSkipped: 0 }; } function updateWebhookMetrics(instanceUrl) { const now = Date.now(); webhookMetrics.lastGlobalWebhookTimestamp = now; webhookMetrics.totalWebhookEventsReceived++; if (instanceUrl) { const metrics = webhookMetrics.instances.get(instanceUrl) || { lastWebhookTimestamp: null, eventsReceived: 0, pollsSkipped: 0 }; metrics.lastWebhookTimestamp = now; metrics.eventsReceived++; webhookMetrics.instances.set(instanceUrl, metrics); } } function incrementPollsSkipped(instanceUrl) { if (instanceUrl) { const metrics = webhookMetrics.instances.get(instanceUrl) || { lastWebhookTimestamp: null, eventsReceived: 0, pollsSkipped: 0 }; metrics.pollsSkipped++; webhookMetrics.instances.set(instanceUrl, metrics); } } function getGlobalWebhookMetrics() { return { lastGlobalWebhookTimestamp: webhookMetrics.lastGlobalWebhookTimestamp, totalWebhookEventsReceived: webhookMetrics.totalWebhookEventsReceived, instances: Object.fromEntries(webhookMetrics.instances) }; } module.exports = cache; module.exports.getWebhookMetrics = getWebhookMetrics; module.exports.updateWebhookMetrics = updateWebhookMetrics; module.exports.incrementPollsSkipped = incrementPollsSkipped; module.exports.getGlobalWebhookMetrics = getGlobalWebhookMetrics;