import { Sqlite3Static } from "@sqlite.org/sqlite-wasm"; import { BatchStatement, ExternalChangeSet, Row, SqlArgs, SqlValue, SqliteDriver } from "@remelondb/core"; //#region src/protocol.d.ts interface Endpoint { postMessage(message: unknown): void; addMessageListener(listener: (message: unknown) => void): void; /** Tear down the transport (a real Worker terminates — this is what * releases the SAH pool's file locks). In-process endpoints may omit it. */ terminate?(): void; } type StorageKind = 'opfs' | 'memory'; type WorkerRequest = { readonly id: number; } & ({ readonly op: 'open'; readonly name: string; readonly storage: StorageKind; } | { readonly op: 'close'; readonly name: string; } | { readonly op: 'query'; readonly name: string; readonly sql: string; readonly args: readonly SqlValue[]; } | { readonly op: 'execute'; readonly name: string; readonly sql: string; readonly args: readonly SqlValue[]; } | { readonly op: 'executeBatch'; readonly name: string; readonly statements: readonly (readonly [string, readonly (readonly SqlValue[])[]])[]; } | { readonly op: 'setUserVersion'; readonly name: string; readonly version: number; } | { readonly op: 'destroy'; readonly name: string; } | /** Liveness probe (the broker checks its compute channel with it). */ { readonly op: 'ping'; } | /** Broker-only graceful retirement: close databases and release OPFS. */ { readonly op: 'releasePool'; } | /** * Write-block arbitration (docs/multi-tab.md), answered by the broker * itself — these never reach SQLite. An exclusive slot excludes every * other slot; shared slots (read consistency windows) coexist. */ { readonly op: 'acquireSlot'; readonly exclusive: boolean; } | { readonly op: 'releaseSlot'; readonly slot: number; } | /** * Change broadcast (docs/multi-tab.md), answered by the broker: relays * a committed change set to every OTHER tab holding `name` open. */ { readonly op: 'publishChanges'; readonly name: string; readonly changes: ExternalChangeSet; } | /** * Sync-lease request (broker-answered): granted when the asker holds * the lease, or it is free or expired. Asking again renews. */ { readonly op: 'syncTurn'; readonly name: string; readonly leaseMs: number; }); type WorkerResponse = { readonly id: number; readonly ok: true; readonly result: unknown; } | { readonly id: number; readonly ok: false; readonly error: string; readonly code?: 'OPFS_POOL_HELD'; readonly diagnostic?: string; }; //#endregion //#region src/WebSqliteDriver.d.ts /** * Thrown when OPFS storage is unavailable in the current browser context — * Firefox private browsing / "never remember history", or blocked site data. * OPFS is denied there even over https, so persistent offline mode cannot * start. Callers can catch this (or check `code === 'OPFS_UNAVAILABLE'`) to * degrade gracefully — surface a clear message, or reopen with * `storage: 'memory'` — instead of failing opaquely. * @category Driver */ declare class OpfsUnavailableError extends Error { readonly code: "OPFS_UNAVAILABLE"; constructor(options?: { cause?: unknown; }); } /** * Fail fast when OPFS is blocked, before any worker/broker is spawned. * `getDirectory()` throws on the main thread too in a denied context, so a * cheap probe here avoids a doomed startup — and, in shared mode, the broker * respawn loop that a storage-denied compute worker would otherwise trigger * (it looks "gone" rather than "refused"). A no-op where OPFS can't be * probed (no `navigator.storage`, e.g. Node): the worker path still reports. */ declare function probeOpfs(storage: { getDirectory?: () => Promise; } | undefined): Promise; /** @category Driver */ interface WebSqliteDriverOptions { /** * 'opfs' (default): persistent, via the OPFS SyncAccessHandle pool. * Unavailable OPFS is a loud error, never a silent downgrade. * 'memory': explicit non-persistent storage (tests, previews). */ readonly storage?: StorageKind; /** * The SAH pool allows one owner per origin, so a database can be open * in one tab at a time. Default: opening a database another tab holds * fails with a clear error. With `takeover: true`, this driver takes * the database instead — the other tab's driver shuts down and its * `onTakenOver` callback fires (in-flight statements there are * abandoned; committed data is safe on disk). Unused while shared mode * is active, where every tab joins one owner and there is nothing to * take over; it applies to the single-tab fallback. */ readonly takeover?: boolean; /** Called when another tab takes this database over (see `takeover`). * Unused while shared mode is active, for the same reason. */ readonly onTakenOver?: () => void; /** * Opt in to the SharedWorker owner (docs/multi-tab.md): all tabs route * through one worker, so same-name opens SHARE a live connection — * no lock, no takeover; commits broadcast into every tab's cache, * write blocks arbitrate across tabs, and sync runs on one tab at a * time via a lease. Where `SharedWorker` is unavailable (Chrome for * Android), falls back to the default single-owner behavior. */ readonly shared?: boolean; /** * Shared mode: how long a granted sync lease lasts before another tab * may take it over. Renewal happens implicitly on each sync tick, so * this only matters after the holder goes away. Default 10s. */ readonly syncLeaseMs?: number; /** * Shared mode: deadline for the broker to answer the open request, * after which open() rejects instead of hanging. The default (15s) * clears the slowest honest startup — a first visit fetching the * sqlite-wasm binary over a slow network — with margin; a genuine * hang then reports in seconds instead of never. */ readonly openTimeoutMs?: number; /** Override the transport — used by tests to run in-process. */ readonly createEndpoint?: () => Endpoint; } /** * SqliteDriver for browsers: SQLite-WASM running in a dedicated Worker * (OPFS sync-access handles are worker-only), reached via postMessage RPC. * See docs/reference/driver.md for why the seam is async. * @category Driver */ declare class WebSqliteDriver implements SqliteDriver { private readonly options; private endpoint; private name; private nextId; private pending; private releaseTabLock; private takenOver; private destroyedElsewhere; /** * @internal The compute worker this tab spawned for the broker, when it * was the one asked to host it. It lives and dies with the tab — except * in tests, which terminate it explicitly so the SAH pool's handles are * released before the next test file needs them. */ hostedComputeWorker: { terminate(): void; } | null; private externalChangesHandler; constructor(options?: WebSqliteDriverOptions); /** True when this driver routes through the SharedWorker owner. */ private get sharedMode(); private createEndpoint; /** * Cross-tab coordination via the Web Locks API (origin-scoped, so tabs * see each other). The lock is held for the connection's lifetime. * Returns false when locks are unavailable (Node tests, non-secure * contexts) — coordination is then skipped and behavior is unchanged. */ private acquireTabLock; private handleTakenOver; private releaseLock; private notOpenError; private request; private handleResponse; /** Reject every in-flight request — the transport itself is dead. */ private failAllPending; open(name: string): Promise<{ userVersion: number; }>; private get openName(); /** * Cross-tab write-block arbitration (docs/multi-tab.md). In shared mode * the broker grants slots; in dedicated mode storage is exclusive to * this driver and its in-process queue, so the slot is a free no-op. */ acquireWorkSlot(exclusive: boolean): Promise<() => Promise>; /** * Change propagation (docs/multi-tab.md): commits publish to the broker, * which relays them to the other tabs; the broker's relays arrive here * and go to the registered handler. Both are shared-mode-only — * dedicated storage has no other context to tell. */ publishChanges(changes: ExternalChangeSet): void; onExternalChanges(handler: (changes: ExternalChangeSet) => void): void; /** * Sync ownership (docs/multi-tab.md): in shared mode, a lease from * the broker decides which tab's synchronize actually runs; asking * again renews it, and an expired lease passes to the next asker (a * closed tab stops renewing, so ownership self-heals). Dedicated * storage always owns sync. */ requestSyncTurn(): Promise; close(): Promise; query(sql: string, args: SqlArgs): Promise; execute(sql: string, args: SqlArgs): Promise; executeBatch(statements: readonly BatchStatement[]): Promise; setUserVersion(version: number): Promise; destroy(): Promise; } //#endregion //#region src/errors.d.ts /** * The OPFS SyncAccessHandle pool remained owned after the bounded retry. * `diagnostic` preserves the browser's final acquisition error. * @category Driver */ declare class OpfsPoolHeldError extends Error { readonly diagnostic: string; readonly code: "OPFS_POOL_HELD"; constructor(diagnostic: string); } //#endregion //#region src/server.d.ts declare class SqliteWorkerServer { private readonly sqlite3; private connections; private poolUtil; constructor(sqlite3: Sqlite3Static); /** * Acquire the SAH pool, absorbing the holder-death race (remelonDB#3): * when a worker dies holding the pool, the browser releases its * handles asynchronously — seconds later under a real tab close. A * held pool is transient, so retry with backoff before declaring * storage unavailable. Chromium and Firefox report it as * NoModificationAllowedError; WebKit reports InvalidStateError * (remelonDB#67). Other failures (no OPFS, private mode) fail fast. */ private installPool; private connection; private prepared; private closeConnection; open(name: string, storage: 'opfs' | 'memory'): Promise<{ userVersion: number; }>; /** Close every database before handing this worker's OPFS pool back. */ releasePool(): void; handle(request: WorkerRequest): unknown; } /** Wire a server to a single endpoint (see createSqliteWorkerServing). */ declare function serveSqliteWorker(endpoint: Endpoint, loadSqlite: () => Promise): void; //#endregion export { type Endpoint, OpfsPoolHeldError, OpfsUnavailableError, SqliteWorkerServer, type StorageKind, WebSqliteDriver, type WebSqliteDriverOptions, type WorkerRequest, type WorkerResponse, probeOpfs, serveSqliteWorker };