Statistikportal für den All-the-Mods-10-Server (Minecraft 1.21.1, NeoForge 21.1): - mod/: serverseitige NeoForge-Mod atmstats – Ereignisse, Live-Zähler, Servergesundheit, FTB-Dateien; Outbox auf der Festplatte, signierter gzip-Versand per HTTPS mit Backoff - backend/: Fastify + PostgreSQL – idempotenter Ingest, Projektionen, Parser für SNBT, FTB Quests/Teams, world/stats, world/advancements und latest.log; Ranglisten, Snapshots, Retention, öffentliche API, SSE-Live-Updates, Admin mit argon2/CSRF, SFTP-Worker - web/: Next.js-Dashboard – Übersicht, Ranglisten, Spielerprofile, ATM10, Server, Datenquellen, Verwaltung - shared/: Protokoll v1 und API-Typen; tools/: Simulator, Log-Anonymisierer, RCON-Client - deploy/: Docker Compose hinter Traefik, Backup; docs/: Architektur, Datenquellen, Betrieb Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
46 lines
1.6 KiB
TypeScript
46 lines
1.6 KiB
TypeScript
import type { LiveEvent } from '@atm/shared/api';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import type { LiveHub } from '../live/hub.ts';
|
|
|
|
export const HEARTBEAT_MS = 25_000;
|
|
const MAX_CLIENTS = 500;
|
|
|
|
const frame = (e: LiveEvent) => `id: ${e.id}\nevent: ${e.kind}\ndata: ${JSON.stringify(e.data)}\n\n`;
|
|
|
|
export async function liveRoutes(app: FastifyInstance, { hub }: { hub: LiveHub }) {
|
|
let clients = 0;
|
|
const open = new Set<{ end: () => void }>();
|
|
// Hijacked streams are not tracked by Fastify; end them so shutdown does not hang.
|
|
app.addHook('onClose', async () => {
|
|
for (const res of open) res.end();
|
|
});
|
|
|
|
app.get<{ Headers: { 'last-event-id'?: string } }>('/api/live', async (req, reply) => {
|
|
if (clients >= MAX_CLIENTS) return reply.code(503).send({ error: 'too_many_clients' });
|
|
clients++;
|
|
reply.hijack();
|
|
const res = reply.raw;
|
|
res.writeHead(200, {
|
|
'content-type': 'text/event-stream; charset=utf-8',
|
|
'cache-control': 'no-cache, no-transform',
|
|
connection: 'keep-alive',
|
|
'x-accel-buffering': 'no',
|
|
});
|
|
res.write(`retry: 5000\n: connected\n\n`);
|
|
|
|
const lastId = Number(req.headers['last-event-id']);
|
|
if (Number.isFinite(lastId) && lastId > 0) for (const e of hub.since(lastId)) res.write(frame(e));
|
|
|
|
const unsubscribe = hub.subscribe((e) => res.write(frame(e)));
|
|
const heartbeat = setInterval(() => res.write(': ping\n\n'), HEARTBEAT_MS);
|
|
open.add(res);
|
|
const close = () => {
|
|
clearInterval(heartbeat);
|
|
unsubscribe();
|
|
open.delete(res);
|
|
clients--;
|
|
};
|
|
req.raw.on('close', close);
|
|
});
|
|
}
|