mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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).
This commit is contained in:
@@ -412,6 +412,10 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
|
||||
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
|
||||
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
|
||||
|
||||
# Skip the postinstall native-runtime warm-up (useful in CI / headless installs). Default: 0.
|
||||
# Used by: scripts/postinstall.mjs.
|
||||
#OMNIROUTE_SKIP_POSTINSTALL=0
|
||||
|
||||
# Force a DB healthcheck regardless of cadence. Default: 0.
|
||||
# Used by: src/lib/db/core.ts::shouldRunDbHealthCheck().
|
||||
#OMNIROUTE_FORCE_DB_HEALTHCHECK=0
|
||||
|
||||
19
bin/cli/runtime/index.mjs
Normal file
19
bin/cli/runtime/index.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import { loadSqliteRuntime } from "./sqliteRuntime.mjs";
|
||||
|
||||
let warmed = false;
|
||||
|
||||
/**
|
||||
* Pre-resolves native runtimes at startup so the first DB access is fast
|
||||
* and EBUSY-resilient on Windows.
|
||||
*
|
||||
* Tray runtime (systray2) is warmed lazily by initTray() in bin/cli/tray/.
|
||||
*/
|
||||
export async function warmUpRuntimes() {
|
||||
if (warmed) return;
|
||||
warmed = true;
|
||||
try {
|
||||
await loadSqliteRuntime();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
export { loadSqliteRuntime };
|
||||
48
bin/cli/runtime/magicBytes.mjs
Normal file
48
bin/cli/runtime/magicBytes.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
|
||||
/**
|
||||
* Validates that a file starts with a known native-binary magic number.
|
||||
* Returns the platform label ("elf" | "macho" | "macho-le" | "macho-fat" | "pe")
|
||||
* or null if unrecognized / missing / unreadable.
|
||||
*
|
||||
* Ref: 9router/cli/hooks/sqliteRuntime.js (algorithm origin).
|
||||
*/
|
||||
export function validateBinaryMagic(path) {
|
||||
if (!existsSync(path)) return null;
|
||||
let buf;
|
||||
try {
|
||||
buf = readFileSync(path, { encoding: null }).subarray(0, 16);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (buf.length < 4) return null;
|
||||
|
||||
// ELF: 7F 45 4C 46
|
||||
if (buf[0] === 0x7f && buf[1] === 0x45 && buf[2] === 0x4c && buf[3] === 0x46) return "elf";
|
||||
|
||||
// Mach-O big-endian: FE ED FA CF (64-bit) / FE ED FA CE (32-bit)
|
||||
if (buf[0] === 0xfe && buf[1] === 0xed && buf[2] === 0xfa && buf[3] === 0xcf) return "macho";
|
||||
if (buf[0] === 0xfe && buf[1] === 0xed && buf[2] === 0xfa && buf[3] === 0xce) return "macho";
|
||||
|
||||
// Mach-O little-endian: CF FA ED FE (64-bit) / CE FA ED FE (32-bit)
|
||||
if (buf[0] === 0xcf && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) return "macho-le";
|
||||
if (buf[0] === 0xce && buf[1] === 0xfa && buf[2] === 0xed && buf[3] === 0xfe) return "macho-le";
|
||||
|
||||
// Mach-O fat (universal binary): CA FE BA BE
|
||||
if (buf[0] === 0xca && buf[1] === 0xfe && buf[2] === 0xba && buf[3] === 0xbe) return "macho-fat";
|
||||
|
||||
// PE (Windows .node — DLL): MZ at offset 0
|
||||
if (buf[0] === 0x4d && buf[1] === 0x5a) return "pe";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected magic label for the current platform.
|
||||
* Used to validate that a runtime-installed binary matches this OS.
|
||||
*/
|
||||
export function platformBinaryLabel() {
|
||||
if (process.platform === "win32") return "pe";
|
||||
if (process.platform === "darwin") return "macho";
|
||||
return "elf";
|
||||
}
|
||||
126
bin/cli/runtime/sqliteRuntime.mjs
Normal file
126
bin/cli/runtime/sqliteRuntime.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
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;
|
||||
}
|
||||
80
docs/ops/SQLITE_RUNTIME.md
Normal file
80
docs/ops/SQLITE_RUNTIME.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# SQLite Runtime Resolution
|
||||
|
||||
OmniRoute resolves its SQLite driver at startup through a 5-step fallback chain:
|
||||
|
||||
1. **Bundled `better-sqlite3`** (via `dependencies` in `package.json`)
|
||||
— fastest, native binary, installed by `npm install` when build tools are present.
|
||||
|
||||
2. **Runtime-installed `better-sqlite3`** (in `~/.omniroute/runtime/`)
|
||||
— installed lazily on first run **OR** by `scripts/build/postinstall.mjs → scripts/postinstall.mjs`.
|
||||
Validates native `.node` magic bytes (ELF / Mach-O / PE) before loading
|
||||
to guard against corrupt or wrong-platform binaries.
|
||||
|
||||
3. **`node:sqlite`** (Node ≥22.5 stdlib) — no native build needed; used when
|
||||
both better-sqlite3 paths fail. Limited feature set.
|
||||
|
||||
4. **`sql.js`** (WASM) — final fallback. Works everywhere but is slower
|
||||
and writes data on an interval rather than synchronously.
|
||||
|
||||
## Why this complexity?
|
||||
|
||||
- **Windows EBUSY**: `npm install -g omniroute@latest` can fail if the previous
|
||||
version's `better_sqlite3.node` is locked by a running process. The runtime
|
||||
install in `~/.omniroute/runtime/` sidesteps the global npm cache.
|
||||
- **No build tools**: Some environments (corporate Windows without VS Build
|
||||
Tools, minimal Docker images) cannot compile `better-sqlite3`. The runtime
|
||||
installer resolves a pre-built binary from the npm registry; the fallback
|
||||
drivers ensure OmniRoute still boots even if that fails.
|
||||
- **Air-gapped systems**: If the npm registry is unreachable, `node:sqlite`
|
||||
or `sql.js` guarantee baseline functionality.
|
||||
|
||||
## Magic-byte validation
|
||||
|
||||
Before loading a runtime-installed `.node` file, OmniRoute reads the first 8
|
||||
bytes and matches against known platform magics:
|
||||
|
||||
| Platform | Bytes (hex) | Label |
|
||||
| --------------------- | ------------- | ----------- |
|
||||
| Linux | `7F 45 4C 46` | `elf` |
|
||||
| macOS 64-bit BE | `FE ED FA CF` | `macho` |
|
||||
| macOS 64-bit LE | `CF FA ED FE` | `macho-le` |
|
||||
| macOS fat (universal) | `CA FE BA BE` | `macho-fat` |
|
||||
| Windows | `4D 5A` (MZ) | `pe` |
|
||||
|
||||
A mismatched magic → file is ignored, fallback continues to the next step.
|
||||
|
||||
## Checking the active driver
|
||||
|
||||
```typescript
|
||||
import { getDriverInfo } from "@/lib/db/core";
|
||||
|
||||
const info = getDriverInfo();
|
||||
// { source: "bundled" | "runtime" | "runtime-installed-now" | "node-sqlite" | "sql-js",
|
||||
// kind: "better-sqlite3" | "node-sqlite" | "sql-js" }
|
||||
```
|
||||
|
||||
## Manual control
|
||||
|
||||
```bash
|
||||
# Skip postinstall warm-up (for fast CI installs)
|
||||
OMNIROUTE_SKIP_POSTINSTALL=1 npm install -g omniroute
|
||||
|
||||
# Force-reinstall runtime better-sqlite3
|
||||
rm -rf ~/.omniroute/runtime
|
||||
omniroute # will reinstall on next start
|
||||
|
||||
# Check what driver is active
|
||||
omniroute config db-info # (if CLI command exists)
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
Pattern adapted from `9router/cli/hooks/sqliteRuntime.js`.
|
||||
|
||||
Implementation:
|
||||
|
||||
- `bin/cli/runtime/magicBytes.mjs` — binary magic-byte validation helpers
|
||||
- `bin/cli/runtime/sqliteRuntime.mjs` — 5-step runtime resolver + lazy installer
|
||||
- `bin/cli/runtime/index.mjs` — startup orchestrator (`warmUpRuntimes()`)
|
||||
- `scripts/postinstall.mjs` — npm post-install hook (non-fatal warm-up)
|
||||
- `src/lib/db/core.ts` — `ensureDbInitialized()` / `getDriverInfo()` exports
|
||||
@@ -78,19 +78,20 @@ echo "OMNIROUTE_WS_BRIDGE_SECRET=$(openssl rand -base64 32)"
|
||||
|
||||
OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These variables control data location, encryption, and lifecycle.
|
||||
|
||||
| Variable | Default | Source File | Description |
|
||||
| -------------------------------------- | -------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `DATA_DIR` | `~/.omniroute/` | `src/lib/db/core.ts` | Root directory for SQLite DB, backups, and data files. Override for Docker volumes or custom paths. |
|
||||
| `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. |
|
||||
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. |
|
||||
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips the automatic database backup that runs before migrations on every startup. |
|
||||
| `OMNIROUTE_CRYPT_KEY` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** for `STORAGE_ENCRYPTION_KEY`. Accepted as a fallback when the primary variable is absent. |
|
||||
| `OMNIROUTE_API_KEY_BASE64` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** (Base64-encoded form) accepted as a fallback. Decoded automatically before use. |
|
||||
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(unset)_ | `src/lib/db/core.ts` | Override the periodic SQLite healthcheck interval (ms). When unset, defaults are derived from `NODE_ENV`. |
|
||||
| `OMNIROUTE_FORCE_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts` | Set to `1` to force the DB healthcheck loop on, even when it would normally be skipped (e.g., short-lived tasks). |
|
||||
| `OMNIROUTE_MIGRATIONS_DIR` | _(auto-detect)_ | `src/lib/db/migrationRunner.ts` | Override the directory that the migration runner scans. Useful when shipping bundled migrations in custom builds. |
|
||||
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Flush interval (ms) for the batched spend/cost writer. Lower values reduce write coalescing; higher values reduce DB contention. |
|
||||
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Max buffered spend entries before a forced flush. Raise on high-QPS deployments; lower when bounded memory matters more. |
|
||||
| Variable | Default | Source File | Description |
|
||||
| -------------------------------------- | -------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `DATA_DIR` | `~/.omniroute/` | `src/lib/db/core.ts` | Root directory for SQLite DB, backups, and data files. Override for Docker volumes or custom paths. |
|
||||
| `STORAGE_ENCRYPTION_KEY` | _(empty = disabled)_ | `src/lib/db/encryption.ts` | AES key for full SQLite database encryption at rest. Generate with `openssl rand -hex 32`. |
|
||||
| `STORAGE_ENCRYPTION_KEY_VERSION` | `v1` | `scripts/build/bootstrap-env.mjs`, `electron/main.js` | Version label for the encryption key. Increment when performing key rotation to support decryption of old backups. |
|
||||
| `DISABLE_SQLITE_AUTO_BACKUP` | `false` | `src/lib/db/backup.ts` | When `true`, skips the automatic database backup that runs before migrations on every startup. |
|
||||
| `OMNIROUTE_CRYPT_KEY` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** for `STORAGE_ENCRYPTION_KEY`. Accepted as a fallback when the primary variable is absent. |
|
||||
| `OMNIROUTE_API_KEY_BASE64` | _(unset)_ | `src/lib/db/encryption.ts` | **Legacy alias** (Base64-encoded form) accepted as a fallback. Decoded automatically before use. |
|
||||
| `OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS` | _(unset)_ | `src/lib/db/core.ts` | Override the periodic SQLite healthcheck interval (ms). When unset, defaults are derived from `NODE_ENV`. |
|
||||
| `OMNIROUTE_FORCE_DB_HEALTHCHECK` | `0` | `src/lib/db/core.ts` | Set to `1` to force the DB healthcheck loop on, even when it would normally be skipped (e.g., short-lived tasks). |
|
||||
| `OMNIROUTE_SKIP_POSTINSTALL` | `0` | `scripts/postinstall.mjs` | Set to `1` to skip the native-runtime warm-up during `npm install`. Useful in CI/headless installs where sqlite is already built. |
|
||||
| `OMNIROUTE_MIGRATIONS_DIR` | _(auto-detect)_ | `src/lib/db/migrationRunner.ts` | Override the directory that the migration runner scans. Useful when shipping bundled migrations in custom builds. |
|
||||
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Flush interval (ms) for the batched spend/cost writer. Lower values reduce write coalescing; higher values reduce DB contention. |
|
||||
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Max buffered spend entries before a forced flush. Raise on high-QPS deployments; lower when bounded memory matters more. |
|
||||
|
||||
### Scenarios
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
"src/shared/utils/nodeRuntimeSupport.ts",
|
||||
".env.example",
|
||||
"scripts/build/postinstall.mjs",
|
||||
"bin/cli/runtime/",
|
||||
"scripts/postinstall.mjs",
|
||||
"scripts/build/postinstallSupport.mjs",
|
||||
"scripts/dev/responses-ws-proxy.mjs",
|
||||
"scripts/check/check-supported-node-runtime.ts",
|
||||
|
||||
@@ -314,3 +314,11 @@ await fixBetterSqliteBinary();
|
||||
await fixWreqJsBinary();
|
||||
await ensureSwcHelpers();
|
||||
await syncProjectEnv();
|
||||
|
||||
// Warm up native runtimes (better-sqlite3 in ~/.omniroute/runtime/).
|
||||
// Non-fatal: errors are caught inside postinstall.mjs.
|
||||
try {
|
||||
await import("../postinstall.mjs");
|
||||
} catch {
|
||||
// Silently skip — runtime warm-up is best-effort.
|
||||
}
|
||||
|
||||
27
scripts/postinstall.mjs
Normal file
27
scripts/postinstall.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Post-install warm-up for OmniRoute native runtimes.
|
||||
*
|
||||
* Runs after the main `scripts/build/postinstall.mjs` binary-copy step.
|
||||
* Tries to pre-resolve better-sqlite3 into ~/.omniroute/runtime/ so that
|
||||
* the first execution is fast and EBUSY-resilient on Windows.
|
||||
*
|
||||
* Non-fatal: any error is printed as a warning and install continues.
|
||||
*/
|
||||
|
||||
if (
|
||||
process.env.OMNIROUTE_SKIP_POSTINSTALL === "1" ||
|
||||
process.env.CI === "true" ||
|
||||
process.env.CI === "1"
|
||||
) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const { warmUpRuntimes } = await import("../bin/cli/runtime/index.mjs");
|
||||
await warmUpRuntimes();
|
||||
} catch (err) {
|
||||
console.warn(`[omniroute] postinstall warm-up skipped: ${err?.message ?? err}`);
|
||||
}
|
||||
})();
|
||||
@@ -1398,6 +1398,49 @@ export function resetDbInstance() {
|
||||
closeDbInstance();
|
||||
}
|
||||
|
||||
// ──────────────── Runtime Driver Info ────────────────
|
||||
|
||||
type DbDriverInfo = { source: string; kind: string };
|
||||
let driverInfoCached: DbDriverInfo | null = null;
|
||||
|
||||
function setDriverInfo(info: DbDriverInfo) {
|
||||
driverInfoCached = info;
|
||||
}
|
||||
|
||||
/** Returns how better-sqlite3 was resolved (bundled / runtime / etc.). Null if not yet init. */
|
||||
export function getDriverInfo(): DbDriverInfo | null {
|
||||
return driverInfoCached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async initializer that pre-resolves the SQLite runtime before first DB access.
|
||||
*
|
||||
* Call this at process startup (before any call to getDbInstance()) so that
|
||||
* if the bundled better-sqlite3 binary is unavailable, the runtime installer
|
||||
* can place it in ~/.omniroute/runtime/ without blocking a synchronous caller.
|
||||
*
|
||||
* Idempotent — safe to call multiple times.
|
||||
*/
|
||||
export async function ensureDbInitialized(): Promise<void> {
|
||||
if (getDb()) return;
|
||||
|
||||
try {
|
||||
const runtimeModule = await import("../../../bin/cli/runtime/sqliteRuntime.mjs" as any);
|
||||
const { driver, source } = await runtimeModule.loadSqliteRuntime();
|
||||
setDriverInfo({ source, kind: driver.kind as string });
|
||||
if ((driver.kind as string) !== "better-sqlite3") {
|
||||
console.warn(
|
||||
`[DB] better-sqlite3 unavailable (resolved via ${source}/${driver.kind}). ` +
|
||||
`OmniRoute may fall back to read-only or limited functionality.`
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Runtime loader unavailable (CLI context only) — DB init falls through to normal path.
|
||||
}
|
||||
|
||||
getDbInstance();
|
||||
}
|
||||
|
||||
// ──────────────── JSON → SQLite Migration ────────────────
|
||||
|
||||
function migrateFromJson(db: SqliteDatabase, jsonPath: string) {
|
||||
|
||||
62
tests/unit/runtime/magicBytes.test.ts
Normal file
62
tests/unit/runtime/magicBytes.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { validateBinaryMagic, platformBinaryLabel } from "../../../bin/cli/runtime/magicBytes.mjs";
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), "magic-test-"));
|
||||
|
||||
test("detects ELF", () => {
|
||||
const p = join(dir, "fake.so");
|
||||
writeFileSync(p, Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0, 0, 0, 0]));
|
||||
assert.equal(validateBinaryMagic(p), "elf");
|
||||
});
|
||||
|
||||
test("detects Mach-O 64-bit BE", () => {
|
||||
const p = join(dir, "fake.dylib");
|
||||
writeFileSync(p, Buffer.from([0xfe, 0xed, 0xfa, 0xcf, 0, 0, 0, 0]));
|
||||
assert.equal(validateBinaryMagic(p), "macho");
|
||||
});
|
||||
|
||||
test("detects Mach-O LE", () => {
|
||||
const p = join(dir, "fake-le.dylib");
|
||||
writeFileSync(p, Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 0, 0, 0, 0]));
|
||||
assert.equal(validateBinaryMagic(p), "macho-le");
|
||||
});
|
||||
|
||||
test("detects Mach-O fat binary", () => {
|
||||
const p = join(dir, "fake.fat");
|
||||
writeFileSync(p, Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 0]));
|
||||
assert.equal(validateBinaryMagic(p), "macho-fat");
|
||||
});
|
||||
|
||||
test("detects PE (MZ)", () => {
|
||||
const p = join(dir, "fake.node");
|
||||
writeFileSync(p, Buffer.from([0x4d, 0x5a, 0, 0, 0, 0, 0, 0]));
|
||||
assert.equal(validateBinaryMagic(p), "pe");
|
||||
});
|
||||
|
||||
test("returns null for non-binary", () => {
|
||||
const p = join(dir, "fake.txt");
|
||||
writeFileSync(p, "hello world", "utf-8");
|
||||
assert.equal(validateBinaryMagic(p), null);
|
||||
});
|
||||
|
||||
test("returns null for missing file", () => {
|
||||
assert.equal(validateBinaryMagic(join(dir, "nope.bin")), null);
|
||||
});
|
||||
|
||||
test("returns null for too-short file", () => {
|
||||
const p = join(dir, "short.bin");
|
||||
writeFileSync(p, Buffer.from([0x7f, 0x45]));
|
||||
assert.equal(validateBinaryMagic(p), null);
|
||||
});
|
||||
|
||||
test("platformBinaryLabel matches process.platform", () => {
|
||||
const expected =
|
||||
process.platform === "win32" ? "pe" : process.platform === "darwin" ? "macho" : "elf";
|
||||
assert.equal(platformBinaryLabel(), expected);
|
||||
});
|
||||
|
||||
test.after(() => rmSync(dir, { recursive: true, force: true }));
|
||||
48
tests/unit/runtime/sqliteRuntime.test.ts
Normal file
48
tests/unit/runtime/sqliteRuntime.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { loadSqliteRuntime, clearRuntimeCache } from "../../../bin/cli/runtime/sqliteRuntime.mjs";
|
||||
|
||||
test("loadSqliteRuntime returns a result with driver and source", async () => {
|
||||
clearRuntimeCache();
|
||||
const r = await loadSqliteRuntime();
|
||||
assert.ok(r, "result is truthy");
|
||||
assert.ok(typeof r.driver === "object", "driver is object");
|
||||
assert.ok(typeof r.source === "string", "source is string");
|
||||
});
|
||||
|
||||
test("loaded driver is one of the known kinds", async () => {
|
||||
clearRuntimeCache();
|
||||
const r = await loadSqliteRuntime();
|
||||
assert.ok(
|
||||
["better-sqlite3", "node-sqlite", "sql-js"].includes(r.driver.kind),
|
||||
`kind="${r.driver.kind}" must be known`
|
||||
);
|
||||
});
|
||||
|
||||
test("loadSqliteRuntime caches the result (same object reference)", async () => {
|
||||
clearRuntimeCache();
|
||||
const a = await loadSqliteRuntime();
|
||||
const b = await loadSqliteRuntime();
|
||||
assert.strictEqual(a, b, "second call returns cached object");
|
||||
});
|
||||
|
||||
test("runtime dir exists after loadSqliteRuntime when runtime source used", async () => {
|
||||
clearRuntimeCache();
|
||||
const r = await loadSqliteRuntime();
|
||||
if (r.source === "runtime-installed-now" || r.source === "runtime") {
|
||||
const runtimeDir = join(homedir(), ".omniroute", "runtime");
|
||||
assert.ok(existsSync(runtimeDir), "runtime dir created");
|
||||
}
|
||||
});
|
||||
|
||||
test("clearRuntimeCache allows fresh resolution", async () => {
|
||||
clearRuntimeCache();
|
||||
const first = await loadSqliteRuntime();
|
||||
clearRuntimeCache();
|
||||
const second = await loadSqliteRuntime();
|
||||
// Both should resolve to the same kind, but are different call results
|
||||
assert.equal(first.driver.kind, second.driver.kind, "same driver kind after cache clear");
|
||||
});
|
||||
Reference in New Issue
Block a user