66 lines
1.6 KiB
TypeScript
66 lines
1.6 KiB
TypeScript
import { getDb, hashIp } from "./db";
|
|
import { lookupCountry } from "./geo";
|
|
|
|
type PendingHit = {
|
|
domain_id: number;
|
|
ts: number;
|
|
ip_hash: string;
|
|
country: string | null;
|
|
user_agent: string | null;
|
|
referer: string | null;
|
|
path: string | null;
|
|
};
|
|
|
|
const buffer: PendingHit[] = [];
|
|
let flushTimer: NodeJS.Timeout | null = null;
|
|
|
|
function flush() {
|
|
if (buffer.length === 0) return;
|
|
const batch = buffer.splice(0, buffer.length);
|
|
try {
|
|
const db = getDb();
|
|
const stmt = db.prepare(
|
|
"INSERT INTO hits (domain_id, ts, ip_hash, country, user_agent, referer, path) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
|
);
|
|
db.transaction(() => {
|
|
for (const h of batch) {
|
|
stmt.run(h.domain_id, h.ts, h.ip_hash, h.country, h.user_agent, h.referer, h.path);
|
|
}
|
|
})();
|
|
} catch (e) {
|
|
console.error("[hits] flush failed", e);
|
|
}
|
|
}
|
|
|
|
function scheduleFlush() {
|
|
if (flushTimer) return;
|
|
flushTimer = setTimeout(() => {
|
|
flushTimer = null;
|
|
flush();
|
|
}, 5000);
|
|
}
|
|
|
|
export async function recordHit(input: {
|
|
domain_id: number;
|
|
ip: string;
|
|
user_agent: string | null;
|
|
referer: string | null;
|
|
path: string | null;
|
|
}) {
|
|
const country = await lookupCountry(input.ip).catch(() => null);
|
|
buffer.push({
|
|
domain_id: input.domain_id,
|
|
ts: Date.now(),
|
|
ip_hash: hashIp(input.ip),
|
|
country,
|
|
user_agent: input.user_agent ? input.user_agent.slice(0, 500) : null,
|
|
referer: input.referer ? input.referer.slice(0, 500) : null,
|
|
path: input.path ? input.path.slice(0, 500) : null,
|
|
});
|
|
if (buffer.length >= 100) flush();
|
|
else scheduleFlush();
|
|
}
|
|
|
|
export function flushHitsSync() {
|
|
flush();
|
|
}
|