import Database from 'better-sqlite3'; import { mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { config } from '../config.js'; import { migrations } from './migrations.js'; import { runMigrations } from './migration-runner.js'; import { tuneConnection, tuneMigrationConnection } from './connection.js'; import { timeStatement } from './slow-statements.js'; let _db: Database.Database | null = null; let optimizeTimer: NodeJS.Timeout | undefined; export function db(): Database.Database { if (_db) return _db; mkdirSync(dirname(resolve(config.dbPath)), { recursive: true }); const handle = new Database(config.dbPath); // Do all the risky init on a local handle first — only assign the module // singleton once migrations succeed. A half-migrated handle that's cached // silently breaks every subsequent query without a chance to retry. try { console.info('[database] startup migrations starting'); handle.pragma('journal_mode = WAL'); handle.pragma('auto_vacuum = INCREMENTAL'); handle.pragma('foreign_keys = ON'); tuneMigrationConnection(handle); runMigrations(handle, migrations); handle.pragma('optimize = 0x10002'); handle.pragma('shrink_memory'); tuneConnection(handle); console.info('[database] startup migrations complete'); } catch (err) { console.error('[database] startup migrations failed', err); handle.close(); throw err; } // Instrument runtime queries after migrations. Each prepare must return a // fresh statement: modes, permanent bindings, and active iterators are mutable // state owned by the caller, even when another caller uses identical SQL. const slowMs = Number(process.env.EC_SLOW_SQL_MS ?? 0); if (Number.isFinite(slowMs) && slowMs > 0) { const prepare = handle.prepare.bind(handle); handle.prepare = ((sql: string) => timeStatement(prepare(sql), sql, slowMs)) as typeof handle.prepare; } _db = handle; optimizeTimer = setInterval(() => { try { handle.pragma('optimize'); } catch (error) { console.error('[database] optimize failed:', error); } }, 60 * 60_000); optimizeTimer.unref(); return _db; } export function closeDb(): void { clearInterval(optimizeTimer); optimizeTimer = undefined; if (_db) { _db.close(); _db = null; } }