98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
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<App> {
|
|
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<void> | undefined;
|
|
|
|
constructor(
|
|
private readonly host: ServerHost,
|
|
private readonly database: Database,
|
|
) {}
|
|
|
|
async start(): Promise<void> {
|
|
await this.database.start();
|
|
await this.host.start();
|
|
}
|
|
|
|
/** Stop transports before releasing the database they may still use. */
|
|
async stop(): Promise<void> {
|
|
this.stopPromise ??= (async (): Promise<void> => {
|
|
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;
|
|
}
|