mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-06 12:27:39 +00:00
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
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 = <T extends (...args: any[]) => 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 });
|
|
}
|
|
}
|