import { SqlJsStatic, Database } from 'sql.js'; import { SqlDriver, SqlDriverConfig } from '@objectstack/driver-sql'; /** * Thin wrapper over sql.js {@link Database} that mimics the surface of * `better-sqlite3`'s `Database` (only the methods the Knex dialect uses). * * Persistence is handled here, not in the Knex dialect, so it can be * orchestrated per-connection without polluting the SQL execution path. */ /** When to flush the in-memory WASM database to disk. */ type PersistMode = 'on-disconnect' | 'on-write' | `debounced:${number}`; interface WasmConnectionOptions { /** * On-disk file path. `:memory:` (or any value starting with `:`) skips * persistence entirely and the database lives only for the process. */ filename: string; /** When to persist. Default: `on-disconnect`. */ persist?: PersistMode; /** Pre-loaded sql.js module. If omitted, loaded lazily on first connect. */ sqlJs?: SqlJsStatic; /** * Optional override for the `.wasm` locator passed to `initSqlJs()`. * Defaults to resolving the file from the `sql.js` package on disk * (works in Node and WebContainer). */ locateFile?: (file: string) => string; /** Optional logger; defaults to `console`. */ logger?: { warn: (msg: string, meta?: unknown) => void; }; } /** * A sql.js-backed connection that exposes the `prepare`/`exec`/`close` * subset used by Knex's SQLite dialect. Mutations are queued through a * configurable persistence strategy so the on-disk file stays in sync. */ declare class WasmSqliteConnection { /** * Process-wide counter making each atomic-write temp filename unique, so * concurrent connections (or overlapping flushes) never target the same * temp path. Combined with `process.pid` for cross-process uniqueness. */ private static tmpSeq; readonly filename: string; readonly persist: PersistMode; readonly isEphemeral: boolean; private db; private fs; private dirty; private debounceMs; private debounceTimer; private flushChain; private destroyed; private logger; /** * Whether a `BEGIN…COMMIT/ROLLBACK` transaction is currently open. Tracked * because sql.js's {@link Database.export} closes and reopens the database * (it has no in-place serialize), and closing a connection rolls back any * open transaction. Flushing mid-transaction would therefore silently * abort it, leaving the eventual `COMMIT` to fail with * "cannot commit - no transaction is active". We defer the flush until the * transaction fully closes. See {@link noteTransactionControl}. */ private rootTxActive; /** Open `SAVEPOINT` depth (nested transactions emitted by Knex). */ private savepointDepth; /** A flush was requested while a transaction was open; run it on close. */ private flushDeferred; /** True while any transaction (root or savepoint) is in flight. */ private get inTransaction(); constructor(opts: WasmConnectionOptions); /** Open the underlying sql.js database, loading bytes from disk if any. */ open(sqlJs?: SqlJsStatic, locateFile?: (file: string) => string): Promise; /** * Force sql.js to actually read a page so a malformed image surfaces now * rather than on the first business query. `PRAGMA quick_check` walks the * b-tree structure without the full-scan cost of `integrity_check`; a healthy * database returns a single `ok` row. Any thrown error (raw string or Error) * or a non-`ok` result is treated as corruption. */ private assertReadable; /** * Move a corrupt database file aside so its bytes are preserved for * post-mortem while a fresh, empty database takes its place. Best-effort: * failures here must not prevent the server from booting on a clean DB. */ private quarantineCorruptFile; /** * Move a write-ahead log left behind by a *real* SQLite aside (#3941). * * The native driver keeps file-backed databases in WAL mode, and a clean close * checkpoints the log away — so a non-empty `-wal` here means the last * process died without one. That log is a problem in both directions, and * neither is something wasm SQLite can fix: it cannot read the log (we load * only the main image, so any transaction still in there is invisible), and it * must not leave it in place either — the next {@link flush} rewrites the * image, and a real SQLite opening a fresh image beside a stale log would * replay frames that no longer belong to it. * * So rename it, which loses nothing recoverable (the bytes are preserved for a * real `sqlite3` to recover from) and disarms the mismatch. Best-effort: this * is a dev-only step-down path and must never prevent a boot. */ private quarantineOrphanedWal; /** * Update transaction state from a transaction-control statement and, when a * transaction has just fully closed, run any flush that was deferred while * it was open. Called by the Knex dialect for every `BEGIN` / `COMMIT` / * `ROLLBACK` / `SAVEPOINT` / `RELEASE` statement. * * We bias toward "in transaction": an unrecognised form leaves the flag set, * which at worst delays a flush (safe) rather than exporting mid-transaction * (which would abort it). */ noteTransactionControl(sql: string): void; /** * Record that the statement just executed CHANGED the database, and schedule * a flush according to {@link persist}. * * Deliberately takes no argument. It used to filter the caller's Knex * `method` against a local write-method allowlist, which made "did this * mutate?" a decision taken in TWO places — here and in the dialect's * execution-path branch — and the two disagreed: an `INSERT … RETURNING` * runs down the dialect's ROW-returning branch (it has rows to return), that * branch never called this method at all, and so a whole class of committed * writes was never marked dirty and never reached disk (#4518). One decision, * one owner: {@link statementMutatesDatabase} in the dialect classifies the * statement, and this method just does what it is told. */ markDirty(): void; /** * Force a write of the current database state to disk. * * Flushes are strictly serialized through a single promise chain: every call * appends an export+write step that runs after all previously-queued steps. * This matters because sql.js `export()` mutates the live connection (it * closes and reopens the database), so two exports must never overlap — and * because the returned promise must not resolve until the caller's own write * has hit disk (deterministic for tests and for `close()`). Each step * re-checks `dirty` at run time, so a no-op write collapses cheaply and a * write that arrived mid-flush is captured by the next queued step. */ flush(): Promise; /** * Write the database bytes to disk atomically: write to a sibling temp file, * fsync it, then `rename()` it over the target. * * A plain `writeFile(this.filename, …)` truncates the target and streams the * new bytes in place, so a process killed mid-write (a dev-server restart, * Ctrl-C, or crash — likely under `on-write`, where every dispatcher tick * flushes) leaves a half-written file. sql.js then rejects that file on the * next boot with "database disk image is malformed". `rename(2)` is atomic * within a filesystem, so a reader always sees either the complete old file * or the complete new one — never a torn image. The temp file lives in the * same directory as the target so the rename stays intra-filesystem. */ private atomicWriteFile; /** Close the database, flushing any pending writes first. */ close(): Promise; /** Access the raw sql.js database (for the Knex dialect). */ get raw(): Database; } /** * SQLite-on-WASM driver for ObjectStack. * * Extends {@link SqlDriver} so all CRUD / schema / introspection / multi-tenant * logic is inherited as-is. Only the Knex transport is swapped to a custom * dialect ({@link Client_WasmSqlite}) backed by sql.js + Node `fs` persistence, * which lets the same `SqlDriver` codepath run inside StackBlitz WebContainer * (Node-in-browser) without the native `better-sqlite3` N-API binding. */ /** Public configuration for {@link SqliteWasmDriver}. */ interface SqliteWasmDriverConfig { /** * SQLite filename. Use `:memory:` for an ephemeral database that is never * persisted. Any other value is treated as a Node `fs` path and the * sql.js database bytes are flushed back to disk according to {@link persist}. */ filename: string; /** * Persistence strategy. Default: `'on-disconnect'`. * * - `'on-disconnect'` — flush once when the driver disconnects (and on * `process.beforeExit`). * - `'on-write'` — flush after every mutation. Safest, slowest. * - `` `debounced:${ms}` `` — debounce flushes by N milliseconds. Good * balance under bursty writes. */ persist?: PersistMode; /** Pre-loaded sql.js module — skips lazy import. */ sqlJs?: SqlJsStatic; /** * Override for sql.js's `locateFile`. Defaults to resolving the `.wasm` * file inside the installed `sql.js` package, which works in Node and * WebContainer. */ locateFile?: (file: string) => string; /** Knex pool overrides. The dialect already defaults to `{ min: 1, max: 1 }`. */ pool?: SqlDriverConfig['pool']; /** Optional logger. Defaults to `console`. */ logger?: WasmConnectionOptions['logger']; } /** * SqlDriver subclass that runs Knex against sql.js (WASM SQLite). * * Behaves identically to the standard SQLite path — the dialect's * {@link Client_WasmSqlite._query} reports `lastID`/`changes` exactly the * way better-sqlite3 does, so {@link SqlDriver}'s SQL generation, returning * clauses, and schema introspection all keep working. */ declare class SqliteWasmDriver extends SqlDriver { readonly name: string; readonly version: string; /** * Force the SQLite branch in {@link SqlDriver}. The base class detects * SQLite by string-matching `config.client`, but we pass the dialect class * directly so the string check would miss. */ protected get isSqlite(): boolean; /** * Never WAL (#3941). The base driver switches a file-backed SQLite database to * WAL so several processes can share one file. Nothing here is shared: the live * database sits in this process's WASM heap, and what reaches disk is a byte * image {@link flush} exports from it — another process reads that snapshot, * never the database. So the pragma buys this transport nothing. * * It is also not free. Journal mode is a persistent header change in the * operator's file, and under WAL the export path's correctness would rest on * sql.js checkpointing the log while `export()` closes and reopens the * database. Measured, it does — no row is lost today — which is why this is a * declined default and not a bug report. But a transport that persists by * serializing an image should not be one implementation detail away from * dropping committed rows for a concurrency benefit it cannot use. * * Declared rather than discovered: sql.js *accepts* `journal_mode = WAL`, * because its VFS is memory-backed, so the refusal the base class gets from * `:memory:` never comes — and an image whose header already says WAL (one a * native run left behind) reports `wal` here too. */ protected get supportsWalJournal(): boolean; private wasmConfig; private beforeExitHandler; constructor(config: SqliteWasmDriverConfig); /** Translate the public config into a Knex config that uses our dialect. */ static toKnexConfig(config: SqliteWasmDriverConfig): SqlDriverConfig; connect(): Promise; disconnect(): Promise; /** * Force a flush of the in-memory database to disk. No-op for ephemeral * databases or when no fs is available. */ flush(): Promise; } /** Connection settings recognised by the WASM SQLite dialect. */ interface WasmSqliteConnectionSettings { filename: string; persist?: PersistMode; sqlJs?: SqlJsStatic; locateFile?: (file: string) => string; logger?: WasmConnectionOptions['logger']; } /** * Back-compat re-export. Prefer `getClient_WasmSqlite()` so the dialect * is resolved lazily; the named export triggers the factory on first * access of any static property. * * Note: importing this binding will execute the factory at import time * in some bundlers, which defeats the lazy pattern. New code should call * `getClient_WasmSqlite()` directly. */ declare const Client_WasmSqlite: any; declare const _default: { id: string; version: string; onEnable: (context: any) => Promise; }; export { Client_WasmSqlite, type PersistMode, SqliteWasmDriver, type SqliteWasmDriverConfig, type WasmConnectionOptions, WasmSqliteConnection, type WasmSqliteConnectionSettings, _default as default };