asset_browser/syncroStats.js
setonc a558804026 Initial commit — asset browser web app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 09:06:25 -04:00

100 lines
3.2 KiB
JavaScript
Executable file

'use strict';
// Rolling request log for Syncro API calls.
// Shared between server.js (writes) and auth/admin.js (reads).
const fs = require('fs');
const path = require('path');
const PERSIST_PATH = path.join(__dirname, 'data', 'syncro-history.json');
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
const PERSIST_INTERVAL_MS = 60_000; // flush to disk at most once per minute
const _log = []; // timestamps for rolling 60s count
const _buckets = new Map(); // minuteTs (ms) → request count
// ── Load persisted history on startup ────────────────────────────────────────
(function _loadHistory() {
try {
const raw = fs.readFileSync(PERSIST_PATH, 'utf8');
const data = JSON.parse(raw);
const cutoff = Date.now() - SEVEN_DAYS_MS;
for (const [tsStr, count] of Object.entries(data)) {
const ts = Number(tsStr);
if (ts >= cutoff) _buckets.set(ts, count);
}
} catch (_) {
// File missing or corrupt — start fresh, no problem
}
})();
// ── Persist helper ────────────────────────────────────────────────────────────
let _persistTimer = null;
function _schedulePersist() {
if (_persistTimer) return;
_persistTimer = setTimeout(() => {
_persistTimer = null;
_flushToDisk();
}, PERSIST_INTERVAL_MS);
}
function _flushToDisk() {
try {
const obj = {};
for (const [ts, count] of _buckets) obj[ts] = count;
fs.writeFileSync(PERSIST_PATH, JSON.stringify(obj));
} catch (_) {}
}
// ── Public API ────────────────────────────────────────────────────────────────
function logRequest() {
const now = Date.now();
// Rolling 60s log
_log.push(now);
while (_log.length && _log[0] < now - 60_000) _log.shift();
// Per-minute bucket
const minuteTs = Math.floor(now / 60_000) * 60_000;
_buckets.set(minuteTs, (_buckets.get(minuteTs) ?? 0) + 1);
// Prune buckets older than 7 days (Map is in insertion/chronological order)
const cutoff = now - SEVEN_DAYS_MS;
for (const ts of _buckets.keys()) {
if (ts < cutoff) _buckets.delete(ts);
else break;
}
_schedulePersist();
}
function getUsage() {
const now = Date.now();
while (_log.length && _log[0] < now - 60_000) _log.shift();
return _log.length;
}
// Returns an array of { ts, count } for the given window (capped at 7 days).
// Already in ascending chronological order.
function getHistory(windowMs) {
const now = Date.now();
const cutoff = now - Math.min(windowMs, SEVEN_DAYS_MS);
const result = [];
for (const [ts, count] of _buckets) {
if (ts >= cutoff) result.push({ ts, count });
}
return result;
}
// Returns the number of minute buckets in the last 7 days where requests >= limit.
function getLimitHits(limit) {
const cutoff = Date.now() - SEVEN_DAYS_MS;
let hits = 0;
for (const [ts, count] of _buckets) {
if (ts >= cutoff && count >= limit) hits++;
}
return hits;
}
module.exports = { logRequest, getUsage, getHistory, getLimitHits };