import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3"; import Database from "better-sqlite3"; import type BetterSqlite3 from "better-sqlite3"; import * as schema from "./schema/schema"; import path from "path"; import fs from "fs"; import { APP_PATH } from "@server/lib/consts"; import { existsSync, mkdirSync } from "fs"; export const location = path.join(APP_PATH, "db", "db.sqlite"); export const exists = checkFileExists(location); bootstrapVolume(); /** * Wraps better-sqlite3 Statement to call `finalize()` immediately after * execution, freeing native sqlite3_stmt memory deterministically instead * of waiting for GC. Fixes steady off-heap growth under load (#2120). * WARNING: Finalizes after first execution — incompatible with drizzle's * reusable .prepare() builders. No such usage exists in this codebase. */ function autoFinalizeStatement( stmt: BetterSqlite3.Statement ): BetterSqlite3.Statement { const wrapExec = any>(fn: T): T => { return function (this: any, ...args: any[]) { try { return fn.apply(this, args); } finally { try { // finalize() exists on the native Statement at runtime but // is missing from @types/better-sqlite3. (stmt as any).finalize(); } catch { // Already finalized — harmless } } } as unknown as T; }; stmt.run = wrapExec(stmt.run); stmt.get = wrapExec(stmt.get); stmt.all = wrapExec(stmt.all); return stmt; } function createDb() { const sqlite = new Database(location); // Enable WAL mode — allows concurrent readers + single writer, preventing // contention across subsystems (verifySession, Traefik, audit, ping). sqlite.pragma("journal_mode = WAL"); // Wait up to 5s on SQLITE_BUSY instead of failing — prevents audit log // retry loops that accumulate memory. sqlite.pragma("busy_timeout = 5000"); // NORMAL sync mode: safe with WAL, reduces write lock hold time. sqlite.pragma("synchronous = NORMAL"); // 64 MB page cache (default 2 MB) — reduces I/O round-trips on large // TraefikConfigManager JOINs that block the event loop. sqlite.pragma("cache_size = -65536"); // 256 MB memory-mapped I/O — OS serves reads from page cache directly, // reducing event-loop blocking. sqlite.pragma("mmap_size = 268435456"); // Wrap prepare() so every drizzle-orm statement is auto-finalized after // first use, preventing sqlite3_stmt accumulation between GC cycles. const originalPrepare = sqlite.prepare.bind(sqlite); (sqlite as any).prepare = function autoFinalizePrepare(source: string) { return autoFinalizeStatement(originalPrepare(source)); }; return DrizzleSqlite(sqlite, { schema }); } export const db = createDb(); export default db; export const primaryDb = db; export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] >[0]; export const DB_TYPE: "pg" | "sqlite" = "sqlite"; function checkFileExists(filePath: string): boolean { try { fs.accessSync(filePath); return true; } catch { return false; } } function bootstrapVolume() { const appPath = APP_PATH; const dbDir = path.join(appPath, "db"); const logsDir = path.join(appPath, "logs"); // check if the db directory exists and create it if it doesn't if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } // check if the logs directory exists and create it if it doesn't if (!existsSync(logsDir)) { mkdirSync(logsDir, { recursive: true }); } // THIS IS FOR TRAEFIK; NOT REALLY NEEDED, BUT JUST IN CASE const traefikDir = path.join(appPath, "traefik"); // check if the traefik directory exists and create it if it doesn't if (!existsSync(traefikDir)) { mkdirSync(traefikDir, { recursive: true }); } }