68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
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;
|
|
}
|
|
}
|