100 lines
3.2 KiB
JavaScript
Executable file
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 };
|