@optimystic/db-p2p-storage-ns 0.13.5 → 0.16.2
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 +30 -16
- package/dist/src/connection-mutex.d.ts +26 -0
- package/dist/src/connection-mutex.d.ts.map +1 -0
- package/dist/src/connection-mutex.js +33 -0
- package/dist/src/connection-mutex.js.map +1 -0
- package/dist/src/db.d.ts +60 -14
- package/dist/src/db.d.ts.map +1 -1
- package/dist/src/db.js +36 -14
- package/dist/src/db.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/ns-opener.d.ts +13 -4
- package/dist/src/ns-opener.d.ts.map +1 -1
- package/dist/src/ns-opener.js +67 -22
- package/dist/src/ns-opener.js.map +1 -1
- package/dist/src/sqlite-storage.d.ts +52 -29
- package/dist/src/sqlite-storage.d.ts.map +1 -1
- package/dist/src/sqlite-storage.js +94 -58
- package/dist/src/sqlite-storage.js.map +1 -1
- package/package.json +3 -3
- package/src/connection-mutex.ts +33 -0
- package/src/db.ts +72 -19
- package/src/index.ts +1 -1
- package/src/ns-opener.ts +71 -24
- package/src/sqlite-storage.ts +113 -59
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`** —
|
|
7
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
|
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 `
|
|
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
|
-
/**
|
|
45
|
-
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* -
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
|
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
|
|
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.
|
package/dist/src/db.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}`);
|
package/dist/src/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
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"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
package/dist/src/index.js.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/src/ns-opener.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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":"
|
|
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"}
|
package/dist/src/ns-opener.js
CHANGED
|
@@ -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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await this.raw.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAAA,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"ns-opener.js","sourceRoot":"","sources":["../../src/ns-opener.ts"],"names":[],"mappings":"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"}
|