@optimystic/db-p2p-storage-ns 0.14.1 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,9 +3,12 @@
3
3
  SQLite-backed storage backend for Optimystic NativeScript peers (iOS and
4
4
  Android, native — not React Native). Provides:
5
5
 
6
- - **`SqliteRawStorage`** — implements `IRawStorage` so a NativeScript node
7
- persists block metadata, revisions, pending transactions, committed
8
- transactions, and materialized blocks across app restarts.
6
+ - **`SqliteRawStorage`** — the `IRawStorage` a NativeScript node uses to
7
+ persist block metadata, revisions, pending transactions, committed
8
+ transactions, and materialized blocks across app restarts. It is a thin
9
+ shell over the shared `KvRawStorage` kernel driven by `SqliteStoreDriver`:
10
+ the kernel owns all JSON serialization, the driver maps the five logical
11
+ stores onto the five SQLite tables.
9
12
  - **`SqliteKVStore`** — implements `IKVStore` for the persistent transaction
10
13
  state used to recover crashed two-phase commits.
11
14
  - **`loadOrCreateNSPeerKey`** — generates an Ed25519 libp2p private key on
@@ -61,23 +64,34 @@ const libp2p = await createLibp2pNode({
61
64
  });
62
65
  ```
63
66
 
64
- The same handle is shared by all three consumers. SQLite serializes writes
65
- inside a single connection; reads/writes are short-lived enough that
66
- single-connection contention is a non-issue for a client peer.
67
+ The same handle is shared by all three consumers. SQLite allows at most one
68
+ open transaction per connection, so the wrapper serializes every mutating
69
+ operation — `exec`, statement `run`, and whole `transaction` bodies — on a
70
+ per-connection FIFO mutex. Without it, two concurrent `transaction` bodies
71
+ would each `BEGIN` on the shared connection; the second nests, throws, and its
72
+ rollback discards the first's still-open writes. Reads (`get`/`all`) stay off
73
+ the mutex to preserve read concurrency.
67
74
 
68
75
  ## Persistence model
69
76
 
70
77
  The package opens a single SQLite database (`optimystic.sqlite` by default
71
- in the NativeScript app's documents directory) with six tables:
72
-
73
- | Table | Key | Value |
74
- |----------------|----------------------------------|----------------------------------|
75
- | `metadata` | `block_id` | `BlockMetadata` (JSON) |
76
- | `revisions` | `(block_id, rev)` | `action_id` |
77
- | `pending` | `(block_id, action_id)` | `Transform` (JSON) |
78
- | `transactions` | `(block_id, action_id)` | `Transform` (JSON) |
79
- | `materialized` | `(block_id, action_id)` | `IBlock` (JSON) |
80
- | `kv` | `key` | `s_val` (TEXT) or `b_val` (BLOB) |
78
+ in the NativeScript app's documents directory) with six tables. The five
79
+ block-storage tables keep their original columns and keys, but their value
80
+ columns are now **BLOB**: `SqliteStoreDriver` binds/reads the kernel's raw
81
+ `Uint8Array` bytes and the shared `KvRawStorage` kernel owns the JSON/UTF-8
82
+ codec. SQLite stores a `Uint8Array` as a BLOB and returns it as a `Uint8Array`,
83
+ so bytes round-trip exactly (a TEXT column would risk UTF-8-coercing non-ASCII
84
+ JSON bytes). The "Decoded" column is the logical type the kernel decodes back
85
+ into; keys stay TEXT/INTEGER.
86
+
87
+ | Table | Key | Stored value | Decoded (logical) type |
88
+ |----------------|----------------------------------|--------------|------------------------|
89
+ | `metadata` | `block_id` | `BLOB` | `BlockMetadata` |
90
+ | `revisions` | `(block_id, rev)` | `BLOB` (`action_id` col) | `ActionId` |
91
+ | `pending` | `(block_id, action_id)` | `BLOB` | `Transform` |
92
+ | `transactions` | `(block_id, action_id)` | `BLOB` | `Transform` |
93
+ | `materialized` | `(block_id, action_id)` | `BLOB` | `IBlock` |
94
+ | `kv` | `key` | `s_val` (TEXT) or `b_val` (BLOB) | — (not kernel-backed) |
81
95
 
82
96
  Pragmas: `journal_mode = WAL`, `synchronous = NORMAL`, `foreign_keys = OFF`.
83
97
  Schema is versioned via `PRAGMA user_version`.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Per-connection FIFO mutex for the shared SQLite handle.
3
+ *
4
+ * SQLite allows at most one open transaction per connection, so every mutating
5
+ * operation on a shared connection (plain writes AND transaction bodies) must be
6
+ * serialized or a concurrent `BEGIN` will nest and cross-rollback another
7
+ * operation's still-open writes.
8
+ *
9
+ * This is deliberately NOT the global `Latches` keyed map from
10
+ * `@optimystic/db-core` — that is process-wide and keyed by string. Here we want
11
+ * one mutex bound to one connection instance, so the wrapper holds its own.
12
+ *
13
+ * The chain tail is kept non-rejecting: a failing task must not poison the queue
14
+ * for the operations behind it, so `serialize` continues the chain regardless of
15
+ * the prior task's outcome while still surfacing each task's own result/error to
16
+ * its own caller.
17
+ */
18
+ export declare class ConnectionMutex {
19
+ private tail;
20
+ /**
21
+ * Queue `task` behind all previously-queued tasks and run it once they settle.
22
+ * Resolves/rejects with `task`'s own outcome; never rejects the shared tail.
23
+ */
24
+ serialize<T>(task: () => Promise<T> | T): Promise<T>;
25
+ }
26
+ //# sourceMappingURL=connection-mutex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-mutex.d.ts","sourceRoot":"","sources":["../../src/connection-mutex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,eAAe;IAC3B,OAAO,CAAC,IAAI,CAAuC;IAEnD;;;OAGG;IACH,SAAS,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAQpD"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Per-connection FIFO mutex for the shared SQLite handle.
3
+ *
4
+ * SQLite allows at most one open transaction per connection, so every mutating
5
+ * operation on a shared connection (plain writes AND transaction bodies) must be
6
+ * serialized or a concurrent `BEGIN` will nest and cross-rollback another
7
+ * operation's still-open writes.
8
+ *
9
+ * This is deliberately NOT the global `Latches` keyed map from
10
+ * `@optimystic/db-core` — that is process-wide and keyed by string. Here we want
11
+ * one mutex bound to one connection instance, so the wrapper holds its own.
12
+ *
13
+ * The chain tail is kept non-rejecting: a failing task must not poison the queue
14
+ * for the operations behind it, so `serialize` continues the chain regardless of
15
+ * the prior task's outcome while still surfacing each task's own result/error to
16
+ * its own caller.
17
+ */
18
+ export class ConnectionMutex {
19
+ tail = Promise.resolve();
20
+ /**
21
+ * Queue `task` behind all previously-queued tasks and run it once they settle.
22
+ * Resolves/rejects with `task`'s own outcome; never rejects the shared tail.
23
+ */
24
+ serialize(task) {
25
+ // Run `task` whether the prior task fulfilled or rejected.
26
+ const run = this.tail.then(task, task);
27
+ // Advance the tail with a settled-either-way promise so a rejection here
28
+ // does not reject the next task's `.then(task, task)` prematurely.
29
+ this.tail = run.then(() => undefined, () => undefined);
30
+ return run;
31
+ }
32
+ }
33
+ //# sourceMappingURL=connection-mutex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-mutex.js","sourceRoot":"","sources":["../../src/connection-mutex.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,eAAe;IACnB,IAAI,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAEnD;;;OAGG;IACH,SAAS,CAAI,IAA0B;QACtC,2DAA2D;QAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACvC,yEAAyE;QACzE,mEAAmE;QACnE,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvD,OAAO,GAAG,CAAC;IACZ,CAAC;CACD"}
package/dist/src/db.d.ts CHANGED
@@ -19,7 +19,7 @@ export type SqliteRow = Record<string, SqliteParam>;
19
19
  *
20
20
  * Both `node:sqlite` and `@nativescript-community/sqlite` cache parsed SQL
21
21
  * internally, so re-binding a prepared statement is cheaper than re-parsing a
22
- * raw `execSQL`. The storage classes prepare each query once per instance.
22
+ * raw `execute`. The storage classes prepare each query once per instance.
23
23
  */
24
24
  export interface SqliteStatement {
25
25
  /** Execute with the given bind parameters, ignoring any returned rows. */
@@ -29,20 +29,50 @@ export interface SqliteStatement {
29
29
  /** Execute and return all rows (already drained — safe to await between yields). */
30
30
  all(...params: SqliteParam[]): Promise<SqliteRow[]>;
31
31
  }
32
+ /**
33
+ * Statements that execute directly on an already-OPEN transaction.
34
+ *
35
+ * Handed to the `transaction(fn)` callback. Its prepared statements run on the
36
+ * raw connection *inside* the mutex slot the transaction already holds, so they
37
+ * must NOT re-acquire the connection mutex — doing so would deadlock (the
38
+ * transaction body holds the only slot). This is the explicit-context seam the
39
+ * fix threads through instead of a shared `inTransaction` flag, which cannot
40
+ * tell an inner statement (bypass the lock) from a concurrent external write
41
+ * (block on it).
42
+ */
43
+ export interface SqliteTransaction {
44
+ /** Prepare a statement bound to the open transaction; bypasses the mutex. */
45
+ prepare(sql: string): SqliteStatement;
46
+ }
32
47
  /**
33
48
  * Minimal SQLite driver surface used by this package.
34
49
  *
35
50
  * Wraps either the NativeScript plugin (in production) or a Node SQLite
36
51
  * driver (in tests). Async on every method so the NS plugin's I/O can be
37
52
  * Promised — even where the underlying call is synchronous.
53
+ *
54
+ * A single connection is shared across the storage classes, so every mutating
55
+ * operation (`exec`, statement `run`, and `transaction` bodies) is serialized on
56
+ * a per-connection FIFO mutex. Reads (`get`/`all`) are intentionally left
57
+ * unserialized to preserve read concurrency.
38
58
  */
39
59
  export interface SqliteDb {
40
- /** Execute one or more semicolon-separated statements; no result rows. */
60
+ /** Execute one or more semicolon-separated statements; no result rows. Mutex-guarded. */
41
61
  exec(sql: string): Promise<void>;
42
- /** Prepare a parameterized statement for repeated execution. */
62
+ /** Prepare a parameterized statement for repeated execution (writes are mutex-guarded). */
43
63
  prepare(sql: string): SqliteStatement;
44
- /** Run `fn` inside `BEGIN ... COMMIT` (rolls back on throw). */
45
- transaction<T>(fn: () => Promise<T>): Promise<T>;
64
+ /**
65
+ * Run `fn` inside `BEGIN IMMEDIATE ... COMMIT` (rolls back on throw), holding
66
+ * the connection mutex for the whole body. `fn` receives a `SqliteTransaction`
67
+ * whose statements run directly on the open transaction without re-locking.
68
+ *
69
+ * NOTE: `fn` must issue its writes through the provided `tx`, never through a
70
+ * class-level statement from `db.prepare(...)` (those re-acquire the mutex and
71
+ * deadlock behind the slot this transaction holds), and must not call
72
+ * `db.transaction(...)` again — nested calls deadlock for the same reason. No
73
+ * current caller does either; guard here if that ever changes.
74
+ */
75
+ transaction<T>(fn: (tx: SqliteTransaction) => Promise<T>): Promise<T>;
46
76
  /** Release the underlying handle. */
47
77
  close(): Promise<void>;
48
78
  }
@@ -51,17 +81,33 @@ export declare const DEFAULT_DB_VERSION = 1;
51
81
  /**
52
82
  * Schema for the NativeScript storage backend.
53
83
  *
54
- * Mirrors the IndexedDB object stores 1:1:
55
- * - `metadata` → per-block `BlockMetadata` (JSON).
56
- * - `revisions` → revision lookup keyed by `(block_id, rev)`.
57
- * - `pending` → uncommitted transforms keyed by `(block_id, action_id)`.
58
- * - `transactions` → committed transforms keyed by `(block_id, action_id)`.
59
- * - `materialized` → materialized blocks keyed by `(block_id, action_id)`.
84
+ * Mirrors the IndexedDB object stores 1:1. The five block-storage stores keep
85
+ * their original columns and keys, but their value columns are now BLOB: the
86
+ * shared `KvRawStorage` kernel (driven by `SqliteStoreDriver`) owns all JSON /
87
+ * UTF-8 (de)serialization, so the driver only ever binds/reads the kernel's raw
88
+ * `Uint8Array` values. SQLite stores a `Uint8Array` as a BLOB and returns it as
89
+ * a `Uint8Array`, so bytes round-trip exactly (a TEXT column would risk UTF-8
90
+ * coercion corrupting non-ASCII JSON bytes). Keys stay TEXT/INTEGER.
91
+ * - `metadata` → per-block `BlockMetadata` bytes keyed by `block_id`.
92
+ * - `revisions` → revision lookup keyed by `(block_id, rev)`; `action_id` column
93
+ * holds the kernel's encoded `ActionId` bytes (BLOB).
94
+ * - `pending` → uncommitted transform bytes keyed by `(block_id, action_id)`.
95
+ * - `transactions` → committed transform bytes keyed by `(block_id, action_id)`.
96
+ * - `materialized` → materialized block bytes keyed by `(block_id, action_id)`.
60
97
  * - `kv` → generic string keyspace shared by `IKVStore` (`s_val`) and the
61
- * identity helper (`b_val`). The two columns avoid a base64 round-trip
62
- * for the libp2p private key.
98
+ * identity helper (`b_val`). NOT kernel-backed; unchanged. The two columns
99
+ * avoid a base64 round-trip for the libp2p private key.
100
+ *
101
+ * NOTE: value columns changed TEXT→BLOB with the kernel refactor, but every
102
+ * statement is `CREATE TABLE IF NOT EXISTS` — so a database file created by a
103
+ * pre-refactor build keeps its old TEXT-column schema, and its rows hold JSON
104
+ * *strings* the kernel now reads back and `TextDecoder.decode`s (garbling /
105
+ * throwing). There is no migration. Safe today because this backend is
106
+ * NativeScript-only with no shipped production data; if a build ever ships to
107
+ * devices that later upgrade, add a `user_version` bump that drops+recreates
108
+ * these five tables (or a real migration) before assuming BLOB rows.
63
109
  */
64
- export declare const SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS metadata (\n\tblock_id TEXT PRIMARY KEY,\n\tvalue TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS revisions (\n\tblock_id TEXT NOT NULL,\n\trev INTEGER NOT NULL,\n\taction_id TEXT NOT NULL,\n\tPRIMARY KEY (block_id, rev)\n);\n\nCREATE TABLE IF NOT EXISTS pending (\n\tblock_id TEXT NOT NULL,\n\taction_id TEXT NOT NULL,\n\tvalue TEXT NOT NULL,\n\tPRIMARY KEY (block_id, action_id)\n);\n\nCREATE TABLE IF NOT EXISTS transactions (\n\tblock_id TEXT NOT NULL,\n\taction_id TEXT NOT NULL,\n\tvalue TEXT NOT NULL,\n\tPRIMARY KEY (block_id, action_id)\n);\n\nCREATE TABLE IF NOT EXISTS materialized (\n\tblock_id TEXT NOT NULL,\n\taction_id TEXT NOT NULL,\n\tvalue TEXT NOT NULL,\n\tPRIMARY KEY (block_id, action_id)\n);\n\nCREATE TABLE IF NOT EXISTS kv (\n\tkey TEXT PRIMARY KEY,\n\ts_val TEXT,\n\tb_val BLOB\n);\n";
110
+ export declare const SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS metadata (\n\tblock_id TEXT PRIMARY KEY,\n\tvalue BLOB NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS revisions (\n\tblock_id TEXT NOT NULL,\n\trev INTEGER NOT NULL,\n\taction_id BLOB NOT NULL,\n\tPRIMARY KEY (block_id, rev)\n);\n\nCREATE TABLE IF NOT EXISTS pending (\n\tblock_id TEXT NOT NULL,\n\taction_id TEXT NOT NULL,\n\tvalue BLOB NOT NULL,\n\tPRIMARY KEY (block_id, action_id)\n);\n\nCREATE TABLE IF NOT EXISTS transactions (\n\tblock_id TEXT NOT NULL,\n\taction_id TEXT NOT NULL,\n\tvalue BLOB NOT NULL,\n\tPRIMARY KEY (block_id, action_id)\n);\n\nCREATE TABLE IF NOT EXISTS materialized (\n\tblock_id TEXT NOT NULL,\n\taction_id TEXT NOT NULL,\n\tvalue BLOB NOT NULL,\n\tPRIMARY KEY (block_id, action_id)\n);\n\nCREATE TABLE IF NOT EXISTS kv (\n\tkey TEXT PRIMARY KEY,\n\ts_val TEXT,\n\tb_val BLOB\n);\n";
65
111
  /**
66
112
  * Apply pragmas, run the schema DDL, and stamp `user_version` so future
67
113
  * migrations can branch on it. Idempotent — safe to call on every open.
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,kFAAkF;AAClF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;AAE9D,oEAAoE;AACpE,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC/B,0EAA0E;IAC1E,GAAG,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,gEAAgE;IAChE,GAAG,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAC9D,oFAAoF;IACpF,GAAG,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;CACpD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,QAAQ;IACxB,0EAA0E;IAC1E,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,gEAAgE;IAChE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC,gEAAgE;IAChE,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACjD,qCAAqC;IACrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,sBAAsB,CAAC;AACnD,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,UAAU,+2BAuCtB,CAAC;AAQF;;;GAGG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,MAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAInG;AAED;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,kFAAkF;AAClF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;AAE9D,oEAAoE;AACpE,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC/B,0EAA0E;IAC1E,GAAG,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,gEAAgE;IAChE,GAAG,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;IAC9D,oFAAoF;IACpF,GAAG,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;CACpD;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB;IACjC,6EAA6E;IAC7E,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;CACtC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,QAAQ;IACxB,yFAAyF;IACzF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,2FAA2F;IAC3F,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACtC;;;;;;;;;;OAUG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACtE,qCAAqC;IACrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,eAAO,MAAM,eAAe,sBAAsB,CAAC;AACnD,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,UAAU,+2BAuCtB,CAAC;AAQF;;;GAGG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,MAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAUnG;AAED;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC"}
package/dist/src/db.js CHANGED
@@ -15,47 +15,63 @@ export const DEFAULT_DB_VERSION = 1;
15
15
  /**
16
16
  * Schema for the NativeScript storage backend.
17
17
  *
18
- * Mirrors the IndexedDB object stores 1:1:
19
- * - `metadata` → per-block `BlockMetadata` (JSON).
20
- * - `revisions` → revision lookup keyed by `(block_id, rev)`.
21
- * - `pending` → uncommitted transforms keyed by `(block_id, action_id)`.
22
- * - `transactions` → committed transforms keyed by `(block_id, action_id)`.
23
- * - `materialized` → materialized blocks keyed by `(block_id, action_id)`.
18
+ * Mirrors the IndexedDB object stores 1:1. The five block-storage stores keep
19
+ * their original columns and keys, but their value columns are now BLOB: the
20
+ * shared `KvRawStorage` kernel (driven by `SqliteStoreDriver`) owns all JSON /
21
+ * UTF-8 (de)serialization, so the driver only ever binds/reads the kernel's raw
22
+ * `Uint8Array` values. SQLite stores a `Uint8Array` as a BLOB and returns it as
23
+ * a `Uint8Array`, so bytes round-trip exactly (a TEXT column would risk UTF-8
24
+ * coercion corrupting non-ASCII JSON bytes). Keys stay TEXT/INTEGER.
25
+ * - `metadata` → per-block `BlockMetadata` bytes keyed by `block_id`.
26
+ * - `revisions` → revision lookup keyed by `(block_id, rev)`; `action_id` column
27
+ * holds the kernel's encoded `ActionId` bytes (BLOB).
28
+ * - `pending` → uncommitted transform bytes keyed by `(block_id, action_id)`.
29
+ * - `transactions` → committed transform bytes keyed by `(block_id, action_id)`.
30
+ * - `materialized` → materialized block bytes keyed by `(block_id, action_id)`.
24
31
  * - `kv` → generic string keyspace shared by `IKVStore` (`s_val`) and the
25
- * identity helper (`b_val`). The two columns avoid a base64 round-trip
26
- * for the libp2p private key.
32
+ * identity helper (`b_val`). NOT kernel-backed; unchanged. The two columns
33
+ * avoid a base64 round-trip for the libp2p private key.
34
+ *
35
+ * NOTE: value columns changed TEXT→BLOB with the kernel refactor, but every
36
+ * statement is `CREATE TABLE IF NOT EXISTS` — so a database file created by a
37
+ * pre-refactor build keeps its old TEXT-column schema, and its rows hold JSON
38
+ * *strings* the kernel now reads back and `TextDecoder.decode`s (garbling /
39
+ * throwing). There is no migration. Safe today because this backend is
40
+ * NativeScript-only with no shipped production data; if a build ever ships to
41
+ * devices that later upgrade, add a `user_version` bump that drops+recreates
42
+ * these five tables (or a real migration) before assuming BLOB rows.
27
43
  */
28
44
  export const SCHEMA_SQL = `
