mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 21:15:16 +02:00
88 lines
3.0 KiB
JavaScript
88 lines
3.0 KiB
JavaScript
'use strict';
|
|
const http = require('node:http');
|
|
const { events } = require('./data');
|
|
|
|
// Indexed implementation: per-type arrays sorted by timestamp, with prefix
|
|
// sums, built once at startup. Per query the range is located with binary
|
|
// search; only the matching slice is touched.
|
|
function buildIndex() {
|
|
const byType = new Map();
|
|
for (const event of events) {
|
|
if (!byType.has(event.type)) byType.set(event.type, []);
|
|
byType.get(event.type).push(event);
|
|
}
|
|
for (const rows of byType.values()) {
|
|
rows.sort((a, b) => a.ts - b.ts);
|
|
const prefix = new Float64Array(rows.length + 1);
|
|
for (let i = 0; i < rows.length; i++) prefix[i + 1] = prefix[i] + rows[i].value;
|
|
rows.prefixSums = prefix;
|
|
}
|
|
return byType;
|
|
}
|
|
|
|
function lowerBound(rows, ts) {
|
|
let lo = 0;
|
|
let hi = rows.length;
|
|
while (lo < hi) {
|
|
const mid = (lo + hi) >> 1;
|
|
if (rows[mid].ts < ts) lo = mid + 1; else hi = mid;
|
|
}
|
|
return lo;
|
|
}
|
|
|
|
function upperBound(rows, ts) {
|
|
let lo = 0;
|
|
let hi = rows.length;
|
|
while (lo < hi) {
|
|
const mid = (lo + hi) >> 1;
|
|
if (rows[mid].ts <= ts) lo = mid + 1; else hi = mid;
|
|
}
|
|
return lo;
|
|
}
|
|
|
|
const EMPTY = { count: 0, sum: 0, avg: null, p50: null, p95: null, p99: null, min: null, max: null };
|
|
|
|
function summarize(index, type, from, to) {
|
|
const rows = index.get(type);
|
|
if (!rows) return EMPTY;
|
|
const lo = from === null ? 0 : lowerBound(rows, from);
|
|
const hi = to === null ? rows.length : upperBound(rows, to);
|
|
const count = hi - lo;
|
|
if (count <= 0) return EMPTY;
|
|
const sum = rows.prefixSums[hi] - rows.prefixSums[lo];
|
|
const values = new Array(count);
|
|
for (let i = 0; i < count; i++) values[i] = rows[lo + i].value;
|
|
values.sort((a, b) => a - b);
|
|
const rank = p => values[Math.ceil((p / 100) * count) - 1];
|
|
const avgCents = Math.floor((sum * 200 + count) / (count * 2));
|
|
return { count, sum, avg: avgCents / 100,
|
|
p50: rank(50), p95: rank(95), p99: rank(99), min: values[0], max: values[count - 1] };
|
|
}
|
|
|
|
function createApp() {
|
|
const index = buildIndex();
|
|
return http.createServer((req, res) => {
|
|
const url = new URL(req.url, 'http://localhost');
|
|
if (req.method === 'GET' && url.pathname === '/stats') {
|
|
const type = url.searchParams.get('type');
|
|
const hasFrom = url.searchParams.has('from');
|
|
const hasTo = url.searchParams.has('to');
|
|
const from = hasFrom ? Number(url.searchParams.get('from')) : null;
|
|
const to = hasTo ? Number(url.searchParams.get('to')) : null;
|
|
if ((hasFrom && !Number.isFinite(from)) || (hasTo && !Number.isFinite(to))
|
|
|| (from !== null && to !== null && from > to)) {
|
|
res.writeHead(400, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'invalid bounds' }));
|
|
return;
|
|
}
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ type, from, to, ...summarize(index, type, from, to) }));
|
|
return;
|
|
}
|
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'not found' }));
|
|
});
|
|
}
|
|
|
|
module.exports = { createApp };
|