Show authenticated unique online player count on home screen

This commit is contained in:
2026-10-15 00:07:41 +09:00
parent 7c414655c8
commit 32d32a6ebc
9 changed files with 99 additions and 0 deletions
+1
View File
@@ -42,6 +42,7 @@ async function setup(kind: 'expired' | 'orphan' | 'rematch-wait' | 'result') {
createGlobalSettingsCache: () => createGlobalSettingsCache(storage, send),
createAppFooter: () => ({ setConnection() {}, openRules() {} }),
installSoundControls() {},
createPresence: () => ({ connect() {} }),
createClientAnalytics: () => ({ ready() {}, screen() {}, gameFinished() {}, gameStarted() {}, error() {}, click() {} }),
createResultReporter: (api: any) => ({ destroy() {}, resumePending: async () => {}, submit: api.record }),
createMatchView: () => ({ update() {}, destroy() {} }), createPeer: () => ({}),
+35
View File
@@ -0,0 +1,35 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { Server } from 'socket.io';
import { io as client, type Socket } from 'socket.io-client';
import { registerPresence } from '../src/server/presence.js';
import type { GameSessionStore } from '../src/server/game-session-store.js';
test('presence counts authenticated players once across tabs and removes disconnected players', async () => {
const http = createServer(), io = new Server(http);
registerPresence(io, { async get(auth: { token: string; gameSessionId: string }) {
if (auth.token !== 'valid') throw new Error('INVALID_SESSION');
return { playerId: auth.gameSessionId.startsWith('a') ? 'a' : 'b' };
} } as unknown as GameSessionStore);
await new Promise<void>(resolve => http.listen(0, '127.0.0.1', resolve));
const url = `http://127.0.0.1:${(http.address() as { port: number }).port}/presence`;
const sockets: Socket[] = [];
const open = (id: string, token = 'valid') => {
const socket = client(url, { autoConnect: false, forceNew: true, auth: { gameSessionId: id, token }, reconnection: false });
sockets.push(socket); return socket;
};
const next = (socket: Socket) => new Promise<number>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('PRESENCE_TIMEOUT')), 3000);
socket.once('presence:count', n => { clearTimeout(timer); resolve(n); });
});
try {
const a = open('a1'), aFirst = next(a); a.connect(); assert.equal(await aFirst, 1);
const secondTab = open('a2'), tabFirst = next(secondTab); secondTab.connect(); assert.equal(await tabFirst, 1);
const b = open('b1'), bFirst = next(b), aUpdate = next(a); b.connect();
assert.equal(await bFirst, 2); assert.equal(await aUpdate, 2);
const bad = open('bad', 'invalid');
const rejected = new Promise<void>(resolve => bad.once('connect_error', () => resolve())); bad.connect(); await rejected;
const lastA = next(secondTab); a.disconnect(); b.disconnect(); assert.equal(await lastA, 1);
} finally { for (const socket of sockets) socket.disconnect(); await new Promise<void>(resolve => io.close(() => resolve())); }
});