136 lines
3.7 KiB
JavaScript
136 lines
3.7 KiB
JavaScript
// 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;
|