'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 };