import { RTMTWS } from "./rtmt/rtmt_websocket"; import { GameManager } from "./game/GameManager"; import { GameStoreImpl } from "./game/gamestore/GameStoreImpl"; import { MatchMakerImpl } from "./matchmaker/MatchMakerImpl"; import { ExpressApp } from "./server/ExpressApp"; import { WebServer } from "./server/WebServer"; import { KeycloakTokenVerifier, readAuthConfig } from "./auth"; import { createPool, runMigrations } from "./db/postgres"; import { PostgresGameHistoryRepository } from "./history/GameHistoryRepository"; import { createValkeyClient } from "./live/valkey"; import { ValkeyLiveGameStore } from "./live/LiveGameStore"; import { GameRecorder } from "./game/GameRecorder"; import { LiveGames } from "./game/LiveGames"; import { GameHistoryService } from "./history/GameHistoryService"; import { ValkeyCache } from "./cache/Cache"; const DATABASE_URL = process.env.DATABASE_URL || "postgres://mancala:mancala@localhost:5433/mancala"; const VALKEY_URL = process.env.VALKEY_URL || "redis://localhost:6379"; const MIGRATION_RETRY_MS = 30_000; const VALKEY_STARTUP_WAIT_MS = 5_000; async function main() { const authConfig = readAuthConfig(); const tokenVerifier = new KeycloakTokenVerifier({ issuer: authConfig.keycloakIssuer, jwksUrl: authConfig.keycloakJwksUrl, allowedClientIds: authConfig.keycloakClientIds, }); const pool = createPool(DATABASE_URL); migrateWhenReachable(pool); const valkey = createValkeyClient(VALKEY_URL); await waitForReady(valkey, VALKEY_STARTUP_WAIT_MS); const liveGameStore = new ValkeyLiveGameStore(valkey); const repository = new PostgresGameHistoryRepository(pool); const recorder = new GameRecorder(repository, liveGameStore); const history = new GameHistoryService(repository, new ValkeyCache(valkey), () => recorder.getUnsaved()); recorder.onSaved((record) => history.onGameSaved(record)); const rtmt = new RTMTWS({ tokenVerifier, legacyUserKeyAuth: authConfig.legacyUserKeyAuth }); const gameStore = new GameStoreImpl(); const matchMaker = new MatchMakerImpl({ rtmt }); const gameManager = new GameManager({ rtmt, gameStore, matchMaker, liveGameStore, recorder }); const liveGames = new LiveGames({ rtmt, gameStore, gameManager }); // Before accepting connections, so reconnecting players find their games. await gameManager.restoreLiveGames(); recorder.start(); gameManager.startTimers(); const expressApp = new ExpressApp({ gameStore, gameManager, liveGames, history, tokenVerifier, legacyUserKeyAuth: authConfig.legacyUserKeyAuth, }); const server = new WebServer({ expressApp: expressApp.app }); rtmt.initWebSocket(server.server); } // The game runs without the database; saved games wait in the recorder until migrations have run. function migrateWhenReachable(pool: ReturnType) { runMigrations(pool).catch((err) => { console.error(`(DB) Migrations failed, retrying in ${MIGRATION_RETRY_MS / 1000}s:`, err.message); setTimeout(() => migrateWhenReachable(pool), MIGRATION_RETRY_MS); }); } function waitForReady(client: ReturnType, timeoutMs: number): Promise { if (client.status === "ready") return Promise.resolve(); return new Promise((resolve) => { const timer = setTimeout(() => { console.error("(Valkey) Not reachable yet; starting without restoring live games"); resolve(); }, timeoutMs); client.once("ready", () => { clearTimeout(timer); resolve(); }); }); } main().catch((err) => { console.error(err); process.exit(1); });