29
45
  CREATE TABLE IF NOT EXISTS metadata (
30
46
  block_id TEXT PRIMARY KEY,
31
- value TEXT NOT NULL
47
+ value BLOB NOT NULL
32
48
  );
33
49
 
34
50
  CREATE TABLE IF NOT EXISTS revisions (
35
51
  block_id TEXT NOT NULL,
36
52
  rev INTEGER NOT NULL,
37
- action_id TEXT NOT NULL,
53
+ action_id BLOB NOT NULL,
38
54
  PRIMARY KEY (block_id, rev)
39
55
  );
40
56
 
41
57
  CREATE TABLE IF NOT EXISTS pending (
42
58
  block_id TEXT NOT NULL,
43
59
  action_id TEXT NOT NULL,
44
- value TEXT NOT NULL,
60
+ value BLOB NOT NULL,
45
61
  PRIMARY KEY (block_id, action_id)
46
62
  );
47
63
 
48
64
  CREATE TABLE IF NOT EXISTS transactions (
49
65
  block_id TEXT NOT NULL,
50
66
  action_id TEXT NOT NULL,
51
- value TEXT NOT NULL,
67
+ value BLOB NOT NULL,
52
68
  PRIMARY KEY (block_id, action_id)
53
69
  );
54
70
 
55
71
  CREATE TABLE IF NOT EXISTS materialized (
56
72
  block_id TEXT NOT NULL,
57
73
  action_id TEXT NOT NULL,
58
- value TEXT NOT NULL,
74
+ value BLOB NOT NULL,
59
75
  PRIMARY KEY (block_id, action_id)
60
76
  );
