@aztec/kv-store 0.0.1-commit.b8a057fa → 0.0.1-commit.be03c316
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/dest/deprecated/indexeddb/index.d.ts +1 -7
- package/dest/deprecated/indexeddb/index.d.ts.map +1 -1
- package/dest/deprecated/indexeddb/index.js +0 -12
- package/dest/sqlite-opfs/errors.d.ts +28 -1
- package/dest/sqlite-opfs/errors.d.ts.map +1 -1
- package/dest/sqlite-opfs/errors.js +43 -0
- package/dest/sqlite-opfs/index.d.ts +3 -5
- package/dest/sqlite-opfs/index.d.ts.map +1 -1
- package/dest/sqlite-opfs/index.js +2 -8
- package/dest/sqlite-opfs/manage.d.ts +22 -0
- package/dest/sqlite-opfs/manage.d.ts.map +1 -0
- package/dest/sqlite-opfs/manage.js +38 -0
- package/dest/sqlite-opfs/pool_integrity.d.ts +20 -0
- package/dest/sqlite-opfs/pool_integrity.d.ts.map +1 -0
- package/dest/sqlite-opfs/pool_integrity.js +282 -0
- package/dest/sqlite-opfs/pool_lock.d.ts +8 -0
- package/dest/sqlite-opfs/pool_lock.d.ts.map +1 -0
- package/dest/sqlite-opfs/pool_lock.js +47 -0
- package/dest/sqlite-opfs/store.d.ts +6 -1
- package/dest/sqlite-opfs/store.d.ts.map +1 -1
- package/dest/sqlite-opfs/store.js +69 -33
- package/dest/sqlite-opfs/worker.js +32 -5
- package/package.json +7 -7
- package/src/deprecated/indexeddb/index.ts +1 -26
- package/src/sqlite-opfs/errors.ts +50 -0
- package/src/sqlite-opfs/index.ts +8 -20
- package/src/sqlite-opfs/manage.ts +46 -0
- package/src/sqlite-opfs/pool_integrity.ts +327 -0
- package/src/sqlite-opfs/pool_lock.ts +62 -0
- package/src/sqlite-opfs/store.ts +57 -23
- package/src/sqlite-opfs/worker.ts +34 -5
|
@@ -21,6 +21,7 @@ import type { ResultRow, SqlValue } from './messages.js';
|
|
|
21
21
|
export declare class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
|
|
22
22
|
#private;
|
|
23
23
|
readonly isEphemeral: boolean;
|
|
24
|
+
private readonly poolLock?;
|
|
24
25
|
private constructor();
|
|
25
26
|
/**
|
|
26
27
|
* Opens (or creates) a SQLite database stored in the OPFS SAH Pool. When `ephemeral`
|
|
@@ -29,6 +30,10 @@ export declare class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
|
|
|
29
30
|
* required when multiple stores coexist in the same tab, because the SAH Pool holds
|
|
30
31
|
* an exclusive lock on its directory.
|
|
31
32
|
*
|
|
33
|
+
* Persistent stores hold an origin-wide Web Lock for the pool directory until close
|
|
34
|
+
* or delete. If another store instance already owns it, open fails immediately with
|
|
35
|
+
* `SqlitePoolBusyError`.
|
|
36
|
+
*
|
|
32
37
|
* Pass `encryptionKey` (exactly 32 bytes) to enable at-rest encryption via sqlite3mc's
|
|
33
38
|
* ChaCha20 page cipher. The key buffer is **transferred** to the worker — its
|
|
34
39
|
* ArrayBuffer detaches on the caller side after `postMessage`. This is intentional:
|
|
@@ -76,4 +81,4 @@ export declare class AztecSQLiteOPFSStore implements AztecAsyncKVStore {
|
|
|
76
81
|
/** Runs a SELECT statement and returns rows in array row-mode. */
|
|
77
82
|
allAsync(sql: string, bind?: SqlValue[]): Promise<ResultRow[]>;
|
|
78
83
|
}
|
|
79
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
84
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmUuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zcWxpdGUtb3Bmcy9zdG9yZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUdwRCxPQUFPLEtBQUssRUFBRSxlQUFlLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUM5RCxPQUFPLEtBQUssRUFBRSxHQUFHLEVBQUUsU0FBUyxFQUFFLEtBQUssRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3JFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDbEUsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDMUQsT0FBTyxLQUFLLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQztBQUNyRSxPQUFPLEtBQUssRUFBRSxhQUFhLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUMxRCxPQUFPLEtBQUssRUFBRSxtQkFBbUIsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ3RFLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFJaEUsT0FBTyxLQUFLLEVBQUUsU0FBUyxFQUFFLFFBQVEsRUFBaUMsTUFBTSxlQUFlLENBQUM7QUFPeEY7Ozs7Ozs7OztHQVNHO0FBQ0gscUJBQWEsb0JBQXFCLFlBQVcsaUJBQWlCOzthQWUxQyxXQUFXLEVBQUUsT0FBTztJQUNwQyxPQUFPLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQztJQUw1QixPQUFPLGVBMEJOO0lBRUQ7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7T0FtQkc7SUFDSCxPQUFhLElBQUksQ0FDZixHQUFHLEVBQUUsTUFBTSxFQUNYLElBQUksQ0FBQyxFQUFFLE1BQU0sRUFDYixTQUFTLEdBQUUsT0FBZSxFQUMxQixhQUFhLENBQUMsRUFBRSxNQUFNLEVBQ3RCLGFBQWEsQ0FBQyxFQUFFLFVBQVUsR0FDekIsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBMkMvQjtJQUVELE9BQU8sQ0FBQyxDQUFDLFNBQVMsR0FBRyxFQUFFLENBQUMsU0FBUyxLQUFLLEVBQUUsSUFBSSxFQUFFLE1BQU0sR0FBRyxhQUFhLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUV6RTtJQUVELE9BQU8sQ0FBQyxDQUFDLFNBQVMsR0FBRyxFQUFFLElBQUksRUFBRSxNQUFNLEdBQUcsYUFBYSxDQUFDLENBQUMsQ0FBQyxDQUVyRDtJQUVELFlBQVksQ0FBQyxDQUFDLFNBQVMsR0FBRyxFQUFFLENBQUMsU0FBUyxLQUFLLEVBQUUsSUFBSSxFQUFFLE1BQU0sR0FBRyxrQkFBa0IsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBRW5GO0lBRUQsV0FBVyxDQUFDLENBQUMsU0FBUyxHQUFHLEVBQUUsS0FBSyxFQUFFLE1BQU0sR0FBRyxpQkFBaUIsQ0FBQyxDQUFDLENBQUMsQ0FFOUQ7SUFFRCxTQUFTLENBQUMsQ0FBQyxTQUFTLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxHQUFHLGVBQWUsQ0FBQyxDQUFDLENBQUMsQ0FFM0Q7SUFFRCxhQUFhLENBQUMsQ0FBQyxTQUFTLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxHQUFHLG1CQUFtQixDQUFDLENBQUMsQ0FBQyxDQUVuRTtJQUVELGdCQUFnQixDQUFDLENBQUMsRUFBRSxRQUFRLEVBQUUsTUFBTSxPQUFPLENBQUMsQ0FBQyxDQUFDLEdBQUcsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQXdCMUQ7SUFFSyxLQUFLLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUUzQjtJQUVLLE1BQU0sSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBZTVCO0lBRUQ7Ozs7Ozs7T0FPRztJQUNILFlBQVksSUFBSSxPQUFPLENBQUMsU0FBUyxDQUFDLENBRWpDO0lBRUssS0FBSyxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FhM0I7SUFFRCxRQUFRLENBQUMsUUFBUSxFQUFFLE1BQU0sRUFBRSxRQUFRLENBQUMsRUFBRSxPQUFPLEdBQUcsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUU1RDtJQUVEOzs7O09BSUc7SUFDRyxRQUFRLElBQUksT0FBTyxDQUFDLFVBQVUsQ0FBQyxDQU1wQztJQUVEOzs7O09BSUc7SUFDSCxRQUFRLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxJQUFJLENBQUMsRUFBRSxRQUFRLEVBQUUsR0FBRyxPQUFPLENBQUM7UUFBRSxPQUFPLEVBQUUsTUFBTSxDQUFBO0tBQUUsQ0FBQyxDQU1yRTtJQUVELGtFQUFrRTtJQUNsRSxRQUFRLENBQUMsR0FBRyxFQUFFLE1BQU0sRUFBRSxJQUFJLENBQUMsRUFBRSxRQUFRLEVBQUUsR0FBRyxPQUFPLENBQUMsU0FBUyxFQUFFLENBQUMsQ0FJN0Q7Q0F1REYifQ==
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite-opfs/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAGpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAIhE,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAiC,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/sqlite-opfs/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { SerialQueue } from '@aztec/foundation/queue';
|
|
2
2
|
import { SQLiteOPFSAztecArray } from './array.js';
|
|
3
|
-
import { SqliteEncryptionError } from './errors.js';
|
|
3
|
+
import { SqliteCorruptionError, SqliteEncryptionError, isCorruptionMessage } from './errors.js';
|
|
4
4
|
import { SQLiteOPFSAztecMap } from './map.js';
|
|
5
5
|
import { SQLiteOPFSAztecMultiMap } from './multi_map.js';
|
|
6
|
+
import { quarantineDuplicatePool } from './pool_integrity.js';
|
|
7
|
+
import { acquirePoolLock, normalizePoolDirectory } from './pool_lock.js';
|
|
6
8
|
import { SQLiteOPFSAztecSet } from './set.js';
|
|
7
9
|
import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
8
10
|
/**
|
|
@@ -16,6 +18,7 @@ import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
|
16
18
|
* nested ops bypass it to avoid deadlock.
|
|
17
19
|
*/ export class AztecSQLiteOPFSStore {
|
|
18
20
|
isEphemeral;
|
|
21
|
+
poolLock;
|
|
19
22
|
#worker;
|
|
20
23
|
#pending;
|
|
21
24
|
#txQueue;
|
|
@@ -24,13 +27,16 @@ import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
|
24
27
|
#nextId;
|
|
25
28
|
#inTx;
|
|
26
29
|
#closed;
|
|
27
|
-
|
|
30
|
+
#workerFailed;
|
|
31
|
+
constructor(worker, name, log, isEphemeral, poolLock){
|
|
28
32
|
this.isEphemeral = isEphemeral;
|
|
33
|
+
this.poolLock = poolLock;
|
|
29
34
|
this.#pending = new Map();
|
|
30
35
|
this.#txQueue = new SerialQueue();
|
|
31
36
|
this.#nextId = 0;
|
|
32
37
|
this.#inTx = false;
|
|
33
38
|
this.#closed = false;
|
|
39
|
+
this.#workerFailed = false;
|
|
34
40
|
this.#worker = worker;
|
|
35
41
|
this.#name = name;
|
|
36
42
|
this.#log = log;
|
|
@@ -45,6 +51,7 @@ import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
|
45
51
|
handler.resolve(ev.data);
|
|
46
52
|
};
|
|
47
53
|
this.#worker.onerror = (ev)=>{
|
|
54
|
+
this.#workerFailed = true;
|
|
48
55
|
this.#log.error(`SQLite worker crashed: ${ev.message}`);
|
|
49
56
|
this.#rejectPending(`SQLite worker crashed: ${ev.message}`);
|
|
50
57
|
};
|
|
@@ -57,6 +64,10 @@ import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
|
57
64
|
* required when multiple stores coexist in the same tab, because the SAH Pool holds
|
|
58
65
|
* an exclusive lock on its directory.
|
|
59
66
|
*
|
|
67
|
+
* Persistent stores hold an origin-wide Web Lock for the pool directory until close
|
|
68
|
+
* or delete. If another store instance already owns it, open fails immediately with
|
|
69
|
+
* `SqlitePoolBusyError`.
|
|
70
|
+
*
|
|
60
71
|
* Pass `encryptionKey` (exactly 32 bytes) to enable at-rest encryption via sqlite3mc's
|
|
61
72
|
* ChaCha20 page cipher. The key buffer is **transferred** to the worker — its
|
|
62
73
|
* ArrayBuffer detaches on the caller side after `postMessage`. This is intentional:
|
|
@@ -74,30 +85,40 @@ import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
|
74
85
|
}
|
|
75
86
|
const dbName = name && !ephemeral ? name : `tmp-${globalThis.crypto.getRandomValues(new Uint8Array(8)).join('')}`;
|
|
76
87
|
log.debug(`Opening SQLite-OPFS ${ephemeral ? 'ephemeral ' : ''}${encryptionKey ? 'encrypted ' : ''}database ${dbName}`);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const store = new AztecSQLiteOPFSStore(worker, dbName, log, ephemeral);
|
|
81
|
-
// Transfer (not clone) the key buffer to the worker so we don't leave a
|
|
82
|
-
// second copy on the main thread. Caveat: this detaches the caller's
|
|
83
|
-
// encryptionKey.buffer — subsequent reads from the same Uint8Array are empty.
|
|
84
|
-
const transfer = encryptionKey ? [
|
|
85
|
-
encryptionKey.buffer
|
|
86
|
-
] : undefined;
|
|
88
|
+
const effectivePoolDirectory = ephemeral ? undefined : normalizePoolDirectory(poolDirectory);
|
|
89
|
+
const poolLock = effectivePoolDirectory ? await acquirePoolLock(effectivePoolDirectory) : undefined;
|
|
90
|
+
let worker;
|
|
87
91
|
try {
|
|
92
|
+
if (effectivePoolDirectory) {
|
|
93
|
+
const quarantine = await quarantineDuplicatePool(effectivePoolDirectory);
|
|
94
|
+
if (quarantine) {
|
|
95
|
+
log.warn(`Quarantined SQLite-OPFS pool with duplicate logical file mappings`, quarantine);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
worker = new Worker(new URL('./worker.js', import.meta.url), {
|
|
99
|
+
type: 'module'
|
|
100
|
+
});
|
|
101
|
+
const store = new AztecSQLiteOPFSStore(worker, dbName, log, ephemeral, poolLock);
|
|
102
|
+
// Transfer (not clone) the key buffer to the worker so we don't leave a
|
|
103
|
+
// second copy on the main thread. Caveat: this detaches the caller's
|
|
104
|
+
// encryptionKey.buffer — subsequent reads from the same Uint8Array are empty.
|
|
105
|
+
const transfer = encryptionKey ? [
|
|
106
|
+
encryptionKey.buffer
|
|
107
|
+
] : undefined;
|
|
88
108
|
await store.#sendRequest({
|
|
89
109
|
type: 'init',
|
|
90
110
|
id: store.#allocId(),
|
|
91
111
|
dbName,
|
|
92
112
|
ephemeral,
|
|
93
|
-
poolDirectory,
|
|
113
|
+
poolDirectory: effectivePoolDirectory,
|
|
94
114
|
encryptionKey
|
|
95
115
|
}, transfer);
|
|
116
|
+
return store;
|
|
96
117
|
} catch (err) {
|
|
97
|
-
worker
|
|
118
|
+
worker?.terminate();
|
|
119
|
+
await poolLock?.release();
|
|
98
120
|
throw err;
|
|
99
121
|
}
|
|
100
|
-
return store;
|
|
101
122
|
}
|
|
102
123
|
openMap(name) {
|
|
103
124
|
return new SQLiteOPFSAztecMap(this, name);
|
|
@@ -157,14 +178,18 @@ import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
|
157
178
|
return;
|
|
158
179
|
}
|
|
159
180
|
this.#closed = true;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
181
|
+
try {
|
|
182
|
+
await this.#txQueue.end();
|
|
183
|
+
await this.#sendRequest({
|
|
184
|
+
type: 'deleteDb',
|
|
185
|
+
id: this.#allocId(),
|
|
186
|
+
dbName: this.#name
|
|
187
|
+
}).catch((err)=>this.#log.warn(`SQLite deleteDb failed: ${err instanceof Error ? err.message : err}`));
|
|
188
|
+
} finally{
|
|
189
|
+
this.#worker.terminate();
|
|
190
|
+
this.#rejectPending('SQLite store deleted');
|
|
191
|
+
await this.poolLock?.release();
|
|
192
|
+
}
|
|
168
193
|
}
|
|
169
194
|
/**
|
|
170
195
|
* Placeholder — returns zeros to mirror the IndexedDB backend. SQLite exposes real
|
|
@@ -186,13 +211,17 @@ import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
|
186
211
|
return;
|
|
187
212
|
}
|
|
188
213
|
this.#closed = true;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
214
|
+
try {
|
|
215
|
+
await this.#txQueue.end();
|
|
216
|
+
await this.#sendRequest({
|
|
217
|
+
type: 'close',
|
|
218
|
+
id: this.#allocId()
|
|
219
|
+
}).catch(()=>{});
|
|
220
|
+
} finally{
|
|
221
|
+
this.#worker.terminate();
|
|
222
|
+
this.#rejectPending('SQLite store closed');
|
|
223
|
+
await this.poolLock?.release();
|
|
224
|
+
}
|
|
196
225
|
}
|
|
197
226
|
backupTo(_dstPath, _compact) {
|
|
198
227
|
throw new Error('Method not implemented.');
|
|
@@ -253,15 +282,22 @@ import { SQLiteOPFSAztecSingleton } from './singleton.js';
|
|
|
253
282
|
this.#pending.clear();
|
|
254
283
|
}
|
|
255
284
|
#sendRequest(req, transfer) {
|
|
285
|
+
if (this.#workerFailed) {
|
|
286
|
+
return Promise.reject(new Error('SQLite worker has crashed'));
|
|
287
|
+
}
|
|
256
288
|
return new Promise((resolve, reject)=>{
|
|
257
289
|
this.#pending.set(req.id, {
|
|
258
290
|
resolve: (resp)=>{
|
|
259
291
|
if (resp.type === 'err') {
|
|
260
|
-
// Re-hydrate
|
|
261
|
-
//
|
|
262
|
-
//
|
|
292
|
+
// Re-hydrate typed errors so consumers can pattern-match on
|
|
293
|
+
// `instanceof`. Encryption is tagged on the wire (some cases are
|
|
294
|
+
// pre-flight throws with no message to match); corruption is a
|
|
295
|
+
// single unambiguous message, so we classify it here rather than
|
|
296
|
+
// adding a redundant wire field. Everything else stays a plain Error.
|
|
263
297
|
if (resp.encryptionCode !== undefined) {
|
|
264
298
|
reject(new SqliteEncryptionError(resp.encryptionCode, resp.message));
|
|
299
|
+
} else if (isCorruptionMessage(resp.message)) {
|
|
300
|
+
reject(new SqliteCorruptionError(resp.message));
|
|
265
301
|
} else {
|
|
266
302
|
reject(new Error(resp.message));
|
|
267
303
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference lib="webworker" />
|
|
2
2
|
import sqlite3InitModule from '@aztec/sqlite3mc-wasm';
|
|
3
3
|
import { SqliteEncryptionError, isDecryptFailureMessage } from './errors.js';
|
|
4
|
+
import { DEFAULT_SAH_POOL_DIRECTORY } from './pool_lock.js';
|
|
4
5
|
const SCHEMA_SQL = `
|
|
5
6
|
CREATE TABLE IF NOT EXISTS data (
|
|
6
7
|
slot TEXT NOT NULL PRIMARY KEY,
|
|
@@ -15,7 +16,6 @@ const SCHEMA_SQL = `
|
|
|
15
16
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_container_key_count ON data(container, key, key_count);
|
|
16
17
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_container_key_hash ON data(container, key, hash);
|
|
17
18
|
`;
|
|
18
|
-
const DEFAULT_SAH_POOL_DIRECTORY = '.aztec-kv';
|
|
19
19
|
const SAH_POOL_VFS_NAME = 'aztec-kv-opfs';
|
|
20
20
|
const MC_SAH_POOL_VFS_NAME = `multipleciphers-${SAH_POOL_VFS_NAME}`;
|
|
21
21
|
let sqlite3;
|
|
@@ -74,9 +74,24 @@ async function handleInit(dbName, ephemeral, directory, encryptionKey) {
|
|
|
74
74
|
runSql(SCHEMA_SQL);
|
|
75
75
|
}
|
|
76
76
|
function handleClose() {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
try {
|
|
78
|
+
db?.close();
|
|
79
|
+
} finally{
|
|
80
|
+
db = undefined;
|
|
81
|
+
dbPath = undefined;
|
|
82
|
+
releasePool();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Releases the SAH pool's OPFS sync access handles before the terminal RPC is acked. Worker
|
|
87
|
+
* termination releases them only asynchronously, so without this a caller that deletes or reopens
|
|
88
|
+
* the store directory right after close()/delete() resolves races Chromium's cleanup
|
|
89
|
+
* (NoModificationAllowedError from removeEntry, or a hang installing a new pool on the directory).
|
|
90
|
+
* pauseVfs releases the handles without touching file contents; the worker is terminated right
|
|
91
|
+
* after, so the pool is never resumed.
|
|
92
|
+
*/ function releasePool() {
|
|
93
|
+
pool?.pauseVfs();
|
|
94
|
+
pool = undefined;
|
|
80
95
|
}
|
|
81
96
|
async function handleExport() {
|
|
82
97
|
if (!db || !dbPath) {
|
|
@@ -107,6 +122,10 @@ function handleDeleteDb(dbName) {
|
|
|
107
122
|
} catch {
|
|
108
123
|
// File may not exist; ignore.
|
|
109
124
|
}
|
|
125
|
+
// Guarded because pauseVfs refuses (SQLITE_MISUSE) while any file is open through the VFS.
|
|
126
|
+
if (!db) {
|
|
127
|
+
releasePool();
|
|
128
|
+
}
|
|
110
129
|
}
|
|
111
130
|
function requireDb() {
|
|
112
131
|
if (!db) {
|
|
@@ -216,11 +235,19 @@ self.onmessage = async (ev)=>{
|
|
|
216
235
|
}
|
|
217
236
|
} catch (err) {
|
|
218
237
|
const message = err instanceof Error ? err.message : String(err);
|
|
238
|
+
const encryptionCode = detectEncryptionCode(req, err, message);
|
|
239
|
+
if (req.type === 'init') {
|
|
240
|
+
try {
|
|
241
|
+
handleClose();
|
|
242
|
+
} catch {
|
|
243
|
+
// The main thread terminates this worker after a failed init, which releases any remaining OPFS handles.
|
|
244
|
+
}
|
|
245
|
+
}
|
|
219
246
|
respond({
|
|
220
247
|
type: 'err',
|
|
221
248
|
id: req.id,
|
|
222
249
|
message,
|
|
223
|
-
encryptionCode
|
|
250
|
+
encryptionCode
|
|
224
251
|
});
|
|
225
252
|
}
|
|
226
253
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/kv-store",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.be03c316",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/interfaces/index.js",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"./package.local.json"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@aztec/constants": "0.0.1-commit.
|
|
40
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
41
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
42
|
-
"@aztec/native": "0.0.1-commit.
|
|
43
|
-
"@aztec/sqlite3mc-wasm": "0.0.1-commit.
|
|
44
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
39
|
+
"@aztec/constants": "0.0.1-commit.be03c316",
|
|
40
|
+
"@aztec/ethereum": "0.0.1-commit.be03c316",
|
|
41
|
+
"@aztec/foundation": "0.0.1-commit.be03c316",
|
|
42
|
+
"@aztec/native": "0.0.1-commit.be03c316",
|
|
43
|
+
"@aztec/sqlite3mc-wasm": "0.0.1-commit.be03c316",
|
|
44
|
+
"@aztec/stdlib": "0.0.1-commit.be03c316",
|
|
45
45
|
"idb": "^8.0.0",
|
|
46
46
|
"lmdb": "^3.2.0",
|
|
47
47
|
"msgpackr": "^1.11.2",
|
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
|
|
1
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
2
|
|
|
4
|
-
import { initStoreForRollupAndSchemaVersion } from '../../utils.js';
|
|
5
3
|
import { AztecIndexedDBStore } from './store.js';
|
|
6
4
|
|
|
7
5
|
export { AztecIndexedDBStore } from './store.js';
|
|
8
6
|
|
|
9
|
-
/**
|
|
10
|
-
* @deprecated The IndexedDB backend is being retired. Use `@aztec/kv-store/sqlite-opfs` instead.
|
|
11
|
-
*/
|
|
12
|
-
export async function createStore(
|
|
13
|
-
name: string,
|
|
14
|
-
config: DataStoreConfig,
|
|
15
|
-
schemaVersion: number | undefined = undefined,
|
|
16
|
-
log: Logger = createLogger('kv-store'),
|
|
17
|
-
) {
|
|
18
|
-
let { dataDirectory } = config;
|
|
19
|
-
if (typeof dataDirectory !== 'undefined') {
|
|
20
|
-
dataDirectory = `${dataDirectory}/${name}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
log.info(
|
|
24
|
-
dataDirectory
|
|
25
|
-
? `Creating ${name} data store at directory ${dataDirectory} with map size ${config.dataStoreMapSizeKb} KB`
|
|
26
|
-
: `Creating ${name} ephemeral data store with map size ${config.dataStoreMapSizeKb} KB`,
|
|
27
|
-
);
|
|
28
|
-
const store = await AztecIndexedDBStore.open(createLogger('kv-store:indexeddb'), dataDirectory ?? '', false);
|
|
29
|
-
return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
7
|
/**
|
|
33
8
|
* @deprecated The IndexedDB backend is being retired. Use `@aztec/kv-store/sqlite-opfs` instead.
|
|
34
9
|
*/
|
|
@@ -42,3 +42,53 @@ const SQLITE3MC_DECRYPT_ERROR_PATTERNS: readonly RegExp[] = [
|
|
|
42
42
|
export function isDecryptFailureMessage(message: string): boolean {
|
|
43
43
|
return SQLITE3MC_DECRYPT_ERROR_PATTERNS.some(p => p.test(message));
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Error thrown by sqlite-opfs when the on-disk database image is corrupt
|
|
48
|
+
* (`SQLITE_CORRUPT`, result code 11 — "database disk image is malformed").
|
|
49
|
+
*
|
|
50
|
+
* Distinct from {@link SqliteEncryptionError}: no key recovers a corrupt image,
|
|
51
|
+
* so there is nothing to retry. The only escape is to delete the store and start
|
|
52
|
+
* fresh, so consumers pattern-match on `instanceof SqliteCorruptionError` to wipe
|
|
53
|
+
* and reopen rather than surfacing a dead-end error.
|
|
54
|
+
**/
|
|
55
|
+
export class SqliteCorruptionError extends Error {
|
|
56
|
+
constructor(message: string, opts?: { cause?: unknown }) {
|
|
57
|
+
super(message, opts?.cause !== undefined ? { cause: opts.cause } : undefined);
|
|
58
|
+
this.name = 'SqliteCorruptionError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Error thrown when another browser context already owns a store's OPFS pool. */
|
|
63
|
+
export class SqlitePoolBusyError extends Error {
|
|
64
|
+
constructor(public readonly poolDirectory: string) {
|
|
65
|
+
super(`SQLite-OPFS pool "${poolDirectory}" is already in use by another store instance`);
|
|
66
|
+
this.name = 'SqlitePoolBusyError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Error thrown when the browser context does not expose the Web Locks API required by the OPFS SAH pool. */
|
|
71
|
+
export class SqliteWebLocksUnavailableError extends Error {
|
|
72
|
+
constructor() {
|
|
73
|
+
super('SQLite-OPFS requires the Web Locks API, but it is unavailable in this browser context');
|
|
74
|
+
this.name = 'SqliteWebLocksUnavailableError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Strings/codes raised by SQLite when a database image is corrupt. Kept disjoint
|
|
80
|
+
* from {@link SQLITE3MC_DECRYPT_ERROR_PATTERNS}: "file is not a database"
|
|
81
|
+
* (SQLITE_NOTADB) is the decrypt signal, whereas a malformed image is the
|
|
82
|
+
* genuinely-unrecoverable SQLITE_CORRUPT.
|
|
83
|
+
**/
|
|
84
|
+
const SQLITE_CORRUPTION_ERROR_PATTERNS: readonly RegExp[] = [
|
|
85
|
+
/database disk image is malformed/i,
|
|
86
|
+
/\bSQLITE_CORRUPT\b/i,
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns `true` if `message` matches one of the known SQLite corruption strings.
|
|
91
|
+
**/
|
|
92
|
+
export function isCorruptionMessage(message: string): boolean {
|
|
93
|
+
return SQLITE_CORRUPTION_ERROR_PATTERNS.some(p => p.test(message));
|
|
94
|
+
}
|
package/src/sqlite-opfs/index.ts
CHANGED
|
@@ -1,28 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
|
|
1
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
2
|
|
|
4
|
-
import { initStoreForRollupAndSchemaVersion } from '../utils.js';
|
|
5
3
|
import { AztecSQLiteOPFSStore } from './store.js';
|
|
6
4
|
|
|
7
5
|
export { AztecSQLiteOPFSStore } from './store.js';
|
|
8
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
SqliteCorruptionError,
|
|
8
|
+
SqliteEncryptionError,
|
|
9
|
+
SqlitePoolBusyError,
|
|
10
|
+
SqliteWebLocksUnavailableError,
|
|
11
|
+
} from './errors.js';
|
|
9
12
|
export type { SqliteEncryptionErrorCode } from './errors.js';
|
|
10
|
-
|
|
11
|
-
export async function createStore(
|
|
12
|
-
name: string,
|
|
13
|
-
config: DataStoreConfig,
|
|
14
|
-
schemaVersion: number | undefined = undefined,
|
|
15
|
-
log: Logger = createLogger('kv-store'),
|
|
16
|
-
) {
|
|
17
|
-
const { dataDirectory } = config;
|
|
18
|
-
log.info(
|
|
19
|
-
dataDirectory
|
|
20
|
-
? `Creating ${name} SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`
|
|
21
|
-
: `Creating ${name} ephemeral SQLite-OPFS data store with map size ${config.dataStoreMapSizeKb} KB`,
|
|
22
|
-
);
|
|
23
|
-
const store = await AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), name, false);
|
|
24
|
-
return initStoreForRollupAndSchemaVersion(store, schemaVersion, config.rollupAddress, log);
|
|
25
|
-
}
|
|
13
|
+
export { OPFS_POOL_DIR_PREFIX, deletePoolDirectory, deleteStore, listStores, storePoolDirectory } from './manage.js';
|
|
26
14
|
|
|
27
15
|
export function openTmpStore(ephemeral: boolean = false): Promise<AztecSQLiteOPFSStore> {
|
|
28
16
|
return AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), undefined, ephemeral);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { normalizePoolDirectory, withPoolLock } from './pool_lock.js';
|
|
2
|
+
|
|
3
|
+
/** Prefix for the per-store OPFS SAH pool directories owned by this package. */
|
|
4
|
+
export const OPFS_POOL_DIR_PREFIX = '.aztec-kv-';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* OPFS directory holding a store's SAH pool. One directory per store: the SAH-pool VFS allows only one
|
|
8
|
+
* concurrent instance per directory and its capacity does not grow automatically, so sharing a pool across
|
|
9
|
+
* stores would make concurrently opened stores contend for locks and orphaned stores exhaust pool slots.
|
|
10
|
+
*/
|
|
11
|
+
export function storePoolDirectory(effectiveName: string): string {
|
|
12
|
+
return `${OPFS_POOL_DIR_PREFIX}${effectiveName}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Lists the names of every persistent sqlite-opfs store in this origin, by enumerating the per-store pool
|
|
17
|
+
* directories. Includes stores other than the current one, so wallets can surface and clean up data for
|
|
18
|
+
* networks/versions no longer in use.
|
|
19
|
+
*/
|
|
20
|
+
export async function listStores(): Promise<string[]> {
|
|
21
|
+
const root = await navigator.storage.getDirectory();
|
|
22
|
+
const names: string[] = [];
|
|
23
|
+
for await (const [entryName, handle] of root.entries()) {
|
|
24
|
+
if (handle.kind === 'directory' && entryName.startsWith(OPFS_POOL_DIR_PREFIX)) {
|
|
25
|
+
names.push(entryName.slice(OPFS_POOL_DIR_PREFIX.length));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return names;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Permanently deletes a store by effective name (as returned by {@link listStores}). The store must be closed:
|
|
33
|
+
* an open store holds the directory's Web Lock and the removal will reject with `SqlitePoolBusyError`.
|
|
34
|
+
*/
|
|
35
|
+
export async function deleteStore(effectiveName: string): Promise<void> {
|
|
36
|
+
await deletePoolDirectory(storePoolDirectory(effectiveName));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Permanently deletes one OPFS SAH-pool directory after exclusively locking it. */
|
|
40
|
+
export async function deletePoolDirectory(poolDirectory: string): Promise<void> {
|
|
41
|
+
poolDirectory = normalizePoolDirectory(poolDirectory);
|
|
42
|
+
await withPoolLock(poolDirectory, async () => {
|
|
43
|
+
const root = await navigator.storage.getDirectory();
|
|
44
|
+
await root.removeEntry(poolDirectory, { recursive: true });
|
|
45
|
+
});
|
|
46
|
+
}
|