import Fastify, { type FastifyInstance } from 'fastify'; import fastifyStatic from '@fastify/static'; import { Server } from 'socket.io'; import { resolve } from 'node:path'; import { existsSync } from 'node:fs'; import { createStatsStore } from './stats-store.js'; import { systemClock, type Clock } from './clock.js'; import { createAnalyticsStore } from './analytics-store.js'; import { Analytics } from './analytics.js'; import { registerAdmin } from './admin.js'; import { createGameSessionStore } from './game-session-store.js'; import { createRoomStore } from './rooms.js'; import { registerAnalyticsApi, registerGameApi, registerRoomApi } from './game-api.js'; import { registerSignaling, type RtcServerOptions } from './signaling.js'; import { createRankedService } from './ranked-service.js'; import { registerRankedApi } from './ranked-api.js'; export interface ServerOptions { mongoUri: string; databaseName: string; clientDirectory: string; clock?: Clock; adminUser?: string; adminPassword?: string; environment?: 'production' | 'test'; rtc?: RtcServerOptions } export async function createGameServer(options: ServerOptions): Promise { const store = await createStatsStore(options); const app = Fastify({ logger: false, bodyLimit: 16384 }); const io = new Server(app.server, { maxHttpBufferSize: 16384 }); const clock = options.clock ?? systemClock; const gameSessions = await createGameSessionStore(store.db, () => clock.now(), options.environment ?? 'production'); const peerRooms = await createRoomStore(store.db, () => clock.now()); const ranked = await createRankedService(store.db, clock, options.environment ?? 'production'); const signaling = registerSignaling(io, gameSessions, peerRooms, options.rtc, () => clock.now()); registerGameApi(app, gameSessions, store, ranked); registerRoomApi(app, gameSessions, peerRooms, signaling, ranked); registerRankedApi(app, gameSessions, ranked); const analyticsStore = await createAnalyticsStore(store.db); const analytics = new Analytics(analyticsStore, clock, options.environment ?? 'production'); registerAnalyticsApi(app, gameSessions, analytics); app.addHook('onRequest', async request => { if (!request.url.startsWith('/api/admin/') && !request.url.startsWith('/admin/')) analytics.httpRequests++; }); io.use((_socket, next) => next(new Error('USE_SIGNALING'))); app.get('/health', async () => ({ status: 'ok' })); const directory = resolve(options.clientDirectory); await app.register(fastifyStatic, { root: directory, serve: false }); if (existsSync(directory)) { app.get('/', (_request, reply) => reply.sendFile('index.html')); app.get('/assets/*', (request, reply) => { const path = (request.params as { '*': string })['*']; if (!/^[a-zA-Z0-9_.-]+$/.test(path)) return reply.code(404).send(); return reply.sendFile(`assets/${path}`); }); } registerAdmin(app, store, analytics, clock, { user: options.adminUser, password: options.adminPassword }, directory); app.addHook('preClose', async () => { await ranked.close(); await analytics.close(); await new Promise(done => io.close(() => done())); }); app.addHook('onClose', async () => { await store.close(); }); return app; }