68 lines
4.1 KiB
TypeScript
68 lines
4.1 KiB
TypeScript
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 { registerPresence } from './presence.js';
|
|
import { createRankedService } from './ranked-service.js';
|
|
import { registerRankedApi } from './ranked-api.js';
|
|
import { createGlobalSettingsStore } from './global-settings.js';
|
|
import { gameRoute } from './game-api.js';
|
|
import { createHttpOptions, registerHttpOperations, logSafeError } from './http-operations.js';
|
|
export interface ServerOptions { mongoUri: string; databaseName: string; clientDirectory: string; clock?: Clock; adminUser?: string; adminPassword?: string; environment?: 'production' | 'test'; rtc?: RtcServerOptions; trustProxy?: false | string[]; logLevel?: string }
|
|
export async function createGameServer(options: ServerOptions): Promise<FastifyInstance> {
|
|
const store = await createStatsStore(options);
|
|
const app = Fastify(createHttpOptions(options));
|
|
const io = new Server(app.server, { maxHttpBufferSize: 16384 });
|
|
const clock = options.clock ?? systemClock;
|
|
registerHttpOperations(app, { now: () => clock.now(), ready: () => store.db.command({ ping: 1 }, { timeoutMS: 3000 }) });
|
|
const settings = createGlobalSettingsStore(store.db);
|
|
await settings.initialize();
|
|
gameRoute(app, 'GET', '/api/global-settings', async request => {
|
|
const current = await settings.get();
|
|
return Number((request.query as { revision?: string }).revision) === current.revision
|
|
? { revision: current.revision } : { revision: current.revision, settings: current };
|
|
});
|
|
const gameSessions = await createGameSessionStore(store.db, () => clock.now(), options.environment ?? 'production');
|
|
registerPresence(io, gameSessions);
|
|
const peerRooms = await createRoomStore(store.db, () => clock.now());
|
|
const ranked = await createRankedService(store.db, clock, options.environment ?? 'production', () => logSafeError(app.log, 'ranked_background', undefined));
|
|
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, settings);
|
|
app.addHook('preClose', async () => {
|
|
await ranked.close();
|
|
await analytics.close();
|
|
await new Promise<void>(done => io.close(() => done()));
|
|
});
|
|
app.addHook('onClose', async () => { await store.close(); });
|
|
return app;
|
|
}
|