@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/src/db.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* consumers; the `SqliteDb` interface is an internal seam for tests.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { createLogger } from './logger.js';
|
|
15
|
+
|
|
16
|
+
const log = createLogger('db');
|
|
17
|
+
|
|
14
18
|
/** SQL parameter value — covers everything `IRawStorage` and `IKVStore` write. */
|
|
15
19
|
export type SqliteParam = string | number | Uint8Array | null;
|
|
16
20
|
|
|
@@ -49,6 +53,18 @@ export interface SqliteTransaction {
|
|
|
49
53
|
prepare(sql: string): SqliteStatement;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/** Options of {@link SqliteDb.transaction}. */
|
|
57
|
+
export interface SqliteTransactionOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Commit under `PRAGMA synchronous = FULL`, which in write-ahead-log mode syncs the log file at
|
|
60
|
+
* this commit. The log is one append-only file, so that sync covers every frame before it: every
|
|
61
|
+
* earlier `NORMAL` commit on this database, whichever store or helper made it. The pragma is set
|
|
62
|
+
* inside the mutex slot the transaction holds and restored to `NORMAL` after the commit, or after
|
|
63
|
+
* the rollback, so no other write on the connection pays for the sync by accident. Default false.
|
|
64
|
+
*/
|
|
65
|
+
readonly synced?: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
/**
|
|
53
69
|
* Minimal SQLite driver surface used by this package.
|
|
54
70
|
*
|
|
@@ -70,6 +86,10 @@ export interface SqliteDb {
|
|
|
70
86
|
* Run `fn` inside `BEGIN IMMEDIATE ... COMMIT` (rolls back on throw), holding
|
|
71
87
|
* the connection mutex for the whole body. `fn` receives a `SqliteTransaction`
|
|
72
88
|
* whose statements run directly on the open transaction without re-locking.
|
|
89
|
+
* With `options.synced` the commit also syncs the write-ahead log, which makes
|
|
90
|
+
* this transaction and every earlier commit on the database survive a power
|
|
91
|
+
* cut (see {@link SqliteTransactionOptions.synced}); without it the commit
|
|
92
|
+
* survives a crash of this process only, until a later synced commit.
|
|
73
93
|
*
|
|
74
94
|
* NOTE: `fn` must issue its writes through the provided `tx`, never through a
|
|
75
95
|
* class-level statement from `db.prepare(...)` (those re-acquire the mutex and
|
|
@@ -77,11 +97,70 @@ export interface SqliteDb {
|
|
|
77
97
|
* `db.transaction(...)` again — nested calls deadlock for the same reason. No
|
|
78
98
|
* current caller does either; guard here if that ever changes.
|
|
79
99
|
*/
|
|
80
|
-
transaction<T>(fn: (tx: SqliteTransaction) => Promise<T
|
|
100
|
+
transaction<T>(fn: (tx: SqliteTransaction) => Promise<T>, options?: SqliteTransactionOptions): Promise<T>;
|
|
81
101
|
/** Release the underlying handle. */
|
|
82
102
|
close(): Promise<void>;
|
|
83
103
|
}
|
|
84
104
|
|
|
105
|
+
/** The raw connection surface {@link runTransaction} drives: one statement at a time, no rows back. */
|
|
106
|
+
export interface TransactionConnection {
|
|
107
|
+
execute(sql: string): Promise<void> | void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The transaction protocol both `SqliteDb` implementations run inside their mutex slot: with
|
|
112
|
+
* `synced`, `PRAGMA synchronous = FULL` first; then `BEGIN IMMEDIATE`, the body, and `COMMIT`, or
|
|
113
|
+
* `ROLLBACK` when the body or the commit throws; then, synced, `PRAGMA synchronous = NORMAL`
|
|
114
|
+
* whichever way the transaction ended, so the connection's default is restored before the slot is
|
|
115
|
+
* released. `BEGIN IMMEDIATE` takes the write lock up front rather than deferring it to the first
|
|
116
|
+
* write, so contention surfaces here and not mid-body.
|
|
117
|
+
*
|
|
118
|
+
* A failure of the restore after a failed body is logged rather than thrown, so the body's error,
|
|
119
|
+
* the one that matters, is the one the caller sees; after a committed body it is thrown, since the
|
|
120
|
+
* connection would otherwise sync every later commit unasked.
|
|
121
|
+
*/
|
|
122
|
+
export async function runTransaction<T>(
|
|
123
|
+
conn: TransactionConnection,
|
|
124
|
+
tx: SqliteTransaction,
|
|
125
|
+
fn: (tx: SqliteTransaction) => Promise<T>,
|
|
126
|
+
options?: SqliteTransactionOptions,
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
const synced = options?.synced === true;
|
|
129
|
+
if (synced) await conn.execute('PRAGMA synchronous = FULL');
|
|
130
|
+
let result: T;
|
|
131
|
+
try {
|
|
132
|
+
result = await commitOrRollBack(conn, tx, fn);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (synced) {
|
|
135
|
+
try {
|
|
136
|
+
await conn.execute('PRAGMA synchronous = NORMAL');
|
|
137
|
+
} catch (restoreErr) {
|
|
138
|
+
log('restoring synchronous = NORMAL failed after a failed transaction: %o', restoreErr);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
if (synced) await conn.execute('PRAGMA synchronous = NORMAL');
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function commitOrRollBack<T>(conn: TransactionConnection, tx: SqliteTransaction, fn: (tx: SqliteTransaction) => Promise<T>): Promise<T> {
|
|
148
|
+
await conn.execute('BEGIN IMMEDIATE');
|
|
149
|
+
try {
|
|
150
|
+
const result = await fn(tx);
|
|
151
|
+
await conn.execute('COMMIT');
|
|
152
|
+
return result;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
try {
|
|
155
|
+
await conn.execute('ROLLBACK');
|
|
156
|
+
} catch (rollbackErr) {
|
|
157
|
+
// The original error is the one to surface; the rollback's is only logged.
|
|
158
|
+
log('rollback failed after a failed transaction: %o', rollbackErr);
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
85
164
|
export const DEFAULT_DB_NAME = 'optimystic.sqlite';
|
|
86
165
|
export const DEFAULT_DB_VERSION = 1;
|
|
87
166
|
|
|
@@ -90,11 +169,11 @@ export const DEFAULT_DB_VERSION = 1;
|
|
|
90
169
|
*
|
|
91
170
|
* Mirrors the IndexedDB object stores 1:1. The six block-storage stores keep
|
|
92
171
|
* their original columns and keys, but their value columns are now BLOB: the
|
|
93
|
-
* shared `KvRawStorage` kernel (
|
|
94
|
-
* UTF-8 (de)serialization, so the
|
|
95
|
-
* `Uint8Array` values. SQLite stores a `Uint8Array` as a BLOB
|
|
96
|
-
* a `Uint8Array`, so bytes round-trip exactly (a TEXT column
|
|
97
|
-
* coercion corrupting non-ASCII JSON bytes). Keys stay TEXT/INTEGER.
|
|
172
|
+
* shared `KvRawStorage` kernel (over `WriteGroupDriver` over `SqliteBatchBackend`)
|
|
173
|
+
* owns all JSON / UTF-8 (de)serialization, so the backend only ever binds/reads
|
|
174
|
+
* the kernel's raw `Uint8Array` values. SQLite stores a `Uint8Array` as a BLOB
|
|
175
|
+
* and returns it as a `Uint8Array`, so bytes round-trip exactly (a TEXT column
|
|
176
|
+
* would risk UTF-8 coercion corrupting non-ASCII JSON bytes). Keys stay TEXT/INTEGER.
|
|
98
177
|
* - `metadata` → per-block `BlockMetadata` bytes keyed by `block_id`.
|
|
99
178
|
* - `revisions` → revision lookup keyed by `(block_id, rev)`; `action_id` column
|
|
100
179
|
* holds the kernel's encoded `ActionId` bytes (BLOB).
|
|
@@ -107,6 +186,12 @@ export const DEFAULT_DB_VERSION = 1;
|
|
|
107
186
|
* - `kv` → generic string keyspace shared by `IKVStore` (`s_val`) and the
|
|
108
187
|
* identity helper (`b_val`). NOT kernel-backed; unchanged. The two columns
|
|
109
188
|
* avoid a base64 round-trip for the libp2p private key.
|
|
189
|
+
* - `durability_mark` → one row, bumped by `syncSqliteDb` (`sqlite-sync.ts`) so a
|
|
190
|
+
* sync that has no rows of its own to write still has a frame to commit.
|
|
191
|
+
*
|
|
192
|
+
* NOTE: this package's SQL is uppercase throughout, predating the lowercase rule in
|
|
193
|
+
* AGENTS.md, and new statements have kept to it rather than mix cases within one file;
|
|
194
|
+
* convert the package in one pass rather than statement by statement.
|
|
110
195
|
*
|
|
111
196
|
* NOTE: value columns changed TEXT→BLOB with the kernel refactor, but every
|
|
112
197
|
* statement is `CREATE TABLE IF NOT EXISTS` — so a database file created by a
|
|
@@ -163,6 +248,11 @@ CREATE TABLE IF NOT EXISTS kv (
|
|
|
163
248
|
s_val TEXT,
|
|
164
249
|
b_val BLOB
|
|
165
250
|
);
|
|
251
|
+
|
|
252
|
+
CREATE TABLE IF NOT EXISTS durability_mark (
|
|
253
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
254
|
+
n INTEGER NOT NULL
|
|
255
|
+
);
|
|
166
256
|
`;
|
|
167
257
|
|
|
168
258
|
// Assignment-form pragmas return no rows, so they go through exec()/execute().
|
package/src/identity.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys';
|
|
2
2
|
import type { PrivateKey } from '@libp2p/interface';
|
|
3
3
|
import type { SqliteDb } from './db.js';
|
|
4
|
+
import { syncSqliteDb } from './sqlite-sync.js';
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_PEER_KEY_NAME = 'peer-private-key';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Loads the persisted NativeScript peer's libp2p private key, or generates a
|
|
9
|
-
* fresh Ed25519 key on first call and
|
|
10
|
+
* fresh Ed25519 key on first call, persists it, and syncs the database before
|
|
11
|
+
* returning it, so a power cut right after first start cannot give the node a
|
|
12
|
+
* different peer id next time. An existing key is read as it is.
|
|
10
13
|
*
|
|
11
14
|
* The key is stored as raw protobuf bytes in the `kv` table's `b_val` column
|
|
12
15
|
* under `keyName`. SQLite stores BLOB natively, so there is no base64
|
|
@@ -30,5 +33,6 @@ export async function loadOrCreateNSPeerKey(
|
|
|
30
33
|
await db
|
|
31
34
|
.prepare('INSERT INTO kv (key, s_val, b_val) VALUES (?, NULL, ?) ON CONFLICT(key) DO UPDATE SET b_val = excluded.b_val')
|
|
32
35
|
.run(keyName, bytes);
|
|
36
|
+
await syncSqliteDb(db);
|
|
33
37
|
return key;
|
|
34
38
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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';
|
package/src/ns-opener.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConnectionMutex } from './connection-mutex.js';
|
|
2
|
-
import { applySchema, DEFAULT_DB_NAME, DEFAULT_DB_VERSION, type OptimysticNSDBHandle, type SqliteDb, type SqliteParam, type SqliteRow, type SqliteStatement, type SqliteTransaction } from './db.js';
|
|
2
|
+
import { applySchema, runTransaction, DEFAULT_DB_NAME, DEFAULT_DB_VERSION, type OptimysticNSDBHandle, type SqliteDb, type SqliteParam, type SqliteRow, type SqliteStatement, type SqliteTransaction, type SqliteTransactionOptions } from './db.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Minimal subset of `@nativescript-community/sqlite`'s `Db` we depend on.
|
|
@@ -86,29 +86,15 @@ class NSPluginDbWrapper implements SqliteDb {
|
|
|
86
86
|
return new NSPluginStatement(this.raw, sql, this.mutex);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
async transaction<T>(fn: (tx: SqliteTransaction) => Promise<T
|
|
90
|
-
return this.mutex.serialize(
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
prepare: (sql: string) => new NSPluginStatement(this.raw, sql),
|
|
99
|
-
};
|
|
100
|
-
const result = await fn(tx);
|
|
101
|
-
await this.raw.execute('COMMIT');
|
|
102
|
-
return result;
|
|
103
|
-
} catch (err) {
|
|
104
|
-
try {
|
|
105
|
-
await this.raw.execute('ROLLBACK');
|
|
106
|
-
} catch {
|
|
107
|
-
// Swallow rollback failures so we surface the original error.
|
|
108
|
-
}
|
|
109
|
-
throw err;
|
|
110
|
-
}
|
|
111
|
-
});
|
|
89
|
+
async transaction<T>(fn: (tx: SqliteTransaction) => Promise<T>, options?: SqliteTransactionOptions): Promise<T> {
|
|
90
|
+
return this.mutex.serialize(() => runTransaction(
|
|
91
|
+
{ execute: async (sql) => { await this.raw.execute(sql); } },
|
|
92
|
+
// Statements bound to the open transaction bypass the mutex — we
|
|
93
|
+
// already hold the slot; re-locking would deadlock.
|
|
94
|
+
{ prepare: (sql: string) => new NSPluginStatement(this.raw, sql) },
|
|
95
|
+
fn,
|
|
96
|
+
options,
|
|
97
|
+
));
|
|
112
98
|
}
|
|
113
99
|
|
|
114
100
|
async close(): Promise<void> {
|
|
@@ -141,11 +127,16 @@ class NSPluginStatement implements SqliteStatement {
|
|
|
141
127
|
// NOTE: reads run directly on the connection, unserialized, to preserve read
|
|
142
128
|
// concurrency. A read issued while a write transaction is open on this same
|
|
143
129
|
// connection observes that transaction's UNCOMMITTED rows (read-your-connection
|
|
144
|
-
// semantics). Fine today: the
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
130
|
+
// semantics). Fine today: the one transaction writer of the block tables is
|
|
131
|
+
// `SqliteBatchBackend.commit`, and every row it is writing is held by the
|
|
132
|
+
// `WriteGroupDriver` overlay above it until the batch is durable. A point read of
|
|
133
|
+
// such a row is answered by the overlay and never reaches this connection; a
|
|
134
|
+
// listing does reach it, and may see the batch's uncommitted rows, but merges the
|
|
135
|
+
// overlay over them, so it answers the same whether the transaction has committed
|
|
136
|
+
// or not (and a rolled-back batch latches the store). The key-value store and the
|
|
137
|
+
// identity helper write other tables, by autocommit statements, and the sync's
|
|
138
|
+
// `durability_mark` row is read by nobody. If a reader of a batch's rows ever
|
|
139
|
+
// bypasses the overlay, serialize reads too.
|
|
149
140
|
const row = await this.raw.get(this.sql, params);
|
|
150
141
|
if (row === null || row === undefined) return undefined;
|
|
151
142
|
return row as SqliteRow;
|
package/src/sqlite-kv-store.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { StoreFailedError, type IKVStore } from '@optimystic/db-p2p';
|
|
2
2
|
import type { SqliteDb, SqliteStatement } from './db.js';
|
|
3
|
+
import { syncSqliteDb } from './sqlite-sync.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* SQLite-backed `IKVStore` adapter for NativeScript peers.
|
|
@@ -12,6 +13,13 @@ import type { SqliteDb, SqliteStatement } from './db.js';
|
|
|
12
13
|
*
|
|
13
14
|
* Only the `s_val` column is read or written here; the `b_val` column is
|
|
14
15
|
* reserved for binary identity material.
|
|
16
|
+
*
|
|
17
|
+
* Writes are autocommit statements: each is visible, and survives a crash of this process, once it
|
|
18
|
+
* resolves, and the write-ahead log keeps them in order. `flush()` makes them survive a power cut:
|
|
19
|
+
* when anything was written since the last flush it runs `syncSqliteDb`, one synced commit that
|
|
20
|
+
* syncs the shared log and with it every earlier write on the connection; otherwise it resolves at
|
|
21
|
+
* once. A failed sync latches the store, after which every write and flush throws
|
|
22
|
+
* `StoreFailedError` (rule 5 of the flush contract on `RawStoreDriver.flush`); reads keep working.
|
|
15
23
|
*/
|
|
16
24
|
export class SqliteKVStore implements IKVStore {
|
|
17
25
|
private readonly stmts: {
|
|
@@ -21,8 +29,11 @@ export class SqliteKVStore implements IKVStore {
|
|
|
21
29
|
list: SqliteStatement;
|
|
22
30
|
};
|
|
23
31
|
private readonly prefix: string;
|
|
32
|
+
/** Set once a write has resolved, cleared by the flush that syncs it. */
|
|
33
|
+
private dirty = false;
|
|
34
|
+
private fault: { cause: unknown } | undefined;
|
|
24
35
|
|
|
25
|
-
constructor(db: SqliteDb, prefix: string = 'optimystic:txn:') {
|
|
36
|
+
constructor(private readonly db: SqliteDb, prefix: string = 'optimystic:txn:') {
|
|
26
37
|
this.prefix = prefix;
|
|
27
38
|
this.stmts = {
|
|
28
39
|
get: db.prepare('SELECT s_val FROM kv WHERE key = ?'),
|
|
@@ -40,11 +51,15 @@ export class SqliteKVStore implements IKVStore {
|
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
async set(key: string, value: string): Promise<void> {
|
|
54
|
+
this.throwIfFailed();
|
|
43
55
|
await this.stmts.set.run(this.prefix + key, value);
|
|
56
|
+
this.dirty = true;
|
|
44
57
|
}
|
|
45
58
|
|
|
46
59
|
async delete(key: string): Promise<void> {
|
|
60
|
+
this.throwIfFailed();
|
|
47
61
|
await this.stmts.delete.run(this.prefix + key);
|
|
62
|
+
this.dirty = true;
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
async list(prefix: string): Promise<string[]> {
|
|
@@ -58,4 +73,23 @@ export class SqliteKVStore implements IKVStore {
|
|
|
58
73
|
const rows = await this.stmts.list.all(fullPrefix, upper);
|
|
59
74
|
return rows.map(row => (row.key as string).slice(this.prefix.length));
|
|
60
75
|
}
|
|
76
|
+
|
|
77
|
+
/** `IKVStore.flush`: one synced commit when this store wrote since its last flush, otherwise nothing. */
|
|
78
|
+
async flush(): Promise<void> {
|
|
79
|
+
this.throwIfFailed();
|
|
80
|
+
if (!this.dirty) return;
|
|
81
|
+
// Cleared before the sync, so a write resolving while the sync runs is covered by the next
|
|
82
|
+
// flush whether or not this sync's commit happened to come after it.
|
|
83
|
+
this.dirty = false;
|
|
84
|
+
try {
|
|
85
|
+
await syncSqliteDb(this.db);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
this.fault ??= { cause: err };
|
|
88
|
+
throw new StoreFailedError(this.fault.cause);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private throwIfFailed(): void {
|
|
93
|
+
if (this.fault) throw new StoreFailedError(this.fault.cause);
|
|
94
|
+
}
|
|
61
95
|
}
|