import { Worker } from 'node:worker_threads'; import { resolve } from 'node:path'; import { config } from '../config.js'; import { db } from '../db/index.js'; import type { MetricsQuery, MetricsData } from '../../metrics.js'; import type { LeaderboardOptions, LeaderboardResponse, PlayerProfile, LeaderboardEntry } from '../../leaderboard.js'; import type { CapacityHistory, CapacityQuery } from '../../capacity.js'; import { ensureHostSchema } from '../hosts/schema.js'; class ReportWorker { private worker: Worker | null = null; private sequence = 0; private pending = new Map< number, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Pending requests hold resolvers for different report result types. { resolve: (value: any) => void; reject: (error: Error) => void; timer: NodeJS.Timeout } >(); // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each request key identifies a caller-specific report result type. private flights = new Map>(); async close() { const worker = this.worker; this.worker = null; for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(new Error('Report service stopped')); } this.pending.clear(); this.flights.clear(); await worker?.terminate(); } run(kind: string, input: unknown): Promise { const requestInput = input as { query?: unknown; now?: number }; const timed = kind === 'metrics' || kind === 'capacity'; const key = JSON.stringify( timed ? [kind, requestInput.query, Math.floor(requestInput.now! / 30_000)] : [kind, input] ); const flight = this.flights.get(key); if (flight) return flight; if (this.pending.size >= 64) return Promise.reject(new Error('Reports are busy. Please retry shortly.')); db(); // Finish migrations before opening the independent reader. if (!this.worker) { const worker = (this.worker = new Worker( resolve( process.env.NODE_ENV === 'production' ? 'build/reports/worker.mjs' : '.svelte-kit/reports/worker.mjs' ), { workerData: { dbPath: resolve(config.dbPath) } } )); worker.on('message', ({ id, value, error, elapsedMs, cached }) => { const pending = this.pending.get(id); if (!pending) return; clearTimeout(pending.timer); this.pending.delete(id); if (process.env.EC_PERF_LOG === 'true') console.info('[report]', { kind, elapsedMs, cached }); if (error) pending.reject(new Error(error)); else pending.resolve(value); if (!this.pending.size) worker.unref(); }); const fail = (error: Error) => { if (this.worker !== worker) return; this.worker = null; for (const pending of this.pending.values()) { clearTimeout(pending.timer); pending.reject(error); } this.pending.clear(); void worker.terminate(); }; worker.on('error', fail); worker.on('exit', () => fail(new Error('Report worker stopped. Please retry.'))); } this.worker.ref(); const id = ++this.sequence; const promise = new Promise((resolve, reject) => { const timer = setTimeout(() => { // Reset the worker rather than leaving an expensive abandoned query queued. const worker = this.worker; this.worker = null; for (const item of this.pending.values()) { clearTimeout(item.timer); item.reject(new Error('Report timed out. Please retry.')); } this.pending.clear(); void worker?.terminate(); }, 60_000); this.pending.set(id, { resolve, reject, timer }); this.worker!.postMessage({ id, kind, input }); }).finally(() => { if (this.flights.get(key) === promise) this.flights.delete(key); }); this.flights.set(key, promise); return promise; } } const metrics = new ReportWorker(); const leaderboard = new ReportWorker(); const capacity = new ReportWorker(); export const metricsReport = (query: MetricsQuery) => metrics.run('metrics', { query, now: Date.now() }); export const leaderboardReport = (query: LeaderboardOptions) => leaderboard.run('leaderboard', query); export const playerReport = (guid: string, offset: number, options: LeaderboardOptions) => leaderboard.run<{ profile: PlayerProfile | null; entry: LeaderboardEntry | null }>('player', { guid, offset, options }); export const capacityHistoryReport = (query: CapacityQuery) => { ensureHostSchema(db()); // Game changes current placement estimates, not measured host resources. return capacity.run('capacity', { query: { ...query, game: 'all' }, now: Date.now() }); }; export async function closeReports() { await Promise.all([metrics.close(), leaderboard.close(), capacity.close()]); }