Files
OmniRoute/bin/cli/runtime/sqliteRuntime.mjs
diegosouzapw 8f915b18b0 feat(runtime): dynamic SQLite runtime installer with 5-step fallback chain
Adds bin/cli/runtime/sqliteRuntime.mjs that resolves better-sqlite3 from:
(1) bundled optionalDependency, (2) ~/.omniroute/runtime/ install,
(3) lazy npm install into runtime dir, (4) node:sqlite stdlib (Node >=22.5),
(5) bundled sql.js WASM. Each native binary is validated against expected
platform magic bytes (ELF/Mach-O/PE) before load.

Adds bin/cli/runtime/magicBytes.mjs with validateBinaryMagic() helper
(9 tests). Adds bin/cli/runtime/index.mjs as warmUpRuntimes() orchestrator.

Adds scripts/postinstall.mjs warm-up hook (non-fatal, skipped in CI).
Integrates it as the last step of scripts/build/postinstall.mjs.

Extends src/lib/db/core.ts with ensureDbInitialized() (async, idempotent)
and getDriverInfo() so the startup orchestrator can await the resolver
before any DB access, enabling graceful degradation without crashing the
process on missing better-sqlite3.

Solves Windows EBUSY error on 'npm install -g omniroute@latest' while the
previous version is still running, and works in environments without C++
build tools or with unreachable npm registry.

Documents OMNIROUTE_SKIP_POSTINSTALL in .env.example and ENVIRONMENT.md.

Ref: 9router/cli/hooks/sqliteRuntime.js (pattern origin).
2026-05-15 00:05:49 -03:00

127 lines
3.8 KiB
JavaScript

import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { execSync } from "node:child_process";
import { validateBinaryMagic, platformBinaryLabel } from "./magicBytes.mjs";
const RUNTIME_DIR = join(homedir(), ".omniroute", "runtime");
const BETTER_SQLITE3_VERSION = "better-sqlite3@^12.6.2";
let resolvedCached = null;
/**
* Resolves a SQLite driver through a 5-step fallback chain:
* 1. Bundled better-sqlite3 (optionalDependency)
* 2. Runtime-installed better-sqlite3 in ~/.omniroute/runtime/
* 3. Lazy npm install into runtime dir
* 4. node:sqlite (Node ≥22.5 stdlib)
* 5. sql.js (bundled WASM, always available)
*
* Returns { driver, source } where source is one of:
* "bundled" | "runtime" | "runtime-installed-now" | "node-sqlite" | "sql-js"
*/
export async function loadSqliteRuntime() {
if (resolvedCached) return resolvedCached;
const bundled = await tryLoadBundled();
if (bundled) {
resolvedCached = { driver: bundled, source: "bundled" };
return resolvedCached;
}
const runtimeInstalled = await tryLoadRuntimeInstalled();
if (runtimeInstalled) {
resolvedCached = { driver: runtimeInstalled, source: "runtime" };
return resolvedCached;
}
try {
await installRuntime();
const after = await tryLoadRuntimeInstalled();
if (after) {
resolvedCached = { driver: after, source: "runtime-installed-now" };
return resolvedCached;
}
} catch (err) {
console.warn(`[omniroute] runtime install failed: ${err.message}`);
}
try {
const nodeSqlite = await import("node:sqlite");
resolvedCached = {
driver: { kind: "node-sqlite", DatabaseSync: nodeSqlite.DatabaseSync },
source: "node-sqlite",
};
return resolvedCached;
} catch {}
const sqljs = await import("sql.js");
resolvedCached = {
driver: { kind: "sql-js", initSqlJs: sqljs.default ?? sqljs.initSqlJs },
source: "sql-js",
};
return resolvedCached;
}
async function tryLoadBundled() {
try {
const mod = await import("better-sqlite3");
return { kind: "better-sqlite3", Database: mod.default ?? mod };
} catch {
return null;
}
}
async function tryLoadRuntimeInstalled() {
const pkgRoot = join(RUNTIME_DIR, "node_modules", "better-sqlite3");
if (!existsSync(join(pkgRoot, "package.json"))) return null;
const buildDir = join(pkgRoot, "build", "Release");
if (existsSync(buildDir)) {
const nodeFile = readdirSync(buildDir).find((f) => f.endsWith(".node"));
if (nodeFile) {
const magic = validateBinaryMagic(join(buildDir, nodeFile));
const expected = platformBinaryLabel();
if (!magic || (magic !== expected && magic !== "macho-le" && magic !== "macho-fat")) {
console.warn(
`[omniroute] runtime sqlite binary magic mismatch (${magic}${expected}) — skipping`
);
return null;
}
}
}
try {
const mod = await import(pkgRoot);
return { kind: "better-sqlite3", Database: mod.default ?? mod };
} catch {
return null;
}
}
function ensureRuntimeDir() {
if (!existsSync(RUNTIME_DIR)) mkdirSync(RUNTIME_DIR, { recursive: true });
const pkg = join(RUNTIME_DIR, "package.json");
if (!existsSync(pkg)) {
writeFileSync(
pkg,
JSON.stringify({ name: "omniroute-runtime", private: true, type: "commonjs" }),
"utf-8"
);
}
}
async function installRuntime() {
ensureRuntimeDir();
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
execSync(
`${npm} install --prefix "${RUNTIME_DIR}" ${BETTER_SQLITE3_VERSION} --no-audit --no-fund --silent`,
{ stdio: ["ignore", "ignore", "pipe"], timeout: 180_000 }
);
}
/** Clears the cached resolved driver (for testing). */
export function clearRuntimeCache() {
resolvedCached = null;
}