37 lines
1.8 KiB
TypeScript
37 lines
1.8 KiB
TypeScript
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(); });
|
|
});
|
|
}
|