import { Config } from './services/config.ts'; import { Database, MigrationService } from './services/storage/index.ts'; import { Broadcaster } from './services/broadcaster.ts'; import { ApplicationRouter } from './services/router.ts'; import { HttpTransportRouter } from './services/transport/http-transport.ts'; import { WsTransportRouter } from './services/transport/ws-transport.ts'; import { ServerHost } from './services/server-host.ts'; import { Logger } from './utils/logger.ts'; import { DataRoute } from './routes/resources.ts'; /** Application composition root. */ export class App { /** * Construct infrastructure and validate all routes before returning a runnable app. */ static async create(): Promise { const config = Config.fromEnv(); const debug = new Logger('sync-server-v2'); debug('config loaded: %O', config); // Persistence must be ready before routes accept traffic. const database = new Database({ path: config.database.path, debug }); const migrations = new MigrationService(database, debug); await migrations.migrateToLatest(); // Domain services are shared across all transports and route modules. const broadcaster = new Broadcaster(debug); const routes = [ // DataRoute owns resource read/write/subscribe logic and maps resource // ids to broadcaster topics. timestampWindowMs controls write replay protection. new DataRoute(database, broadcaster, config.auth.timestampWindowMs), ]; // Route loading is an explicit startup phase, not first-request work. // ApplicationRouter.create validates every path and rejects duplicates // before any client can connect. const router = await ApplicationRouter.create(routes); // Both transports share one ApplicationRouter. Routes use the shared // Broadcaster directly, while HTTP and WebSocket remain protocol adapters. const http = new HttpTransportRouter(router, debug); const ws = new WsTransportRouter(router, debug, config.server.maxRequestBodyBytes); const host = new ServerHost(config, debug, [http, ws]); return new App(host, database); } private stopPromise: Promise | undefined; constructor( private readonly host: ServerHost, private readonly database: Database, ) {} async start(): Promise { await this.database.start(); await this.host.start(); } /** Stop transports before releasing the database they may still use. */ async stop(): Promise { this.stopPromise ??= (async (): Promise => { try { await this.host.stop(); } finally { await this.database.destroy(); } })(); await this.stopPromise; } } const app = await App.create(); let shuttingDown = false; const shutdown = (signal: NodeJS.Signals): void => { if (shuttingDown) { return; } shuttingDown = true; void app.stop().catch((error) => { console.error('Graceful shutdown failed after ' + signal, error); process.exitCode = 1; }); }; process.once('SIGINT', (): void => shutdown('SIGINT')); process.once('SIGTERM', (): void => shutdown('SIGTERM')); try { await app.start(); } catch (error: unknown) { await app.stop(); throw error; }