Files
Cs2-Mapveto/server/src/realtime.js
T
mregosar f08297dd0d Backend: Match-Lobbys, Map-Veto, Echtzeit und CS2-Serveradapter
Express + Socket.IO + SQLite. Veto-Engine mit konfigurierbaren Map-Pools
und validierten Abläufen, rollenbasierte Berechtigungen, Server-Lebenszyklus
mit Demo-, Docker- und Static-Pool-Adapter (RCON), automatisches Herunterfahren.
2026-10-25 16:50:22 +01:00

93 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Server } from 'socket.io';
/**
* Echtzeit-Kanal. Clients senden nie Aktionen über den Socket – alle Änderungen laufen über die
* REST-API. Der Socket liefert nach jeder Änderung den vollständigen, rollenabhängigen Match-Zustand.
* Dadurch ist ein Client nach Neuladen oder Verbindungsabbruch sofort wieder synchron.
*/
export function attachRealtime(httpServer, { service, config }) {
const io = new Server(httpServer, {
path: '/socket.io',
cors: { origin: config.publicUrl, credentials: false },
connectionStateRecovery: { maxDisconnectionDuration: 2 * 60_000 },
});
// matchId -> Map(socketId -> presenceKey)
const presence = new Map();
service.presence = (matchId) => new Set((presence.get(matchId) ?? new Map()).values());
const sendState = (socket) => {
const { matchId, token } = socket.data;
try {
const viewer = service.resolveViewer(matchId, token);
socket.emit('match:state', service.snapshot(matchId, viewer));
// Rolle kann sich geändert haben (z. B. Spieler entfernt, neuer Captain) – Anwesenheit nachziehen.
presence.get(matchId)?.set(socket.id, viewer.playerId ?? `socket:${socket.id}`);
} catch (err) {
socket.emit('match:error', { error: err.message, code: err.code ?? 'error' });
}
};
let overviewTimer = null;
const broadcastOverview = () => {
clearTimeout(overviewTimer);
overviewTimer = setTimeout(() => io.to('overview').emit('overview', { matches: service.listMatches() }), 150);
};
const pending = new Map();
const broadcastMatch = (matchId) => {
// Mehrere Änderungen innerhalb kurzer Zeit werden zu einem Update zusammengefasst.
if (pending.has(matchId)) return;
pending.set(matchId, setTimeout(async () => {
pending.delete(matchId);
const sockets = await io.in(`match:${matchId}`).fetchSockets();
sockets.forEach(sendState);
}, 50));
};
service.on('changed', (matchId) => {
broadcastMatch(matchId);
broadcastOverview();
});
io.on('connection', (socket) => {
socket.on('overview:subscribe', () => {
socket.join('overview');
socket.emit('overview', { matches: service.listMatches() });
});
socket.on('match:subscribe', ({ matchId, token } = {}) => {
if (typeof matchId !== 'string') return;
for (const room of socket.rooms) if (room.startsWith('match:')) leave(room.slice(6));
socket.data = { matchId, token: typeof token === 'string' ? token : null };
try {
service.getMatch(matchId);
} catch (err) {
socket.emit('match:error', { error: err.message, code: err.code });
return;
}
socket.join(`match:${matchId}`);
if (!presence.has(matchId)) presence.set(matchId, new Map());
service.touch(matchId);
sendState(socket);
broadcastMatch(matchId); // Online-Status für alle aktualisieren
});
const leave = (matchId) => {
socket.leave(`match:${matchId}`);
const map = presence.get(matchId);
if (map?.delete(socket.id)) {
if (map.size === 0) presence.delete(matchId);
service.touch(matchId);
broadcastMatch(matchId);
}
};
socket.on('disconnect', () => {
if (socket.data?.matchId) leave(socket.data.matchId);
});
});
return io;
}