61
77
 
@@ -65,8 +81,8 @@ CREATE TABLE IF NOT EXISTS kv (
65
81
  b_val BLOB
66
82
  );
67
83
  `;
84
+ // Assignment-form pragmas return no rows, so they go through exec()/execute().
68
85
  const PRAGMAS_SQL = `
69
- PRAGMA journal_mode = WAL;
70
86
  PRAGMA synchronous = NORMAL;
71
87
  PRAGMA foreign_keys = OFF;
72
88
  `;
@@ -75,6 +91,12 @@ PRAGMA foreign_keys = OFF;
75
91
  * migrations can branch on it. Idempotent — safe to call on every open.
76
92
  */
77
93
  export async function applySchema(db, version = DEFAULT_DB_VERSION) {
94
+ // `PRAGMA journal_mode = WAL` returns the resulting mode as a row. The NS
95
+ // Android plugin maps exec()→execSQL, which rejects row-returning statements
96
+ // ("Queries can be performed using ... query or rawQuery methods only"), so
97
+ // this pragma must run through a query method. prepare().get() does that on
98
+ // every driver (NS plugin → get()/rawQuery; node:sqlite/better-sqlite3 → get()).
99
+ await db.prepare('PRAGMA journal_mode = WAL').get();
78
100
  await db.exec(PRAGMAS_SQL);
79
101
  await db.exec(SCHEMA_SQL);
80
102
  await db.exec(`PRAGMA user_version = ${version}`);
@@ -1 +1 @@
1
- {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA0CH,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAAC;AACnD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCzB,CAAC;AAEF,MAAM,WAAW,GAAG;;;;CAInB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAY,EAAE,UAAkB,kBAAkB;IACnF,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC3B,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC1B,MAAM,EAAE,CAAC,IAAI,CAAC,yBAAyB,OAAO,EAAE,CAAC,CAAC;AACnD,CAAC"}
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAyEH,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAAC;AACnD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCzB,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,GAAG;;;CAGnB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAY,EAAE,UAAkB,kBAAkB;IACnF,0EAA0E;IAC1E,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,iFAAiF;IACjF,MAAM,EAAE,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC,GAAG,EAAE,CAAC;IACpD,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC3B,MAAM,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC1B,MAAM,EAAE,CAAC,IAAI,CAAC,yBAAyB,OAAO,EAAE,CAAC,CAAC;AACnD,CAAC"}
@@ -1,7 +1,7 @@
1
1
  export type { OptimysticNSDBHandle, SqliteDb, SqliteStatement, SqliteParam, SqliteRow } from './db.js';
2
2
  export { DEFAULT_DB_NAME, DEFAULT_DB_VERSION } from './db.js';
3
3
  export { openOptimysticNSDb, wrapNSPluginDb } from './ns-opener.js';
4
- export { SqliteRawStorage } from './sqlite-storage.js';
4
+ export { SqliteRawStorage, SqliteStoreDriver } from './sqlite-storage.js';
5
5
  export { SqliteKVStore } from './sqlite-kv-store.js';
6
6
  export { loadOrCreateNSPeerKey, DEFAULT_PEER_KEY_NAME } from './identity.js';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACvG,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACvG,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
package/dist/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { DEFAULT_DB_NAME, DEFAULT_DB_VERSION } from './db.js';
2
2
  export { openOptimysticNSDb, wrapNSPluginDb } from './ns-opener.js';
3
- export { SqliteRawStorage } from './sqlite-storage.js';
3
+ export { SqliteRawStorage, SqliteStoreDriver } from './sqlite-storage.js';
4
4
  export { SqliteKVStore } from './sqlite-kv-store.js';
5
5
  export { loadOrCreateNSPeerKey, DEFAULT_PEER_KEY_NAME } from './identity.js';
6
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
@@ -6,7 +6,7 @@ import { type OptimysticNSDBHandle, type SqliteDb } from './db.js';
6
6
  * at typecheck time on non-NativeScript consumers).
7
7
  */
8
8
  interface NSPluginDb {
9
- execSQL(sql: string, params?: ReadonlyArray<unknown>): unknown;
9
+ execute(sql: string, params?: ReadonlyArray<unknown>): unknown;
10
10
  get(sql: string, params?: ReadonlyArray<unknown>): Promise<Record<string, unknown> | null | undefined> | Record<string, unknown> | null | undefined;
11
11
  select(sql: string, params?: ReadonlyArray<unknown>): Promise<Array<Record<string, unknown>>> | Array<Record<string, unknown>>;
12
12
  close(): void | Promise<void>;
@@ -17,9 +17,13 @@ interface NSPluginDb {
17
17
  * the migration to `version`.
18
18
  *
19
19
  * The returned `SqliteDb` handle is safe to share across `SqliteRawStorage`,
20
- * `SqliteKVStore`, and `loadOrCreateNSPeerKey` — SQLite serializes writes
21
- * inside the connection, and our reads/writes are short-lived enough that
22
- * single-connection contention is a non-issue for a client peer.
20
+ * `SqliteKVStore`, and `loadOrCreateNSPeerKey`. Because SQLite allows at most
21
+ * one open transaction per connection, the wrapper serializes every mutating
22
+ * operation — `exec`, statement `run`, and whole `transaction` bodies — through
23
+ * a per-connection FIFO mutex. Without it, two concurrent `transaction` bodies
24
+ * would each `BEGIN` on the shared connection; the second would nest and its
25
+ * rollback would silently discard the first's still-open writes. Reads
26
+ * (`get`/`all`) stay off the mutex to preserve read concurrency.
23
27
  *
24
28
  * `path` may be passed in as the full filesystem path if the caller wants
25
29
  * to control file placement; otherwise the plugin's documents-directory
@@ -31,6 +35,11 @@ export declare function openOptimysticNSDb(name?: string, version?: number): Pro
31
35
  * internal `SqliteDb` interface used by the storage classes. Exported only
32
36
  * for callers that already hold an open NS-plugin handle and want to skip
33
37
  * the opener (rare — typically users just call `openOptimysticNSDb`).
38
+ *
39
+ * NOTE: the serialization mutex lives on the wrapper, not the raw handle. Wrap
40
+ * a given raw connection exactly once — two wrappers over one handle each hold
41
+ * their own mutex and would not serialize against each other, reintroducing the
42
+ * cross-rollback this fix closes.
34
43
  */
35
44
  export declare function wrapNSPluginDb(raw: NSPluginDb): SqliteDb;
36
45
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ns-opener.d.ts","sourceRoot":"","sources":["../../src/ns-opener.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoD,KAAK,oBAAoB,EAAE,KAAK,QAAQ,EAA0D,MAAM,SAAS,CAAC;AAE7K;;;;;GAKG;AACH,UAAU,UAAU;IACnB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAC/D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IACpJ,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/H,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,kBAAkB,CACvC,IAAI,GAAE,MAAwB,EAC9B,OAAO,GAAE,MAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAM/B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,QAAQ,CAExD"}
1
+ {"version":3,"file":"ns-opener.d.ts","sourceRoot":"","sources":["../../src/ns-opener.ts"],"names":[],"mappings":"AACA,OAAO,EAAoD,KAAK,oBAAoB,EAAE,KAAK,QAAQ,EAAkF,MAAM,SAAS,CAAC;AAErM;;;;;GAKG;AACH,UAAU,UAAU;IACnB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAC/D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IACpJ,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/H,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,kBAAkB,CACvC,IAAI,GAAE,MAAwB,EAC9B,OAAO,GAAE,MAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAM/B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,QAAQ,CAExD"}
@@ -1,3 +1,4 @@
1
+ import { ConnectionMutex } from './connection-mutex.js';
1
2
  import { applySchema, DEFAULT_DB_NAME, DEFAULT_DB_VERSION } from './db.js';
2
3
  /**
3
4
  * Opens (creating if needed) the Optimystic SQLite database at `name` under
@@ -5,9 +6,13 @@ import { applySchema, DEFAULT_DB_NAME, DEFAULT_DB_VERSION } from './db.js';
5
6
  * the migration to `version`.
6
7
  *
7
8
  * The returned `SqliteDb` handle is safe to share across `SqliteRawStorage`,
8
- * `SqliteKVStore`, and `loadOrCreateNSPeerKey` — SQLite serializes writes
9
- * inside the connection, and our reads/writes are short-lived enough that
10
- * single-connection contention is a non-issue for a client peer.
9
+ * `SqliteKVStore`, and `loadOrCreateNSPeerKey`. Because SQLite allows at most
10
+ * one open transaction per connection, the wrapper serializes every mutating
11
+ * operation — `exec`, statement `run`, and whole `transaction` bodies — through
12
+ * a per-connection FIFO mutex. Without it, two concurrent `transaction` bodies
13
+ * would each `BEGIN` on the shared connection; the second would nest and its
14
+ * rollback would silently discard the first's still-open writes. Reads
15
+ * (`get`/`all`) stay off the mutex to preserve read concurrency.
11
16
  *
12
17
  * `path` may be passed in as the full filesystem path if the caller wants
13
18
  * to control file placement; otherwise the plugin's documents-directory
@@ -25,45 +30,63 @@ export async function openOptimysticNSDb(name = DEFAULT_DB_NAME, version = DEFAU
25
30
  * internal `SqliteDb` interface used by the storage classes. Exported only
26
31
  * for callers that already hold an open NS-plugin handle and want to skip
27
32
  * the opener (rare — typically users just call `openOptimysticNSDb`).
33
+ *
34
+ * NOTE: the serialization mutex lives on the wrapper, not the raw handle. Wrap
35
+ * a given raw connection exactly once — two wrappers over one handle each hold
36
+ * their own mutex and would not serialize against each other, reintroducing the
37
+ * cross-rollback this fix closes.
28
38
  */
29
39
  export function wrapNSPluginDb(raw) {
30
40
  return new NSPluginDbWrapper(raw);
31
41
  }
32
42
  class NSPluginDbWrapper {
33
43
  raw;
44
+ mutex = new ConnectionMutex();
34
45
  constructor(raw) {
35
46
  this.raw = raw;
36
47
  }
37
48
  async exec(sql) {
38
- // The plugin's execSQL accepts a single statement; split semicolon-
49
+ // The plugin's execute accepts a single statement; split semicolon-
39
50
  // separated DDL so callers can pass the full schema in one go.
40
51
  const statements = sql
41
52
  .split(';')
42
53
  .map(s => s.trim())
43
54
  .filter(s => s.length > 0);
44
- for (const statement of statements) {
45
- await this.raw.execSQL(statement);
46
- }
55
+ await this.mutex.serialize(async () => {
56
+ for (const statement of statements) {
57
+ await this.raw.execute(statement);
58
+ }
59
+ });
47
60
  }
48
61
  prepare(sql) {
49
- return new NSPluginStatement(this.raw, sql);
62
+ // Outside-transaction statement: writes go through the mutex.
63
+ return new NSPluginStatement(this.raw, sql, this.mutex);
50
64
  }
51
65
  async transaction(fn) {
52
- await this.raw.execSQL('BEGIN');
53
- try {
54
- const result = await fn();
55
- await this.raw.execSQL('COMMIT');
56
- return result;
57
- }
58
- catch (err) {
66
+ return this.mutex.serialize(async () => {
67
+ // BEGIN IMMEDIATE takes the write lock up front rather than deferring
68
+ // it to the first write, so contention surfaces here and not mid-body.
69
+ await this.raw.execute('BEGIN IMMEDIATE');
59
70
  try {
60
- await this.raw.execSQL('ROLLBACK');
71
+ // Statements bound to the open transaction bypass the mutex — we
72
+ // already hold the slot; re-locking would deadlock.
73
+ const tx = {
74
+ prepare: (sql) => new NSPluginStatement(this.raw, sql),
75
+ };
76
+ const result = await fn(tx);
77
+ await this.raw.execute('COMMIT');
78
+ return result;
61
79
  }
62
- catch {
63
- // Swallow rollback failures so we surface the original error.
80
+ catch (err) {
81
+ try {
82
+ await this.raw.execute('ROLLBACK');
83
+ }
84
+ catch {
85
+ // Swallow rollback failures so we surface the original error.
86
+ }
87
+ throw err;
64
88
  }
65
- throw err;
66
- }
89
+ });
67
90
  }
68
91
  async close() {
69
92
  await this.raw.close();
@@ -72,20 +95,42 @@ class NSPluginDbWrapper {
72
95
  class NSPluginStatement {
73
96
  raw;
74
97
  sql;
75
- constructor(raw, sql) {
98
+ mutex;
99
+ /**
100
+ * @param mutex When present, `run` is serialized on the connection mutex
101
+ * (outside-transaction writes). When absent, `run` executes directly on the
102
+ * raw connection — used for statements bound to an already-open transaction
103
+ * that already holds the mutex slot.
104
+ */
105
+ constructor(raw, sql, mutex) {
76
106
  this.raw = raw;
77
107
  this.sql = sql;
108
+ this.mutex = mutex;
78
109
  }
79
110
  async run(...params) {
80
- await this.raw.execSQL(this.sql, params);
111
+ if (this.mutex) {
112
+ await this.mutex.serialize(() => this.raw.execute(this.sql, params));
113
+ }
114
+ else {
115
+ await this.raw.execute(this.sql, params);
116
+ }
81
117
  }
82
118
  async get(...params) {
119
+ // NOTE: reads run directly on the connection, unserialized, to preserve read
120
+ // concurrency. A read issued while a write transaction is open on this same
121
+ // connection observes that transaction's UNCOMMITTED rows (read-your-connection
122
+ // semantics). Fine today: the only transaction writer is same-block promote
123
+ // under the commit latch, and cross-block reads are independent. If a future
124
+ // caller reads a block on this connection while another op's transaction on the
125
+ // same rows is mid-flight, it may see uncommitted state — serialize reads too if
126
+ // that ever matters.
83
127
  const row = await this.raw.get(this.sql, params);
84
128
  if (row === null || row === undefined)
85
129
  return undefined;
86
130
  return row;
87
131
  }
88
132
  async all(...params) {
133
+ // NOTE: unserialized read — see the note on `get` above re: uncommitted reads.
89
134
  const rows = await this.raw.select(this.sql, params);
90
135
  return rows;
91
136
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ns-opener.js","sourceRoot":"","sources":["../../src/ns-opener.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ns-opener.js","sourceRoot":"","sources":["../../src/ns-opener.ts"],"names":[],"mappings":"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"}