diff --git a/package-lock.json b/package-lock.json index 66ed265..656441b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "debug": "^4.4.3", "dotenv": "^17.4.2", "hono": "^4.12.26", - "kysely": "^0.29.2", + "kysely": "^0.29.4", + "kysely-node-native-sqlite": "^1.0.1", "ws": "^8.21.0", "zod": "^4.4.3" }, @@ -8314,6 +8315,15 @@ "node": ">=22.0.0" } }, + "node_modules/kysely-node-native-sqlite": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kysely-node-native-sqlite/-/kysely-node-native-sqlite-1.0.1.tgz", + "integrity": "sha512-FHty4bi885waQMMMw2P9F+mqZUdozd1OBWQI72CffsQzbZhcOgsz3jpZRTUM1xAtG1wkz/VQAj3JavQvCdhL2w==", + "license": "ISC", + "peerDependencies": { + "kysely": ">=0.27.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", diff --git a/package.json b/package.json index eca8a9a..0a5bf01 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,8 @@ "debug": "^4.4.3", "dotenv": "^17.4.2", "hono": "^4.12.26", - "kysely": "^0.29.2", + "kysely": "^0.29.4", + "kysely-node-native-sqlite": "^1.0.1", "ws": "^8.21.0", "zod": "^4.4.3" }, diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..08df05e --- /dev/null +++ b/src/app.ts @@ -0,0 +1,59 @@ +import { Config } from './services/config.ts'; +import { Database, MigrationService } from './services/storage/index.ts'; +import { Logger } from './utils/logger.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(); + + return new App(database); + } + + private stopPromise: Promise | undefined; + + constructor(private readonly database: Database) {} + + async start(): Promise {} + + /** Stop transports before releasing the database they may still use. */ + async stop(): Promise { + this.stopPromise ??= 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; +} diff --git a/src/services/storage/database.ts b/src/services/storage/database.ts index f89832b..c3ef093 100644 --- a/src/services/storage/database.ts +++ b/src/services/storage/database.ts @@ -1,5 +1,6 @@ -import DatabaseConstructor from 'better-sqlite3'; -import { Kysely, SqliteDialect } from 'kysely'; +import { CompiledQuery, Kysely } from 'kysely'; +import { NodeNativeSqliteDialect } from 'kysely-node-native-sqlite'; + import type { DatabaseTables } from './tables.ts'; import type { Logger } from '../../utils/logger.ts'; @@ -13,13 +14,13 @@ export type DatabaseOptions = { }; /** - * Thin wrapper around better-sqlite3 and Kysely. + * Thin wrapper around Kysely and NodeNativeSqliteDialect (which uses node:sqlite). * * Owns connection setup, pragma configuration, and graceful teardown. */ export class Database { private readonly debug: Logger; - private readonly sqlite: DatabaseConstructor.Database; + private readonly dialect: NodeNativeSqliteDialect; private readonly kysely: Kysely; /** @@ -32,13 +33,15 @@ export class Database { this.debug = options.debug.extend('database'); // Create the SQLite database. - this.sqlite = new DatabaseConstructor(options.path); - this.configurePragmas(); + this.dialect = new NodeNativeSqliteDialect(options.path); // Create the Kysely database. this.kysely = new Kysely({ - dialect: new SqliteDialect({ database: this.sqlite }), + dialect: this.dialect, }); + + // Configure the SQLite pragmas. + this.configurePragmas(); } /** @@ -65,7 +68,8 @@ export class Database { */ private configurePragmas(): void { this.debug('configuring SQLite pragmas'); - this.sqlite.pragma('journal_mode = WAL'); - this.sqlite.pragma('foreign_keys = ON'); + + this.kysely.executeQuery(CompiledQuery.raw('PRAGMA journal_mode = WAL')); + this.kysely.executeQuery(CompiledQuery.raw('PRAGMA foreign_keys = ON')); } } diff --git a/tsconfig.json b/tsconfig.json index 2f84530..779ded5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,8 @@ "allowImportingTsExtensions": true, "noEmit": true, "declaration": true, - "declarationMap": true + "declarationMap": true, + "types": ["node"] }, "exclude": ["node_modules/**/*", "dist/**/*", "test"] }