import type { Server } from 'socket.io'; import { ApiError, type GameSessionStore } from './game-session-store.js'; import { PRESENCE_AUTH_REFRESH_MS } from '../../constants/presence.js'; // The application is deployed as one process, as is the ranked match service. export function registerPresence(io: Server, sessions: GameSessionStore) { const namespace = io.of('/presence'); let count = 0; function publish() { const next = new Set([...namespace.sockets.values()].filter(socket => socket.connected).map(socket => socket.data.playerId)).size; if (next !== count) { count = next; namespace.emit('presence:count', count); } } namespace.use((socket, next) => { const { gameSessionId, token } = socket.handshake.auth; void sessions.touchPresence({ gameSessionId, token }).then(session => { socket.data.playerId = session.playerId ?? session.gameSessionId; next(); }, error => next(new Error(error instanceof ApiError && error.statusCode === 401 ? 'INVALID_SESSION' : 'PRESENCE_UNAVAILABLE'))); }); namespace.on('connection', socket => { const before = count; publish(); if (before === count) socket.emit('presence:count', count); let checking = false; const timer = setInterval(() => { if (checking) return; checking = true; const { gameSessionId, token } = socket.handshake.auth; void sessions.touchPresence({ gameSessionId, token }).catch(error => { if (error instanceof ApiError && error.statusCode === 401) socket.disconnect(true); else socket.conn.close(); // Transport closure permits Socket.IO to reconnect after a transient outage. }).finally(() => { checking = false; }); }, PRESENCE_AUTH_REFRESH_MS); timer.unref(); socket.on('disconnect', () => { clearInterval(timer); publish(); }); }); }