@optimystic/db-p2p-storage-ns 1.15.0 → 1.16.0
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 +78 -20
- package/dist/src/db.d.ts +45 -7
- package/dist/src/db.d.ts.map +1 -1
- package/dist/src/db.js +71 -5
- package/dist/src/db.js.map +1 -1
- package/dist/src/identity.d.ts +3 -1
- package/dist/src/identity.d.ts.map +1 -1
- package/dist/src/identity.js +5 -1
- package/dist/src/identity.js.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/ns-opener.d.ts.map +1 -1
- package/dist/src/ns-opener.js +16 -31
- package/dist/src/ns-opener.js.map +1 -1
- package/dist/src/sqlite-kv-store.d.ts +15 -1
- package/dist/src/sqlite-kv-store.d.ts.map +1 -1
- package/dist/src/sqlite-kv-store.js +38 -0
- package/dist/src/sqlite-kv-store.js.map +1 -1
- package/dist/src/sqlite-storage.d.ts +126 -45
- package/dist/src/sqlite-storage.d.ts.map +1 -1
- package/dist/src/sqlite-storage.js +246 -129
- package/dist/src/sqlite-storage.js.map +1 -1
- package/dist/src/sqlite-sync.d.ts +17 -0
- package/dist/src/sqlite-sync.d.ts.map +1 -0
- package/dist/src/sqlite-sync.js +21 -0
- package/dist/src/sqlite-sync.js.map +1 -0
- package/package.json +3 -3
- package/src/db.ts +96 -6
- package/src/identity.ts +5 -1
- package/src/index.ts +4 -2
- package/src/ns-opener.ts +20 -29
- package/src/sqlite-kv-store.ts +36 -2
- package/src/sqlite-storage.ts +291 -164
- package/src/sqlite-sync.ts +24 -0
package/README.md
CHANGED
|
@@ -6,9 +6,10 @@ Android, native — not React Native). Provides:
|
|
|
6
6
|
- **`SqliteRawStorage`** — the `IRawStorage` a NativeScript node uses to
|
|
7
7
|
persist block metadata, revisions, pending transactions, committed
|
|
8
8
|
transactions, commit proofs, and materialized blocks across app restarts.
|
|
9
|
-
It is
|
|
10
|
-
`
|
|
11
|
-
|
|
9
|
+
It is the shared `KvRawStorage` kernel over a `WriteGroupDriver` over
|
|
10
|
+
`SqliteBatchBackend`: the kernel owns all JSON serialization, the driver
|
|
11
|
+
holds each group of writes in memory until it is flushed, and the backend
|
|
12
|
+
applies the group as one SQLite transaction over the six tables.
|
|
12
13
|
- **`SqliteKVStore`** — implements `IKVStore` for the persistent transaction
|
|
13
14
|
state used to recover crashed two-phase commits.
|
|
14
15
|
- **`loadOrCreateNSPeerKey`** — generates an Ed25519 libp2p private key on
|
|
@@ -72,27 +73,40 @@ would each `BEGIN` on the shared connection; the second nests, throws, and its
|
|
|
72
73
|
rollback discards the first's still-open writes. Reads (`get`/`all`) stay off
|
|
73
74
|
the mutex to preserve read concurrency.
|
|
74
75
|
|
|
76
|
+
Close the storage before the handle, and the handle last:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
await rawStorage.close(); // flushes what it still holds, syncs it, disarms its timers
|
|
80
|
+
await kvStore.flush();
|
|
81
|
+
await db.close();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`SqliteRawStorage.close()` does not close the shared handle. A write the
|
|
85
|
+
storage still holds when the handle is closed under it is lost, and a timer it
|
|
86
|
+
still has armed would fire on a closed connection.
|
|
87
|
+
|
|
75
88
|
## Persistence model
|
|
76
89
|
|
|
77
90
|
The package opens a single SQLite database (`optimystic.sqlite` by default
|
|
78
|
-
in the NativeScript app's documents directory) with
|
|
91
|
+
in the NativeScript app's documents directory) with eight tables. The six
|
|
79
92
|
block-storage tables keep their original columns and keys, but their value
|
|
80
|
-
columns are
|
|
93
|
+
columns are **BLOB**: `SqliteBatchBackend` binds/reads the kernel's raw
|
|
81
94
|
`Uint8Array` bytes and the shared `KvRawStorage` kernel owns the JSON/UTF-8
|
|
82
95
|
codec. SQLite stores a `Uint8Array` as a BLOB and returns it as a `Uint8Array`,
|
|
83
96
|
so bytes round-trip exactly (a TEXT column would risk UTF-8-coercing non-ASCII
|
|
84
97
|
JSON bytes). The "Decoded" column is the logical type the kernel decodes back
|
|
85
98
|
into; keys stay TEXT/INTEGER.
|
|
86
99
|
|
|
87
|
-
| Table
|
|
88
|
-
|
|
89
|
-
| `metadata`
|
|
90
|
-
| `revisions`
|
|
91
|
-
| `pending`
|
|
92
|
-
| `transactions`
|
|
93
|
-
| `proofs`
|
|
94
|
-
| `materialized`
|
|
95
|
-
| `kv`
|
|
100
|
+
| Table | Key | Stored value | Decoded (logical) type |
|
|
101
|
+
|-------------------|----------------------------------|--------------|------------------------|
|
|
102
|
+
| `metadata` | `block_id` | `BLOB` | `BlockMetadata` |
|
|
103
|
+
| `revisions` | `(block_id, rev)` | `BLOB` (`action_id` col) | `ActionId` |
|
|
104
|
+
| `pending` | `(block_id, action_id)` | `BLOB` | `Transform` |
|
|
105
|
+
| `transactions` | `(block_id, action_id)` | `BLOB` | `Transform` |
|
|
106
|
+
| `proofs` | `(block_id, rev)` | `BLOB` | `BlockCommitProof` |
|
|
107
|
+
| `materialized` | `(block_id, action_id)` | `BLOB` | `IBlock` |
|
|
108
|
+
| `kv` | `key` | `s_val` (TEXT) or `b_val` (BLOB) | — (not kernel-backed) |
|
|
109
|
+
| `durability_mark` | `id` (always 1) | `n` (INTEGER), a counter nothing reads | — (the row a sync commits when it has nothing else to write) |
|
|
96
110
|
|
|
97
111
|
Pragmas: `journal_mode = WAL`, `synchronous = NORMAL`, `foreign_keys = OFF`.
|
|
98
112
|
Schema is versioned via `PRAGMA user_version`.
|
|
@@ -100,21 +114,64 @@ Schema is versioned via `PRAGMA user_version`.
|
|
|
100
114
|
`listRevisions` and `listPendingTransactions` issue bounded
|
|
101
115
|
`SELECT … ORDER BY` queries — never `SELECT * FROM <table>` + JS filter —
|
|
102
116
|
so list latency stays bounded as the tables grow.
|
|
103
|
-
`promotePendingTransaction`
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
`promotePendingTransaction` records two entries of one write group — the
|
|
118
|
+
committed row's insert and the pending row's delete — applied in that group's
|
|
119
|
+
transaction, so the move is atomic across crashes.
|
|
106
120
|
|
|
107
121
|
`getApproximateBytesUsed()` returns `page_count × page_size` from PRAGMAs.
|
|
108
122
|
This is the SQLite database-file footprint; it is **not** a per-block figure.
|
|
109
123
|
That's adequate for `StorageMonitor` — which uses the value as an advisory
|
|
110
124
|
ring-selection input.
|
|
111
125
|
|
|
126
|
+
## Durability
|
|
127
|
+
|
|
128
|
+
`SqliteRawStorage` reports `durability: 'power-loss'`. A write resolves into
|
|
129
|
+
memory and is served from there; `flush()` applies every write made since the
|
|
130
|
+
last flush as **one SQLite transaction**, so a group lands whole or not at all.
|
|
131
|
+
In write-ahead-log mode a commit appends the group's frames to one append-only
|
|
132
|
+
log file, each frame checksummed over the frames before it, and recovery stops
|
|
133
|
+
at the first bad frame, so what a power cut leaves is a prefix of the committed
|
|
134
|
+
groups, in order. Under `synchronous = NORMAL` a commit does not wait for the
|
|
135
|
+
disk; a commit under `FULL` syncs the log, and with it every earlier commit on
|
|
136
|
+
the database. So:
|
|
137
|
+
|
|
138
|
+
- a strict `flush()` (no `withinMs`, or 0) commits the group under `FULL` and
|
|
139
|
+
resolves once it is on disk;
|
|
140
|
+
- a relaxed `flush({ withinMs })` — what a node uses for a write its cohort
|
|
141
|
+
also holds, see [storage.md §How long a write may wait for its sync](../db-p2p/docs/storage.md#how-long-a-write-may-wait-for-its-sync) —
|
|
142
|
+
commits under `NORMAL` and resolves at once; one `FULL` commit by the earliest
|
|
143
|
+
deadline, or a strict flush before then, syncs every group committed since the
|
|
144
|
+
last sync. `whenDurable()` resolves once the groups flushed so far are synced.
|
|
145
|
+
|
|
146
|
+
What a crash loses:
|
|
147
|
+
|
|
148
|
+
| Crash | Lost |
|
|
149
|
+
|---|---|
|
|
150
|
+
| the app's process | a group not yet flushed, still in memory; nothing a flush has resolved for |
|
|
151
|
+
| a power cut or operating-system crash | at most the groups still inside their window, always the most recent ones, each whole; nothing a strict flush has resolved for, and nothing once the window has passed |
|
|
152
|
+
|
|
153
|
+
`SqliteDb.transaction(fn, { synced: true })` is the primitive: inside the
|
|
154
|
+
connection mutex slot it sets `PRAGMA synchronous = FULL`, runs
|
|
155
|
+
`BEGIN IMMEDIATE … COMMIT`, and restores `NORMAL` afterwards, after a rollback
|
|
156
|
+
too. `syncSqliteDb(db)` is a synced transaction that bumps the one-row
|
|
157
|
+
`durability_mark` table, so a sync with no rows of its own still has a frame to
|
|
158
|
+
commit; `SqliteKVStore.flush()` runs it when the store wrote since its last
|
|
159
|
+
flush, and `loadOrCreateNSPeerKey` runs it after writing a newly generated key,
|
|
160
|
+
so a power cut right after first start cannot give the node a different
|
|
161
|
+
identity next time.
|
|
162
|
+
|
|
163
|
+
A failed commit or sync latches the storage: every later write, flush and
|
|
164
|
+
`whenDurable()` rejects with `StoreFailedError` until the app reopens the store
|
|
165
|
+
(rule 5 of the flush contract,
|
|
166
|
+
[storage.md §6](../db-p2p/docs/storage.md#6-a-write-survives-a-crash-once-a-flush-called-after-it-has-resolved)).
|
|
167
|
+
|
|
112
168
|
## Identity
|
|
113
169
|
|
|
114
170
|
`loadOrCreateNSPeerKey(db, keyName?)` writes the libp2p private key as raw
|
|
115
171
|
protobuf bytes (`Uint8Array`) into the `kv` table's `b_val` column under
|
|
116
|
-
`keyName` (default `peer-private-key`)
|
|
117
|
-
base64 round-trip. To rotate the identity, delete that one
|
|
172
|
+
`keyName` (default `peer-private-key`), synced before it returns. SQLite stores
|
|
173
|
+
BLOBs natively — no base64 round-trip. To rotate the identity, delete that one
|
|
174
|
+
row:
|
|
118
175
|
|
|
119
176
|
```ts
|
|
120
177
|
await db.prepare('DELETE FROM kv WHERE key = ?').run('peer-private-key');
|
|
@@ -130,4 +187,5 @@ underlying SQLite file.
|
|
|
130
187
|
the package-private `SqliteDb` interface, so the Node driver is a faithful
|
|
131
188
|
behavioural surrogate for the NativeScript plugin. The suite never imports
|
|
132
189
|
`@nativescript-community/sqlite`, so the plugin's native bindings don't need
|
|
133
|
-
to be installed to run tests.
|
|
190
|
+
to be installed to run tests. No test cuts the power: that a committed group
|
|
191
|
+
survives one rests on SQLite's write-ahead log, as stated above.
|
package/dist/src/db.d.ts
CHANGED
|
@@ -44,6 +44,17 @@ export interface SqliteTransaction {
|
|
|
44
44
|
/** Prepare a statement bound to the open transaction; bypasses the mutex. */
|
|
45
45
|
prepare(sql: string): SqliteStatement;
|
|
46
46
|
}
|
|
47
|
+
/** Options of {@link SqliteDb.transaction}. */
|
|
48
|
+
export interface SqliteTransactionOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Commit under `PRAGMA synchronous = FULL`, which in write-ahead-log mode syncs the log file at
|
|
51
|
+
* this commit. The log is one append-only file, so that sync covers every frame before it: every
|
|
52
|
+
* earlier `NORMAL` commit on this database, whichever store or helper made it. The pragma is set
|
|
53
|
+
* inside the mutex slot the transaction holds and restored to `NORMAL` after the commit, or after
|
|
54
|
+
* the rollback, so no other write on the connection pays for the sync by accident. Default false.
|
|
55
|
+
*/
|
|
56
|
+
readonly synced?: boolean;
|
|
57
|
+
}
|
|
47
58
|
/**
|
|
48
59
|
* Minimal SQLite driver surface used by this package.
|
|
49
60
|
*
|
|
@@ -65,6 +76,10 @@ export interface SqliteDb {
|
|
|
65
76
|
* Run `fn` inside `BEGIN IMMEDIATE ... COMMIT` (rolls back on throw), holding
|
|
66
77
|
* the connection mutex for the whole body. `fn` receives a `SqliteTransaction`
|
|
67
78
|
* whose statements run directly on the open transaction without re-locking.
|
|
79
|
+
* With `options.synced` the commit also syncs the write-ahead log, which makes
|
|
80
|
+
* this transaction and every earlier commit on the database survive a power
|
|
81
|
+
* cut (see {@link SqliteTransactionOptions.synced}); without it the commit
|
|
82
|
+
* survives a crash of this process only, until a later synced commit.
|
|
68
83
|
*
|
|
69
84
|
* NOTE: `fn` must issue its writes through the provided `tx`, never through a
|
|
70
85
|
* class-level statement from `db.prepare(...)` (those re-acquire the mutex and
|
|
@@ -72,10 +87,27 @@ export interface SqliteDb {
|
|
|
72
87
|
* `db.transaction(...)` again — nested calls deadlock for the same reason. No
|
|
73
88
|
* current caller does either; guard here if that ever changes.
|
|
74
89
|
*/
|
|
75
|
-
transaction<T>(fn: (tx: SqliteTransaction) => Promise<T
|
|
90
|
+
transaction<T>(fn: (tx: SqliteTransaction) => Promise<T>, options?: SqliteTransactionOptions): Promise<T>;
|
|
76
91
|
/** Release the underlying handle. */
|
|
77
92
|
close(): Promise<void>;
|
|
78
93
|
}
|
|
94
|
+
/** The raw connection surface {@link runTransaction} drives: one statement at a time, no rows back. */
|
|
95
|
+
export interface TransactionConnection {
|
|
96
|
+
execute(sql: string): Promise<void> | void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The transaction protocol both `SqliteDb` implementations run inside their mutex slot: with
|
|
100
|
+
* `synced`, `PRAGMA synchronous = FULL` first; then `BEGIN IMMEDIATE`, the body, and `COMMIT`, or
|
|
101
|
+
* `ROLLBACK` when the body or the commit throws; then, synced, `PRAGMA synchronous = NORMAL`
|
|
102
|
+
* whichever way the transaction ended, so the connection's default is restored before the slot is
|
|
103
|
+
* released. `BEGIN IMMEDIATE` takes the write lock up front rather than deferring it to the first
|
|
104
|
+
* write, so contention surfaces here and not mid-body.
|
|
105
|
+
*
|
|
106
|
+
* A failure of the restore after a failed body is logged rather than thrown, so the body's error,
|
|
107
|
+
* the one that matters, is the one the caller sees; after a committed body it is thrown, since the
|
|
108
|
+
* connection would otherwise sync every later commit unasked.
|
|
109
|
+
*/
|
|
110
|
+
export declare function runTransaction<T>(conn: TransactionConnection, tx: SqliteTransaction, fn: (tx: SqliteTransaction) => Promise<T>, options?: SqliteTransactionOptions): Promise<T>;
|
|
79
111
|
export declare const DEFAULT_DB_NAME = "optimystic.sqlite";
|
|
80
112
|
export declare const DEFAULT_DB_VERSION = 1;
|
|
81
113
|
/**
|
|
@@ -83,11 +115,11 @@ export declare const DEFAULT_DB_VERSION = 1;
|
|
|
83
115
|
*
|
|
84
116
|
* Mirrors the IndexedDB object stores 1:1. The six block-storage stores keep
|
|
85
117
|
* their original columns and keys, but their value columns are now BLOB: the
|
|
86
|
-
* shared `KvRawStorage` kernel (
|
|
87
|
-
* UTF-8 (de)serialization, so the
|
|
88
|
-
* `Uint8Array` values. SQLite stores a `Uint8Array` as a BLOB
|
|
89
|
-
* a `Uint8Array`, so bytes round-trip exactly (a TEXT column
|
|
90
|
-
* coercion corrupting non-ASCII JSON bytes). Keys stay TEXT/INTEGER.
|
|
118
|
+
* shared `KvRawStorage` kernel (over `WriteGroupDriver` over `SqliteBatchBackend`)
|
|
119
|
+
* owns all JSON / UTF-8 (de)serialization, so the backend only ever binds/reads
|
|
120
|
+
* the kernel's raw `Uint8Array` values. SQLite stores a `Uint8Array` as a BLOB
|
|
121
|
+
* and returns it as a `Uint8Array`, so bytes round-trip exactly (a TEXT column
|
|
122
|
+
* would risk UTF-8 coercion corrupting non-ASCII JSON bytes). Keys stay TEXT/INTEGER.
|
|
91
123
|
* - `metadata` → per-block `BlockMetadata` bytes keyed by `block_id`.
|
|
92
124
|
* - `revisions` → revision lookup keyed by `(block_id, rev)`; `action_id` column
|
|
93
125
|
* holds the kernel's encoded `ActionId` bytes (BLOB).
|
|
@@ -100,6 +132,12 @@ export declare const DEFAULT_DB_VERSION = 1;
|
|
|
100
132
|
* - `kv` → generic string keyspace shared by `IKVStore` (`s_val`) and the
|
|
101
133
|
* identity helper (`b_val`). NOT kernel-backed; unchanged. The two columns
|
|
102
134
|
* avoid a base64 round-trip for the libp2p private key.
|
|
135
|
+
* - `durability_mark` → one row, bumped by `syncSqliteDb` (`sqlite-sync.ts`) so a
|
|
136
|
+
* sync that has no rows of its own to write still has a frame to commit.
|
|
137
|
+
*
|
|
138
|
+
* NOTE: this package's SQL is uppercase throughout, predating the lowercase rule in
|
|
139
|
+
* AGENTS.md, and new statements have kept to it rather than mix cases within one file;
|
|
140
|
+
* convert the package in one pass rather than statement by statement.
|
|
103
141
|
*
|
|
104
142
|
* NOTE: value columns changed TEXT→BLOB with the kernel refactor, but every
|
|
105
143
|
* statement is `CREATE TABLE IF NOT EXISTS` — so a database file created by a
|
|
@@ -110,7 +148,7 @@ export declare const DEFAULT_DB_VERSION = 1;
|
|
|
110
148
|
* devices that later upgrade, add a `user_version` bump that drops+recreates
|
|
111
149
|
* these six tables (or a real migration) before assuming BLOB rows.
|
|
112
150
|
*/
|
|
113
|
-
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 proofs (\n\tblock_id TEXT NOT NULL,\n\trev INTEGER NOT NULL,\n\tvalue BLOB NOT NULL,\n\tPRIMARY KEY (block_id, rev)\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";
|
|
151
|
+
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 proofs (\n\tblock_id TEXT NOT NULL,\n\trev INTEGER NOT NULL,\n\tvalue BLOB NOT NULL,\n\tPRIMARY KEY (block_id, rev)\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\nCREATE TABLE IF NOT EXISTS durability_mark (\n\tid INTEGER PRIMARY KEY CHECK (id = 1),\n\tn INTEGER NOT NULL\n);\n";
|
|
114
152
|
/**
|
|
115
153
|
* Apply pragmas, run the schema DDL, and stamp `user_version` so future
|
|
116
154
|
* 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;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,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,+CAA+C;AAC/C,MAAM,WAAW,wBAAwB;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;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;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1G,qCAAqC;IACrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,uGAAuG;AACvG,MAAM,WAAW,qBAAqB;IACrC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3C;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAAC,CAAC,EACrC,IAAI,EAAE,qBAAqB,EAC3B,EAAE,EAAE,iBAAiB,EACrB,EAAE,EAAE,CAAC,EAAE,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,EACzC,OAAO,CAAC,EAAE,wBAAwB,GAChC,OAAO,CAAC,CAAC,CAAC,CAkBZ;AAmBD,eAAO,MAAM,eAAe,sBAAsB,CAAC;AACnD,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,UAAU,woCAmDtB,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
|
@@ -10,6 +10,61 @@
|
|
|
10
10
|
* The wrapper is **package-private**: only `openOptimysticNSDb` is exported to
|
|
11
11
|
* consumers; the `SqliteDb` interface is an internal seam for tests.
|
|
12
12
|
*/
|
|
13
|
+
import { createLogger } from './logger.js';
|
|
14
|
+
const log = createLogger('db');
|
|
15
|
+
/**
|
|
16
|
+
* The transaction protocol both `SqliteDb` implementations run inside their mutex slot: with
|
|
17
|
+
* `synced`, `PRAGMA synchronous = FULL` first; then `BEGIN IMMEDIATE`, the body, and `COMMIT`, or
|
|
18
|
+
* `ROLLBACK` when the body or the commit throws; then, synced, `PRAGMA synchronous = NORMAL`
|
|
19
|
+
* whichever way the transaction ended, so the connection's default is restored before the slot is
|
|
20
|
+
* released. `BEGIN IMMEDIATE` takes the write lock up front rather than deferring it to the first
|
|
21
|
+
* write, so contention surfaces here and not mid-body.
|
|
22
|
+
*
|
|
23
|
+
* A failure of the restore after a failed body is logged rather than thrown, so the body's error,
|
|
24
|
+
* the one that matters, is the one the caller sees; after a committed body it is thrown, since the
|
|
25
|
+
* connection would otherwise sync every later commit unasked.
|
|
26
|
+
*/
|
|
27
|
+
export async function runTransaction(conn, tx, fn, options) {
|
|
28
|
+
const synced = options?.synced === true;
|
|
29
|
+
if (synced)
|
|
30
|
+
await conn.execute('PRAGMA synchronous = FULL');
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
result = await commitOrRollBack(conn, tx, fn);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (synced) {
|
|
37
|
+
try {
|
|
38
|
+
await conn.execute('PRAGMA synchronous = NORMAL');
|
|
39
|
+
}
|
|
40
|
+
catch (restoreErr) {
|
|
41
|
+
log('restoring synchronous = NORMAL failed after a failed transaction: %o', restoreErr);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
if (synced)
|
|
47
|
+
await conn.execute('PRAGMA synchronous = NORMAL');
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
async function commitOrRollBack(conn, tx, fn) {
|
|
51
|
+
await conn.execute('BEGIN IMMEDIATE');
|
|
52
|
+
try {
|
|
53
|
+
const result = await fn(tx);
|
|
54
|
+
await conn.execute('COMMIT');
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
try {
|
|
59
|
+
await conn.execute('ROLLBACK');
|
|
60
|
+
}
|
|
61
|
+
catch (rollbackErr) {
|
|
62
|
+
// The original error is the one to surface; the rollback's is only logged.
|
|
63
|
+
log('rollback failed after a failed transaction: %o', rollbackErr);
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
13
68
|
export const DEFAULT_DB_NAME = 'optimystic.sqlite';
|
|
14
69
|
export const DEFAULT_DB_VERSION = 1;
|
|
15
70
|
/**
|
|
@@ -17,11 +72,11 @@ export const DEFAULT_DB_VERSION = 1;
|
|
|
17
72
|
*
|
|
18
73
|
* Mirrors the IndexedDB object stores 1:1. The six block-storage stores keep
|
|
19
74
|
* their original columns and keys, but their value columns are now BLOB: the
|
|
20
|
-
* shared `KvRawStorage` kernel (
|
|
21
|
-
* UTF-8 (de)serialization, so the
|
|
22
|
-
* `Uint8Array` values. SQLite stores a `Uint8Array` as a BLOB
|
|
23
|
-
* a `Uint8Array`, so bytes round-trip exactly (a TEXT column
|
|
24
|
-
* coercion corrupting non-ASCII JSON bytes). Keys stay TEXT/INTEGER.
|
|
75
|
+
* shared `KvRawStorage` kernel (over `WriteGroupDriver` over `SqliteBatchBackend`)
|
|
76
|
+
* owns all JSON / UTF-8 (de)serialization, so the backend only ever binds/reads
|
|
77
|
+
* the kernel's raw `Uint8Array` values. SQLite stores a `Uint8Array` as a BLOB
|
|
78
|
+
* and returns it as a `Uint8Array`, so bytes round-trip exactly (a TEXT column
|
|
79
|
+
* would risk UTF-8 coercion corrupting non-ASCII JSON bytes). Keys stay TEXT/INTEGER.
|
|
25
80
|
* - `metadata` → per-block `BlockMetadata` bytes keyed by `block_id`.
|
|
26
81
|
* - `revisions` → revision lookup keyed by `(block_id, rev)`; `action_id` column
|
|
27
82
|
* holds the kernel's encoded `ActionId` bytes (BLOB).
|
|
@@ -34,6 +89,12 @@ export const DEFAULT_DB_VERSION = 1;
|
|
|
34
89
|
* - `kv` → generic string keyspace shared by `IKVStore` (`s_val`) and the
|
|
35
90
|
* identity helper (`b_val`). NOT kernel-backed; unchanged. The two columns
|
|
36
91
|
* avoid a base64 round-trip for the libp2p private key.
|
|
92
|
+
* - `durability_mark` → one row, bumped by `syncSqliteDb` (`sqlite-sync.ts`) so a
|
|
93
|
+
* sync that has no rows of its own to write still has a frame to commit.
|
|
94
|
+
*
|
|
95
|
+
* NOTE: this package's SQL is uppercase throughout, predating the lowercase rule in
|
|
96
|
+
* AGENTS.md, and new statements have kept to it rather than mix cases within one file;
|
|
97
|
+
* convert the package in one pass rather than statement by statement.
|
|
37
98
|
*
|
|
38
99
|
* NOTE: value columns changed TEXT→BLOB with the kernel refactor, but every
|
|
39
100
|
* statement is `CREATE TABLE IF NOT EXISTS` — so a database file created by a
|
|
@@ -90,6 +151,11 @@ CREATE TABLE IF NOT EXISTS kv (
|
|
|
90
151
|
s_val TEXT,
|
|
91
152
|
b_val BLOB
|
|
92
153
|
);
|
|
154
|
+
|
|
155
|
+
CREATE TABLE IF NOT EXISTS durability_mark (
|
|
156
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
157
|
+
n INTEGER NOT NULL
|
|
158
|
+
);
|
|
93
159
|
`;
|
|
94
160
|
// Assignment-form pragmas return no rows, so they go through exec()/execute().
|
|
95
161
|
const PRAGMAS_SQL = `
|
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;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;AA8F/B;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CACnC,IAA2B,EAC3B,EAAqB,EACrB,EAAyC,EACzC,OAAkC;IAElC,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,IAAI,MAAM;QAAE,MAAM,IAAI,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAC5D,IAAI,MAAS,CAAC;IACd,IAAI,CAAC;QACJ,MAAM,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,MAAM,EAAE,CAAC;YACZ,IAAI,CAAC;gBACJ,MAAM,IAAI,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC;YACnD,CAAC;YAAC,OAAO,UAAU,EAAE,CAAC;gBACrB,GAAG,CAAC,sEAAsE,EAAE,UAAU,CAAC,CAAC;YACzF,CAAC;QACF,CAAC;QACD,MAAM,GAAG,CAAC;IACX,CAAC;IACD,IAAI,MAAM;QAAE,MAAM,IAAI,CAAC,OAAO,CAAC,6BAA6B,CAAC,CAAC;IAC9D,OAAO,MAAM,CAAC;AACf,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAI,IAA2B,EAAE,EAAqB,EAAE,EAAyC;IAC/H,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACtC,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5B,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,MAAM,CAAC;IACf,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,CAAC;YACJ,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAChC,CAAC;QAAC,OAAO,WAAW,EAAE,CAAC;YACtB,2EAA2E;YAC3E,GAAG,CAAC,gDAAgD,EAAE,WAAW,CAAC,CAAC;QACpE,CAAC;QACD,MAAM,GAAG,CAAC;IACX,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAAC;AACnD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDzB,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/identity.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ import type { SqliteDb } from './db.js';
|
|
|
3
3
|
export declare const DEFAULT_PEER_KEY_NAME = "peer-private-key";
|
|
4
4
|
/**
|
|
5
5
|
* Loads the persisted NativeScript peer's libp2p private key, or generates a
|
|
6
|
-
* fresh Ed25519 key on first call and
|
|
6
|
+
* fresh Ed25519 key on first call, persists it, and syncs the database before
|
|
7
|
+
* returning it, so a power cut right after first start cannot give the node a
|
|
8
|
+
* different peer id next time. An existing key is read as it is.
|
|
7
9
|
*
|
|
8
10
|
* The key is stored as raw protobuf bytes in the `kv` table's `b_val` column
|
|
9
11
|
* under `keyName`. SQLite stores BLOB natively, so there is no base64
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGxC,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,CAC1C,EAAE,EAAE,QAAQ,EACZ,OAAO,GAAE,MAA8B,GACrC,OAAO,CAAC,UAAU,CAAC,CAarB"}
|
package/dist/src/identity.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys';
|
|
2
|
+
import { syncSqliteDb } from './sqlite-sync.js';
|
|
2
3
|
export const DEFAULT_PEER_KEY_NAME = 'peer-private-key';
|
|
3
4
|
/**
|
|
4
5
|
* Loads the persisted NativeScript peer's libp2p private key, or generates a
|
|
5
|
-
* fresh Ed25519 key on first call and
|
|
6
|
+
* fresh Ed25519 key on first call, persists it, and syncs the database before
|
|
7
|
+
* returning it, so a power cut right after first start cannot give the node a
|
|
8
|
+
* different peer id next time. An existing key is read as it is.
|
|
6
9
|
*
|
|
7
10
|
* The key is stored as raw protobuf bytes in the `kv` table's `b_val` column
|
|
8
11
|
* under `keyName`. SQLite stores BLOB natively, so there is no base64
|
|
@@ -23,6 +26,7 @@ export async function loadOrCreateNSPeerKey(db, keyName = DEFAULT_PEER_KEY_NAME)
|
|
|
23
26
|
await db
|
|
24
27
|
.prepare('INSERT INTO kv (key, s_val, b_val) VALUES (?, NULL, ?) ON CONFLICT(key) DO UPDATE SET b_val = excluded.b_val')
|
|
25
28
|
.run(keyName, bytes);
|
|
29
|
+
await syncSqliteDb(db);
|
|
26
30
|
return key;
|
|
27
31
|
}
|
|
28
32
|
//# sourceMappingURL=identity.js.map
|
package/dist/src/identity.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGpG,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,CAAC,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;AAExD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,EAAY,EACZ,UAAkB,qBAAqB;IAEvC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAChF,MAAM,MAAM,GAAG,GAAG,EAAE,KAAK,CAAC;IAC1B,IAAI,MAAM,YAAY,UAAU,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,sBAAsB,CAAC,MAAM,CAAC,CAAC;IACvC,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,SAAS,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,EAAE;SACN,OAAO,CAAC,8GAA8G,CAAC;SACvH,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACtB,MAAM,YAAY,CAAC,EAAE,CAAC,CAAC;IACvB,OAAO,GAAG,CAAC;AACZ,CAAC"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export type { OptimysticNSDBHandle, SqliteDb, SqliteStatement, SqliteParam, SqliteRow } from './db.js';
|
|
1
|
+
export type { OptimysticNSDBHandle, SqliteDb, SqliteStatement, SqliteTransaction, SqliteTransactionOptions, 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,
|
|
4
|
+
export { SqliteRawStorage, SqliteBatchBackend } from './sqlite-storage.js';
|
|
5
|
+
export type { SqliteRawStorageOptions, SqliteBatchBackendOptions } from './sqlite-storage.js';
|
|
6
|
+
export { syncSqliteDb } from './sqlite-sync.js';
|
|
5
7
|
export { SqliteKVStore } from './sqlite-kv-store.js';
|
|
6
8
|
export { loadOrCreateNSPeerKey, DEFAULT_PEER_KEY_NAME } from './identity.js';
|
|
7
9
|
//# 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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,QAAQ,EAAE,eAAe,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpJ,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,YAAY,EAAE,uBAAuB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,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,7 @@
|
|
|
1
1
|
export { DEFAULT_DB_NAME, DEFAULT_DB_VERSION } from './db.js';
|
|
2
2
|
export { openOptimysticNSDb, wrapNSPluginDb } from './ns-opener.js';
|
|
3
|
-
export { SqliteRawStorage,
|
|
3
|
+
export { SqliteRawStorage, SqliteBatchBackend } from './sqlite-storage.js';
|
|
4
|
+
export { syncSqliteDb } from './sqlite-sync.js';
|
|
4
5
|
export { SqliteKVStore } from './sqlite-kv-store.js';
|
|
5
6
|
export { loadOrCreateNSPeerKey, DEFAULT_PEER_KEY_NAME } from './identity.js';
|
|
6
7
|
//# 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,
|
|
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,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE3E,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ns-opener.d.ts","sourceRoot":"","sources":["../../src/ns-opener.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"ns-opener.d.ts","sourceRoot":"","sources":["../../src/ns-opener.ts"],"names":[],"mappings":"AACA,OAAO,EAAoE,KAAK,oBAAoB,EAAE,KAAK,QAAQ,EAAiH,MAAM,SAAS,CAAC;AAEpP;;;;;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,5 +1,5 @@
|
|
|
1
1
|
import { ConnectionMutex } from './connection-mutex.js';
|
|
2
|
-
import { applySchema, DEFAULT_DB_NAME, DEFAULT_DB_VERSION } from './db.js';
|
|
2
|
+
import { applySchema, runTransaction, DEFAULT_DB_NAME, DEFAULT_DB_VERSION } from './db.js';
|
|
3
3
|
/**
|
|
4
4
|
* Opens (creating if needed) the Optimystic SQLite database at `name` under
|
|
5
5
|
* the NativeScript app's documents directory, applies WAL pragmas, and runs
|
|
@@ -62,31 +62,11 @@ class NSPluginDbWrapper {
|
|
|
62
62
|
// Outside-transaction statement: writes go through the mutex.
|
|
63
63
|
return new NSPluginStatement(this.raw, sql, this.mutex);
|
|
64
64
|
}
|
|
65
|
-
async transaction(fn) {
|
|
66
|
-
return this.mutex.serialize(async () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
try {
|
|
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;
|
|
79
|
-
}
|
|
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;
|
|
88
|
-
}
|
|
89
|
-
});
|
|
65
|
+
async transaction(fn, options) {
|
|
66
|
+
return this.mutex.serialize(() => runTransaction({ execute: async (sql) => { await this.raw.execute(sql); } },
|
|
67
|
+
// Statements bound to the open transaction bypass the mutex — we
|
|
68
|
+
// already hold the slot; re-locking would deadlock.
|
|
69
|
+
{ prepare: (sql) => new NSPluginStatement(this.raw, sql) }, fn, options));
|
|
90
70
|
}
|
|
91
71
|
async close() {
|
|
92
72
|
await this.raw.close();
|
|
@@ -119,11 +99,16 @@ class NSPluginStatement {
|
|
|
119
99
|
// NOTE: reads run directly on the connection, unserialized, to preserve read
|
|
120
100
|
// concurrency. A read issued while a write transaction is open on this same
|
|
121
101
|
// connection observes that transaction's UNCOMMITTED rows (read-your-connection
|
|
122
|
-
// semantics). Fine today: the
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
102
|
+
// semantics). Fine today: the one transaction writer of the block tables is
|
|
103
|
+
// `SqliteBatchBackend.commit`, and every row it is writing is held by the
|
|
104
|
+
// `WriteGroupDriver` overlay above it until the batch is durable. A point read of
|
|
105
|
+
// such a row is answered by the overlay and never reaches this connection; a
|
|
106
|
+
// listing does reach it, and may see the batch's uncommitted rows, but merges the
|
|
107
|
+
// overlay over them, so it answers the same whether the transaction has committed
|
|
108
|
+
// or not (and a rolled-back batch latches the store). The key-value store and the
|
|
109
|
+
// identity helper write other tables, by autocommit statements, and the sync's
|
|
110
|
+
// `durability_mark` row is read by nobody. If a reader of a batch's rows ever
|
|
111
|
+
// bypasses the overlay, serialize reads too.
|
|
127
112
|
const row = await this.raw.get(this.sql, params);
|
|
128
113
|
if (row === null || row === undefined)
|
|
129
114
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ns-opener.js","sourceRoot":"","sources":["../../src/ns-opener.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,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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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IKVStore } from '@optimystic/db-p2p';
|
|
2
2
|
import type { SqliteDb } from './db.js';
|
|
3
3
|
/**
|
|
4
4
|
* SQLite-backed `IKVStore` adapter for NativeScript peers.
|
|
@@ -11,14 +11,28 @@ import type { SqliteDb } from './db.js';
|
|
|
11
11
|
*
|
|
12
12
|
* Only the `s_val` column is read or written here; the `b_val` column is
|
|
13
13
|
* reserved for binary identity material.
|
|
14
|
+
*
|
|
15
|
+
* Writes are autocommit statements: each is visible, and survives a crash of this process, once it
|
|
16
|
+
* resolves, and the write-ahead log keeps them in order. `flush()` makes them survive a power cut:
|
|
17
|
+
* when anything was written since the last flush it runs `syncSqliteDb`, one synced commit that
|
|
18
|
+
* syncs the shared log and with it every earlier write on the connection; otherwise it resolves at
|
|
19
|
+
* once. A failed sync latches the store, after which every write and flush throws
|
|
20
|
+
* `StoreFailedError` (rule 5 of the flush contract on `RawStoreDriver.flush`); reads keep working.
|
|
14
21
|
*/
|
|
15
22
|
export declare class SqliteKVStore implements IKVStore {
|
|
23
|
+
private readonly db;
|
|
16
24
|
private readonly stmts;
|
|
17
25
|
private readonly prefix;
|
|
26
|
+
/** Set once a write has resolved, cleared by the flush that syncs it. */
|
|
27
|
+
private dirty;
|
|
28
|
+
private fault;
|
|
18
29
|
constructor(db: SqliteDb, prefix?: string);
|
|
19
30
|
get(key: string): Promise<string | undefined>;
|
|
20
31
|
set(key: string, value: string): Promise<void>;
|
|
21
32
|
delete(key: string): Promise<void>;
|
|
22
33
|
list(prefix: string): Promise<string[]>;
|
|
34
|
+
/** `IKVStore.flush`: one synced commit when this store wrote since its last flush, otherwise nothing. */
|
|
35
|
+
flush(): Promise<void>;
|
|
36
|
+
private throwIfFailed;
|
|
23
37
|
}
|
|
24
38
|
//# sourceMappingURL=sqlite-kv-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-kv-store.d.ts","sourceRoot":"","sources":["../../src/sqlite-kv-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"sqlite-kv-store.d.ts","sourceRoot":"","sources":["../../src/sqlite-kv-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,KAAK,EAAE,QAAQ,EAAmB,MAAM,SAAS,CAAC;AAGzD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,aAAc,YAAW,QAAQ;IAYjC,OAAO,CAAC,QAAQ,CAAC,EAAE;IAX/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAKpB;IACF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,yEAAyE;IACzE,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,KAAK,CAAiC;gBAEjB,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,MAA0B;IAUvE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAO7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMlC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAY7C,yGAAyG;IACnG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAc5B,OAAO,CAAC,aAAa;CAGrB"}
|