import type { ActionId, BlockId } from '@optimystic/db-core'; import { KvRawStorage, type BatchBackend, type BatchEntry, type CommittedBatch, type SetTimer, type SinkCommitOptions, type StoreDurability, type StoreIdentity, type StoreKey, type WriteGroupOptions } from '@optimystic/db-p2p'; import type { SqliteDb } from './db.js'; /** What a {@link SqliteBatchBackend} is built with: the test-only `setTimer` that arms its deadline timer. */ export interface SqliteBatchBackendOptions { readonly setTimer?: SetTimer; } /** * SQLite {@link BatchBackend}: the six logical block-storage stores mapped to the six relational * tables (`metadata`, `revisions`, `pending`, `transactions`, `proofs`, `materialized`) with their * original columns and keys, every batch applied as ONE SQLite transaction. The reads are the * former per-write driver's statements unchanged, so the on-disk format did not change with the * move to batches. * * `KvRawStorage` owns all JSON serialization, so this backend only ever reads/writes `Uint8Array` * values: the value columns are BLOB and SQLite binds a `Uint8Array` as a BLOB and returns it as a * `Uint8Array`, so no codec lives here (a TEXT column would risk UTF-8 coercion corrupting non-ASCII * JSON bytes). * * ### Why a committed batch survives a power cut, and in order * * `applySchema` (`db.ts`) opens every database in write-ahead-log (WAL) mode under * `synchronous = NORMAL`. A commit appends the batch's frames to the one WAL file, each frame * checksummed over the frames before it, and recovery stops at the first bad frame; before every * checkpoint SQLite syncs the WAL, so the database file never holds later content ahead of earlier * content. What a power cut leaves is therefore a prefix of the committed transactions, in commit * order — rule 3 of the flush contract on `RawStoreDriver.flush` — and a batch is one transaction, * so it survives whole or not at all. A commit under `synchronous = FULL` * (`SqliteTransactionOptions.synced`) syncs the WAL at that commit, and the WAL being one * append-only file, that sync covers every earlier commit on the database, the key-value store's * and the identity helper's included, since all three share one connection. So the sync of a batch * is one `FULL` commit — this batch's own when it must be synced at once, a later batch's, or the * `durability_mark` bump the deadline timer commits — which is what `durability: 'power-loss'` * states. * * ### How a commit decides * * The deadlines, the timer and the backpressure test are the rules of `SyncDeadlines` from * `@optimystic/db-p2p`, shared with the file backend. `commit` takes the batch's deadline as it is * called and decides whether the transaction is synced BEFORE taking the connection mutex, since * the pragma is set as the transaction starts; the bookkeeping — `defer` of a deferred batch, * `take` of the cover a synced one settles — runs INSIDE the transaction body, under the mutex, so * it is ordered against every other commit and sync: a cover taken there is exactly the batches * whose transactions committed earlier, every one of which this `FULL` commit syncs. A deadline * that passes between the decision and the mutex only delays that commit's backpressure to the * timer, which is due anyway. * * A read sees a batch as soon as its transaction commits, before it is synced, which `BatchBackend` * allows; the overlay above serves the batch until it is durable. The handle is shared with * `SqliteKVStore` and `loadOrCreateNSPeerKey`, so `close()` never closes it. Every write of a batch * goes through `tx.prepare`, never a class-level statement, which would re-take the mutex the * transaction holds and deadlock (see `SqliteDb.transaction`). */ export declare class SqliteBatchBackend implements BatchBackend { private readonly db; readonly durability: StoreDurability; private readonly identity; /** One point read per store. Reads run off the mutex, so a class-level statement is right here. */ private readonly reads; private readonly stmts; /** * Batches committed and not yet synced, in order, each with the `durable` the next sync settles; * the earliest deadline among them, with a timer armed for it; and the `window-overrun` episode. * A batch's payload is nothing: its transaction has already applied it. */ private readonly deadlines; private fault; constructor(db: SqliteDb, options?: SqliteBatchBackendOptions); /** True once a transaction or a sync failed; every later `commit` and `sync` then throws {@link StoreFailedError}. */ get failed(): boolean; storeIdentity(): StoreIdentity; get(key: StoreKey): Promise; rangeRevisions(blockId: BlockId, lo: number, hi: number): Promise<[number, Uint8Array][]>; listPendingActionIds(blockId: BlockId): Promise; listBlockIds(): AsyncIterable; approximateBytesUsed(): Promise; /** * Apply the batch as one transaction and resolve with its `durable`. `WriteGroupSink.commit`'s * contract: with `syncWithinMs` 0, or a deadline already passed (`SyncDeadlines.mustSyncNow`), * the transaction commits under `synchronous = FULL`, which syncs this batch and every one held, * and `durable` is already settled; otherwise it commits under `NORMAL` and `durable` settles at * the next sync, which the timer makes by the earliest deadline held. A rejection has latched the * backend. */ commit(entries: readonly BatchEntry[], { syncWithinMs }: SinkCommitOptions): Promise; /** Hasten: one synced commit covering every batch held when it runs; with nothing held, nothing is synced. `WriteGroupSink.sync`'s contract. */ sync(): Promise; /** * The handle is shared, so it is not closed here: only the deadline timer must not outlive the * backend. `WriteGroupDriver.close` flushes strictly first, which took and settled everything * held and so disarmed the timer; a caller that did not flush gets the same here. After a latch * nothing is held, since the latch took and rejected it. */ close(): Promise; private commitDeferred; private commitSynced; /** * The sync step: one synced transaction whose body runs `write`, if given, then takes the cover * of every batch held — under the mutex, so the cover is exactly the batches whose transactions * committed before this one — and, when there is a cover to settle and nothing of its own was * written, bumps `durability_mark` so the commit has a frame to sync. The cover settles once the * commit has resolved. A failure anywhere latches the backend and rejects every covered * `durable`, so no flush waits for a sync the store will not make. * * `owedSince` is the moment the timer fired, when the timer's task runs this: `SyncDeadlines.take` * counts the sync's lateness from it rather than from a deadline the timer's own slop missed. */ private syncHeld; /** The timer fired: run the sync step. Its failure has latched the backend, so it is only logged here. */ private syncInBackground; /** * Every entry in order — a put as `INSERT OR REPLACE`, a delete as `DELETE` — each statement * prepared once per batch against the open transaction. Returns how many were applied. */ private applyEntries; private throwIfFailed; /** * Latch the backend failed on its first fault and reject every batch waiting for a sync that will * now never be made, so no caller waits on a `durable` for ever. Returns the error to throw. */ private latch; } /** The write group's options (`WriteGroupOptions`); the test-only `setTimer` arms both the group's idle flush and the backend's deadline timer. */ export type SqliteRawStorageOptions = WriteGroupOptions; /** * SQLite-backed {@link IRawStorage} for NativeScript peers: the shared {@link KvRawStorage} kernel * over a {@link WriteGroupDriver} over a {@link SqliteBatchBackend}. Every write resolves into the * driver's in-memory overlay; `flush()` hands the group to the backend as one batch, which applies * it as one SQLite transaction, synced before the flush resolves or within the window the flush * allows. The public name/constructor (`new SqliteRawStorage(db)`) is unchanged so existing imports * keep resolving. * * `listBlockIds`/`getApproximateBytesUsed`/`getStoreIdentity`/`close` are re-declared here as * always-present (the backend always implements the first three, so the driver and kernel always * wire them, and the driver always has a `close`) — the base declares them optional, but every NS * consumer relies on them. `close()` flushes what is still held, syncs it, and disarms the timers; * it does not close the shared handle, which the host closes afterwards. */ export declare class SqliteRawStorage extends KvRawStorage { listBlockIds: () => AsyncIterable; getApproximateBytesUsed: () => Promise; getStoreIdentity: () => StoreIdentity; close: () => Promise; constructor(db: SqliteDb, options?: SqliteRawStorageOptions); } //# sourceMappingURL=sqlite-storage.d.ts.map