Improve application responsiveness and background processing

This commit is contained in:
2026-10-04 19:56:06 -04:00
parent 1392a082da
commit ed004ddbcf
70 changed files with 1324 additions and 345 deletions
+64
View File
@@ -0,0 +1,64 @@
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';
class ReportWorker {
private worker: Worker | null = null;
private sequence = 0;
private pending = new Map<number, { resolve: (value: any) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }>();
private flights = new Map<string, Promise<any>>();
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<T>(kind: string, input: unknown): Promise<T> {
const requestInput = input as { query?: MetricsQuery; now?: number };
const key = JSON.stringify(kind === 'metrics' ? [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<T>((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();
export const metricsReport = (query: MetricsQuery) => metrics.run<MetricsData>('metrics', { query, now: Date.now() });
export const leaderboardReport = (query: LeaderboardOptions) => leaderboard.run<LeaderboardResponse>('leaderboard', query);
export const playerReport = (guid: string, offset: number, options: LeaderboardOptions) => leaderboard.run<{ profile: PlayerProfile | null; entry: LeaderboardEntry | null }>('player', { guid, offset, options });
export async function closeReports() { await Promise.all([metrics.close(), leaderboard.close()]); }