Files
OmniRoute/src/lib/db/adapters/nodeSqliteAdapter.ts
anhtahaylove 99fb441434 fix(db): release process listeners when a node:sqlite adapter closes (#13109)
Right precedent — #7494 fixed exactly this for the sql.js adapter and the `node:sqlite` one never got the same treatment, even though it is the default driver whenever better-sqlite3 is unavailable. `/api/db-backups/import` opening a throwaway adapter per request makes it reachable.


I reformatted the changelog fragment to the `changelog.d` convention (`- **fix(scope):** …`) before merging — `check:changelog-integrity` rejects a fragment that does not start with a markdown bullet, which is the same gate your #13158 was about. Wording is yours, unchanged in substance.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:30:32 -03:00

69 lines
2.2 KiB
TypeScript

import type { SqliteAdapter } from "./types";
import {
createNodeSqliteAdapterFromDatabase,
type NodeSqliteDatabaseLike,
} from "./nodeSqliteShared";
const CHECKPOINT_INTERVAL_MS = 60_000;
export async function createNodeSqliteAdapter(filePath: string): Promise<SqliteAdapter> {
// Suprimir ExperimentalWarning
const origEmit = process.emit.bind(process);
(process as NodeJS.Process).emit = function (name: string, ...args: unknown[]) {
if (
name === "warning" &&
args[0] !== null &&
typeof args[0] === "object" &&
"name" in (args[0] as object) &&
(args[0] as { name: string }).name === "ExperimentalWarning"
) {
return false;
}
return origEmit(name as never, ...(args as never[]));
} as typeof process.emit;
const { DatabaseSync } = (await import("node:sqlite" as never)) as {
DatabaseSync: new (path: string) => NodeSqliteDatabaseLike;
};
const db = new DatabaseSync(filePath);
const checkpointTimer = setInterval(() => {
try {
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
} catch {}
}, CHECKPOINT_INTERVAL_MS);
(checkpointTimer as unknown as NodeJS.Timeout).unref?.();
// Declared before gracefulClose so the close path can detach them. Without
// this, every closed adapter leaves three closures pinned on `process` --
// each holding this adapter and its DatabaseSync handle alive -- and short-
// lived adapters (POST /api/db-backups/import opens one per request) trip
// Node's MaxListenersExceededWarning. #7494 fixed exactly this for sql.js.
const onBeforeExit = () => {
adapter.close();
};
const onSignal = () => {
adapter.close();
process.exit(0);
};
function gracefulClose() {
clearInterval(checkpointTimer as unknown as NodeJS.Timeout);
try {
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
} catch {}
process.removeListener("beforeExit", onBeforeExit);
process.removeListener("SIGINT", onSignal);
process.removeListener("SIGTERM", onSignal);
}
const adapter = createNodeSqliteAdapterFromDatabase(db, filePath, gracefulClose);
process.once("beforeExit", onBeforeExit);
process.once("SIGINT", onSignal);
process.once("SIGTERM", onSignal);
return adapter;
}