@aztec/kv-store 0.0.1-commit.a5db02d → 0.0.1-commit.aa0c64f
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/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 -3
- package/dest/sqlite-opfs/index.d.ts.map +1 -1
- package/dest/sqlite-opfs/index.js +2 -2
- package/dest/sqlite-opfs/manage.d.ts +4 -2
- package/dest/sqlite-opfs/manage.d.ts.map +1 -1
- package/dest/sqlite-opfs/manage.js +11 -4
- 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 +17 -6
- package/package.json +7 -7
- package/src/sqlite-opfs/errors.ts +50 -0
- package/src/sqlite-opfs/index.ts +7 -2
- package/src/sqlite-opfs/manage.ts +13 -3
- 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 +17 -6
|
@@ -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,10 +74,13 @@ async function handleInit(dbName, ephemeral, directory, encryptionKey) {
|
|
|
74
74
|
runSql(SCHEMA_SQL);
|
|
75
75
|
}
|
|
76
76
|
function handleClose() {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
try {
|
|
78
|
+
db?.close();
|
|
79
|
+
} finally{
|
|
80
|
+
db = undefined;
|
|
81
|
+
dbPath = undefined;
|
|
82
|
+
releasePool();
|
|
83
|
+
}
|
|
81
84
|
}
|
|
82
85
|
/**
|
|
83
86
|
* Releases the SAH pool's OPFS sync access handles before the terminal RPC is acked. Worker
|
|
@@ -232,11 +235,19 @@ self.onmessage = async (ev)=>{
|
|
|
232
235
|
}
|
|
233
236
|
} catch (err) {
|
|
234
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
|
+
}
|
|
235
246
|
respond({
|
|
236
247
|
type: 'err',
|
|
237
248
|
id: req.id,
|
|
238
249
|
message,
|
|
239
|
-
encryptionCode
|
|
250
|
+
encryptionCode
|
|
240
251
|
});
|
|
241
252
|
}
|
|
242
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.aa0c64f",
|
|
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.aa0c64f",
|
|
40
|
+
"@aztec/ethereum": "0.0.1-commit.aa0c64f",
|
|
41
|
+
"@aztec/foundation": "0.0.1-commit.aa0c64f",
|
|
42
|
+
"@aztec/native": "0.0.1-commit.aa0c64f",
|
|
43
|
+
"@aztec/sqlite3mc-wasm": "0.0.1-commit.aa0c64f",
|
|
44
|
+
"@aztec/stdlib": "0.0.1-commit.aa0c64f",
|
|
45
45
|
"idb": "^8.0.0",
|
|
46
46
|
"lmdb": "^3.2.0",
|
|
47
47
|
"msgpackr": "^1.11.2",
|
|
@@ -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
|
@@ -3,9 +3,14 @@ import { createLogger } from '@aztec/foundation/log';
|
|
|
3
3
|
import { AztecSQLiteOPFSStore } from './store.js';
|
|
4
4
|
|
|
5
5
|
export { AztecSQLiteOPFSStore } from './store.js';
|
|
6
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
SqliteCorruptionError,
|
|
8
|
+
SqliteEncryptionError,
|
|
9
|
+
SqlitePoolBusyError,
|
|
10
|
+
SqliteWebLocksUnavailableError,
|
|
11
|
+
} from './errors.js';
|
|
7
12
|
export type { SqliteEncryptionErrorCode } from './errors.js';
|
|
8
|
-
export { OPFS_POOL_DIR_PREFIX, deleteStore, listStores, storePoolDirectory } from './manage.js';
|
|
13
|
+
export { OPFS_POOL_DIR_PREFIX, deletePoolDirectory, deleteStore, listStores, storePoolDirectory } from './manage.js';
|
|
9
14
|
|
|
10
15
|
export function openTmpStore(ephemeral: boolean = false): Promise<AztecSQLiteOPFSStore> {
|
|
11
16
|
return AztecSQLiteOPFSStore.open(createLogger('kv-store:sqlite-opfs'), undefined, ephemeral);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizePoolDirectory, withPoolLock } from './pool_lock.js';
|
|
2
|
+
|
|
1
3
|
/** Prefix for the per-store OPFS SAH pool directories owned by this package. */
|
|
2
4
|
export const OPFS_POOL_DIR_PREFIX = '.aztec-kv-';
|
|
3
5
|
|
|
@@ -28,9 +30,17 @@ export async function listStores(): Promise<string[]> {
|
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Permanently deletes a store by effective name (as returned by {@link listStores}). The store must be closed:
|
|
31
|
-
* an open store
|
|
33
|
+
* an open store holds the directory's Web Lock and the removal will reject with `SqlitePoolBusyError`.
|
|
32
34
|
*/
|
|
33
35
|
export async function deleteStore(effectiveName: string): Promise<void> {
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
});
|
|
36
46
|
}
|