@livestore/wa-sqlite 0.0.0-snapshot-b9a2e1dee494215d8c403be013e25cbf4d2cf380

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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/README.md +64 -0
  4. package/dist/fts5/wa-sqlite.mjs +2 -0
  5. package/dist/fts5/wa-sqlite.node.mjs +2 -0
  6. package/dist/fts5/wa-sqlite.node.wasm +0 -0
  7. package/dist/fts5/wa-sqlite.wasm +0 -0
  8. package/dist/wa-sqlite-async.mjs +2 -0
  9. package/dist/wa-sqlite-async.wasm +0 -0
  10. package/dist/wa-sqlite-jspi.mjs +2 -0
  11. package/dist/wa-sqlite-jspi.wasm +0 -0
  12. package/dist/wa-sqlite.mjs +2 -0
  13. package/dist/wa-sqlite.node.mjs +2 -0
  14. package/dist/wa-sqlite.node.wasm +0 -0
  15. package/dist/wa-sqlite.wasm +0 -0
  16. package/package.json +57 -0
  17. package/src/FacadeVFS.js +681 -0
  18. package/src/VFS.js +222 -0
  19. package/src/WebLocksMixin.js +414 -0
  20. package/src/examples/AccessHandlePoolVFS.js +458 -0
  21. package/src/examples/IDBBatchAtomicVFS.js +827 -0
  22. package/src/examples/IDBMirrorVFS.js +889 -0
  23. package/src/examples/MemoryAsyncVFS.js +100 -0
  24. package/src/examples/MemoryVFS.js +176 -0
  25. package/src/examples/OPFSAdaptiveVFS.js +437 -0
  26. package/src/examples/OPFSAnyContextVFS.js +300 -0
  27. package/src/examples/OPFSCoopSyncVFS.js +590 -0
  28. package/src/examples/OPFSPermutedVFS.js +1217 -0
  29. package/src/examples/README.md +89 -0
  30. package/src/examples/tag.js +82 -0
  31. package/src/sqlite-api.js +1339 -0
  32. package/src/sqlite-constants.js +275 -0
  33. package/src/types/globals.d.ts +60 -0
  34. package/src/types/index.d.ts +1531 -0
  35. package/src/types/tsconfig.json +6 -0
  36. package/test/AccessHandlePoolVFS.test.js +27 -0
  37. package/test/IDBBatchAtomicVFS.test.js +97 -0
  38. package/test/IDBMirrorVFS.test.js +27 -0
  39. package/test/MemoryAsyncVFS.test.js +27 -0
  40. package/test/MemoryVFS.test.js +27 -0
  41. package/test/OPFSAdaptiveVFS.test.js +27 -0
  42. package/test/OPFSAnyContextVFS.test.js +27 -0
  43. package/test/OPFSCoopSyncVFS.test.js +27 -0
  44. package/test/OPFSPermutedVFS.test.js +27 -0
  45. package/test/TestContext.js +96 -0
  46. package/test/WebLocksMixin.test.js +521 -0
  47. package/test/api.test.js +49 -0
  48. package/test/api_exec.js +89 -0
  49. package/test/api_misc.js +63 -0
  50. package/test/api_statements.js +447 -0
  51. package/test/callbacks.test.js +581 -0
  52. package/test/data/idbv5.json +1 -0
  53. package/test/sql.test.js +64 -0
  54. package/test/sql_0001.js +49 -0
  55. package/test/sql_0002.js +52 -0
  56. package/test/sql_0003.js +83 -0
  57. package/test/sql_0004.js +81 -0
  58. package/test/sql_0005.js +76 -0
  59. package/test/test-worker.js +204 -0
  60. package/test/vfs_xAccess.js +2 -0
  61. package/test/vfs_xClose.js +52 -0
  62. package/test/vfs_xOpen.js +91 -0
  63. package/test/vfs_xRead.js +38 -0
  64. package/test/vfs_xWrite.js +36 -0
@@ -0,0 +1,827 @@
1
+ // Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
2
+ import { FacadeVFS } from '../FacadeVFS.js';
3
+ import * as VFS from '../VFS.js';
4
+ import { WebLocksMixin } from '../WebLocksMixin.js';
5
+
6
+ const RETRYABLE_ERRORS = new Set([
7
+ 'TransactionInactiveError',
8
+ 'InvalidStateError'
9
+ ]);
10
+
11
+ /**
12
+ * @typedef Metadata
13
+ * @property {string} name
14
+ * @property {number} fileSize
15
+ * @property {number} version
16
+ * @property {number} [pendingVersion]
17
+ */
18
+
19
+ class File {
20
+ /** @type {string} */ path;
21
+ /** @type {number} */ flags;
22
+
23
+ /** @type {Metadata} */ metadata;
24
+ /** @type {number} */ fileSize = 0;
25
+
26
+ /** @type {boolean} */ needsMetadataSync = false;
27
+ /** @type {Metadata} */ rollback = null;
28
+ /** @type {Set<number>} */ changedPages = new Set();
29
+
30
+ /** @type {string} */ synchronous = 'full';
31
+ /** @type {IDBTransactionOptions} */ txOptions = { durability: 'strict' };
32
+
33
+ constructor(path, flags, metadata) {
34
+ this.path = path;
35
+ this.flags = flags;
36
+ this.metadata = metadata;
37
+ }
38
+ }
39
+
40
+ export class IDBBatchAtomicVFS extends WebLocksMixin(FacadeVFS) {
41
+ /** @type {Map<number, File>} */ mapIdToFile = new Map();
42
+ lastError = null;
43
+
44
+ log = null; // console.log
45
+
46
+ /** @type {Promise} */ #isReady;
47
+ /** @type {IDBContext} */ #idb;
48
+
49
+ static async create(name, module, options) {
50
+ const vfs = new IDBBatchAtomicVFS(name, module, options);
51
+ await vfs.isReady();
52
+ return vfs;
53
+ }
54
+
55
+ constructor(name, module, options = {}) {
56
+ super(name, module, options);
57
+ this.#isReady = this.#initialize(options.idbName ?? name);
58
+ }
59
+
60
+ async #initialize(name) {
61
+ this.#idb = await IDBContext.create(name);
62
+ }
63
+
64
+ close() {
65
+ this.#idb.close();
66
+ }
67
+
68
+ async isReady() {
69
+ await super.isReady();
70
+ await this.#isReady;
71
+ }
72
+
73
+ getFilename(fileId) {
74
+ const pathname = this.mapIdToFile.get(fileId).path;
75
+ return `IDB(${this.name}):${pathname}`
76
+ }
77
+
78
+ /**
79
+ * @param {string?} zName
80
+ * @param {number} fileId
81
+ * @param {number} flags
82
+ * @param {DataView} pOutFlags
83
+ * @returns {Promise<number>}
84
+ */
85
+ async jOpen(zName, fileId, flags, pOutFlags) {
86
+ try {
87
+ const url = new URL(zName || Math.random().toString(36).slice(2), 'file://');
88
+ const path = url.pathname;
89
+
90
+ let meta = await this.#idb.q(({ metadata }) => metadata.get(path));
91
+ if (!meta && (flags & VFS.SQLITE_OPEN_CREATE)) {
92
+ meta = {
93
+ name: path,
94
+ fileSize: 0,
95
+ version: 0
96
+ };
97
+ await this.#idb.q(({ metadata }) => metadata.put(meta), 'rw');
98
+ }
99
+
100
+ if (!meta) {
101
+ throw new Error(`File ${path} not found`);
102
+ }
103
+
104
+ const file = new File(path, flags, meta);
105
+ this.mapIdToFile.set(fileId, file);
106
+ pOutFlags.setInt32(0, flags, true);
107
+ return VFS.SQLITE_OK;
108
+ } catch (e) {
109
+ this.lastError = e;
110
+ return VFS.SQLITE_CANTOPEN;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @param {string} zName
116
+ * @param {number} syncDir
117
+ * @returns {Promise<number>}
118
+ */
119
+ async jDelete(zName, syncDir) {
120
+ try {
121
+ const url = new URL(zName, 'file://');
122
+ const path = url.pathname;
123
+
124
+ this.#idb.q(({ metadata, blocks }) => {
125
+ const range = IDBKeyRange.bound([path, -Infinity], [path, Infinity]);
126
+ blocks.delete(range);
127
+ metadata.delete(path);
128
+ }, 'rw');
129
+
130
+ if (syncDir) {
131
+ await this.#idb.sync(false);
132
+ }
133
+ return VFS.SQLITE_OK;
134
+ } catch (e) {
135
+ this.lastError = e;
136
+ return VFS.SQLITE_IOERR_DELETE;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {string} zName
142
+ * @param {number} flags
143
+ * @param {DataView} pResOut
144
+ * @returns {Promise<number>}
145
+ */
146
+ async jAccess(zName, flags, pResOut) {
147
+ try {
148
+ const url = new URL(zName, 'file://');
149
+ const path = url.pathname;
150
+
151
+ const meta = await this.#idb.q(({ metadata }) => metadata.get(path));
152
+ pResOut.setInt32(0, meta ? 1 : 0, true);
153
+ return VFS.SQLITE_OK;
154
+ } catch (e) {
155
+ this.lastError = e;
156
+ return VFS.SQLITE_IOERR_ACCESS;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * @param {number} fileId
162
+ * @returns {Promise<number>}
163
+ */
164
+ async jClose(fileId) {
165
+ try {
166
+ const file = this.mapIdToFile.get(fileId);
167
+ this.mapIdToFile.delete(fileId);
168
+
169
+ if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
170
+ await this.#idb.q(({ metadata, blocks }) => {
171
+ metadata.delete(file.path);
172
+ blocks.delete(IDBKeyRange.bound([file.path, 0], [file.path, Infinity]));
173
+ }, 'rw');
174
+ }
175
+
176
+ if (file.needsMetadataSync) {
177
+ this.#idb.q(({ metadata }) => metadata.put(file.metadata), 'rw');
178
+ }
179
+ await this.#idb.sync(file.synchronous === 'full');
180
+ return VFS.SQLITE_OK;
181
+ } catch (e) {
182
+ this.lastError = e;
183
+ return VFS.SQLITE_IOERR_CLOSE;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * @param {number} fileId
189
+ * @param {Uint8Array} pData
190
+ * @param {number} iOffset
191
+ * @returns {Promise<number>}
192
+ */
193
+ async jRead(fileId, pData, iOffset) {
194
+ try {
195
+ const file = this.mapIdToFile.get(fileId);
196
+
197
+ let pDataOffset = 0;
198
+ while (pDataOffset < pData.byteLength) {
199
+ // Fetch the IndexedDB block for this file location.
200
+ const fileOffset = iOffset + pDataOffset;
201
+ const block = await this.#idb.q(({ blocks }) => {
202
+ const range = IDBKeyRange.bound([file.path, -fileOffset], [file.path, Infinity]);
203
+ return blocks.get(range);
204
+ });
205
+
206
+ if (!block || block.data.byteLength - block.offset <= fileOffset) {
207
+ pData.fill(0, pDataOffset);
208
+ return VFS.SQLITE_IOERR_SHORT_READ;
209
+ }
210
+
211
+ // Copy block data.
212
+ const dst = pData.subarray(pDataOffset);
213
+ const srcOffset = fileOffset + block.offset;
214
+ const nBytesToCopy = Math.min(
215
+ Math.max(block.data.byteLength - srcOffset, 0),
216
+ dst.byteLength);
217
+ dst.set(block.data.subarray(srcOffset, srcOffset + nBytesToCopy));
218
+ pDataOffset += nBytesToCopy;
219
+ }
220
+ return VFS.SQLITE_OK;
221
+ } catch (e) {
222
+ this.lastError = e;
223
+ return VFS.SQLITE_IOERR_READ;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * @param {number} fileId
229
+ * @param {Uint8Array} pData
230
+ * @param {number} iOffset
231
+ * @returns {number}
232
+ */
233
+ jWrite(fileId, pData, iOffset) {
234
+ try {
235
+ const file = this.mapIdToFile.get(fileId);
236
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
237
+ if (!file.rollback) {
238
+ // Begin a new write transaction.
239
+ // Add pendingVersion to the metadata in IndexedDB. If we crash
240
+ // during the transaction, this lets subsequent connections
241
+ // know to remove blocks from the failed transaction.
242
+ const pending = Object.assign(
243
+ { pendingVersion: file.metadata.version - 1 },
244
+ file.metadata);
245
+ this.#idb.q(({ metadata }) => metadata.put(pending), 'rw', file.txOptions);
246
+
247
+ file.rollback = Object.assign({}, file.metadata);
248
+ file.metadata.version--;
249
+ }
250
+ }
251
+
252
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
253
+ file.changedPages.add(iOffset);
254
+ }
255
+
256
+ const data = pData.slice();
257
+ const version = file.metadata.version;
258
+ const isOverwrite = iOffset < file.metadata.fileSize;
259
+ if (!isOverwrite ||
260
+ file.flags & VFS.SQLITE_OPEN_MAIN_DB ||
261
+ file.flags & VFS.SQLITE_OPEN_TEMP_DB) {
262
+ const block = {
263
+ path: file.path,
264
+ offset: -iOffset,
265
+ version: version,
266
+ data: pData.slice()
267
+ };
268
+ this.#idb.q(({ blocks }) => {
269
+ blocks.put(block);
270
+ file.changedPages.add(iOffset);
271
+ }, 'rw', file.txOptions);
272
+ } else {
273
+ this.#idb.q(async ({ blocks }) => {
274
+ // Read the existing block.
275
+ const range = IDBKeyRange.bound(
276
+ [file.path, -iOffset],
277
+ [file.path, Infinity]);
278
+ const block = await blocks.get(range);
279
+
280
+ // Modify the block data.
281
+ // @ts-ignore
282
+ block.data.subarray(iOffset + block.offset).set(data);
283
+
284
+ // Write back.
285
+ blocks.put(block);
286
+ }, 'rw', file.txOptions);
287
+
288
+ }
289
+
290
+ if (file.metadata.fileSize < iOffset + pData.length) {
291
+ file.metadata.fileSize = iOffset + pData.length;
292
+ file.needsMetadataSync = true;
293
+ }
294
+ return VFS.SQLITE_OK;
295
+ } catch (e) {
296
+ this.lastError = e;
297
+ return VFS.SQLITE_IOERR_WRITE;
298
+ }
299
+ }
300
+
301
+ /**
302
+ * @param {number} fileId
303
+ * @param {number} iSize
304
+ * @returns {number}
305
+ */
306
+ jTruncate(fileId, iSize) {
307
+ try {
308
+ const file = this.mapIdToFile.get(fileId);
309
+ if (iSize < file.metadata.fileSize) {
310
+ this.#idb.q(({ blocks }) => {
311
+ const range = IDBKeyRange.bound(
312
+ [file.path, -Infinity],
313
+ [file.path, -iSize, Infinity]);
314
+ blocks.delete(range);
315
+ }, 'rw', file.txOptions);
316
+ file.metadata.fileSize = iSize;
317
+ file.needsMetadataSync = true;
318
+ }
319
+ return VFS.SQLITE_OK;
320
+ } catch (e) {
321
+ this.lastError = e;
322
+ return VFS.SQLITE_IOERR_TRUNCATE;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * @param {number} fileId
328
+ * @param {number} flags
329
+ * @returns {Promise<number>}
330
+ */
331
+ async jSync(fileId, flags) {
332
+ try {
333
+ const file = this.mapIdToFile.get(fileId);
334
+ if (file.needsMetadataSync) {
335
+ this.#idb.q(({ metadata }) => metadata.put(file.metadata), 'rw', file.txOptions);
336
+ file.needsMetadataSync = false;
337
+ }
338
+
339
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
340
+ // Sync is only needed here for durability. Visibility for other
341
+ // connections is ensured in jUnlock().
342
+ if (file.synchronous === 'full') {
343
+ await this.#idb.sync(true);
344
+ }
345
+ } else {
346
+ await this.#idb.sync(file.synchronous === 'full');
347
+ }
348
+ return VFS.SQLITE_OK;
349
+ } catch (e) {
350
+ this.lastError = e;
351
+ return VFS.SQLITE_IOERR_FSYNC;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * @param {number} fileId
357
+ * @param {DataView} pSize64
358
+ * @returns {number}
359
+ */
360
+ jFileSize(fileId, pSize64) {
361
+ try {
362
+ const file = this.mapIdToFile.get(fileId);
363
+ pSize64.setBigInt64(0, BigInt(file.metadata.fileSize), true);
364
+ return VFS.SQLITE_OK;
365
+ } catch (e) {
366
+ this.lastError = e;
367
+ return VFS.SQLITE_IOERR_FSTAT;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * @param {number} fileId
373
+ * @param {number} lockType
374
+ * @returns {Promise<number>}
375
+ */
376
+ async jLock(fileId, lockType) {
377
+ // Call the actual lock implementation.
378
+ const file = this.mapIdToFile.get(fileId);
379
+ const result = await super.jLock(fileId, lockType);
380
+
381
+ if (lockType === VFS.SQLITE_LOCK_SHARED) {
382
+ // Update metadata.
383
+ file.metadata = await this.#idb.q(async ({ metadata, blocks }) => {
384
+ // @ts-ignore
385
+ /** @type {Metadata} */ const m = await metadata.get(file.path);
386
+ if (m.pendingVersion) {
387
+ console.warn(`removing failed transaction ${m.pendingVersion}`);
388
+ await new Promise((resolve, reject) => {
389
+ const range = IDBKeyRange.bound([m.name, -Infinity], [m.name, Infinity]);
390
+ const request = blocks.openCursor(range);
391
+ request.onsuccess = () => {
392
+ const cursor = request.result;
393
+ if (cursor) {
394
+ const block = cursor.value;
395
+ if (block.version < m.version) {
396
+ cursor.delete();
397
+ }
398
+ cursor.continue();
399
+ } else {
400
+ resolve();
401
+ }
402
+ };
403
+ request.onerror = () => reject(request.error);
404
+ })
405
+
406
+ delete m.pendingVersion;
407
+ metadata.put(m);
408
+ }
409
+ return m;
410
+ }, 'rw', file.txOptions);
411
+ }
412
+ return result;
413
+ }
414
+
415
+ /**
416
+ * @param {number} fileId
417
+ * @param {number} lockType
418
+ * @returns {Promise<number>}
419
+ */
420
+ async jUnlock(fileId, lockType) {
421
+ if (lockType === VFS.SQLITE_LOCK_NONE) {
422
+ const file = this.mapIdToFile.get(fileId);
423
+ await this.#idb.sync(file.synchronous === 'full');
424
+ }
425
+
426
+ // Call the actual unlock implementation.
427
+ return super.jUnlock(fileId, lockType);
428
+ }
429
+
430
+ /**
431
+ * @param {number} fileId
432
+ * @param {number} op
433
+ * @param {DataView} pArg
434
+ * @returns {number|Promise<number>}
435
+ */
436
+ jFileControl(fileId, op, pArg) {
437
+ try {
438
+ const file = this.mapIdToFile.get(fileId);
439
+ switch (op) {
440
+ case VFS.SQLITE_FCNTL_PRAGMA:
441
+ const key = extractString(pArg, 4);
442
+ const value = extractString(pArg, 8);
443
+ this.log?.('xFileControl', file.path, 'PRAGMA', key, value);
444
+ const setPragmaResponse = response => {
445
+ const encoded = new TextEncoder().encode(response);
446
+ const out = this._module._sqlite3_malloc(encoded.byteLength);
447
+ const outArray = this._module.HEAPU8.subarray(out, out + encoded.byteLength);
448
+ outArray.set(encoded);
449
+ pArg.setUint32(0, out, true);
450
+ return VFS.SQLITE_ERROR;
451
+ };
452
+ switch (key.toLowerCase()) {
453
+ case 'page_size':
454
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
455
+ // Don't allow changing the page size.
456
+ if (value && file.metadata.fileSize) {
457
+ return VFS.SQLITE_ERROR;
458
+ }
459
+ }
460
+ break;
461
+ case 'synchronous':
462
+ if (value) {
463
+ switch (value.toLowerCase()) {
464
+ case '0':
465
+ case 'off':
466
+ file.synchronous = 'off';
467
+ file.txOptions = { durability: 'relaxed' };
468
+ break;
469
+ case '1':
470
+ case 'normal':
471
+ file.synchronous = 'normal';
472
+ file.txOptions = { durability: 'relaxed' };
473
+ break;
474
+ case '2':
475
+ case '3':
476
+ case 'full':
477
+ case 'extra':
478
+ file.synchronous = 'full';
479
+ file.txOptions = { durability: 'strict' };
480
+ break;
481
+ }
482
+ }
483
+ break;
484
+ case 'write_hint':
485
+ return super.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null);
486
+ }
487
+ break;
488
+ case VFS.SQLITE_FCNTL_SYNC:
489
+ this.log?.('xFileControl', file.path, 'SYNC');
490
+ if (file.rollback) {
491
+ const commitMetadata = Object.assign({}, file.metadata);
492
+ const prevFileSize = file.rollback.fileSize
493
+ this.#idb.q(({ metadata, blocks }) => {
494
+ metadata.put(commitMetadata);
495
+
496
+ // Remove old page versions.
497
+ for (const offset of file.changedPages) {
498
+ if (offset < prevFileSize) {
499
+ const range = IDBKeyRange.bound(
500
+ [file.path, -offset, commitMetadata.version],
501
+ [file.path, -offset, Infinity],
502
+ true);
503
+ blocks.delete(range);
504
+ }
505
+ }
506
+ file.changedPages.clear();
507
+ }, 'rw', file.txOptions);
508
+ file.needsMetadataSync = false;
509
+ file.rollback = null;
510
+ }
511
+ break;
512
+ case VFS.SQLITE_FCNTL_BEGIN_ATOMIC_WRITE:
513
+ // Every write transaction is atomic, so this is a no-op.
514
+ this.log?.('xFileControl', file.path, 'BEGIN_ATOMIC_WRITE');
515
+ return VFS.SQLITE_OK;
516
+ case VFS.SQLITE_FCNTL_COMMIT_ATOMIC_WRITE:
517
+ // Every write transaction is atomic, so this is a no-op.
518
+ this.log?.('xFileControl', file.path, 'COMMIT_ATOMIC_WRITE');
519
+ return VFS.SQLITE_OK;
520
+ case VFS.SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE:
521
+ this.log?.('xFileControl', file.path, 'ROLLBACK_ATOMIC_WRITE');
522
+ file.metadata = file.rollback;
523
+ const rollbackMetadata = Object.assign({}, file.metadata);
524
+ this.#idb.q(({ metadata, blocks }) => {
525
+ metadata.put(rollbackMetadata);
526
+
527
+ // Remove pages.
528
+ for (const offset of file.changedPages) {
529
+ blocks.delete([file.path, -offset, rollbackMetadata.version - 1]);
530
+ }
531
+ file.changedPages.clear();
532
+ }, 'rw', file.txOptions);
533
+ file.needsMetadataSync = false;
534
+ file.rollback = null;
535
+ return VFS.SQLITE_OK;
536
+ }
537
+ } catch (e) {
538
+ this.lastError = e;
539
+ return VFS.SQLITE_IOERR;
540
+ }
541
+ return super.jFileControl(fileId, op, pArg);
542
+ }
543
+
544
+ /**
545
+ * @param {number} pFile
546
+ * @returns {number|Promise<number>}
547
+ */
548
+ jDeviceCharacteristics(pFile) {
549
+ return 0
550
+ | VFS.SQLITE_IOCAP_BATCH_ATOMIC
551
+ | VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
552
+ }
553
+
554
+ /**
555
+ * @param {Uint8Array} zBuf
556
+ * @returns {number|Promise<number>}
557
+ */
558
+ jGetLastError(zBuf) {
559
+ if (this.lastError) {
560
+ console.error(this.lastError);
561
+ const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
562
+ const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray);
563
+ zBuf[written] = 0;
564
+ }
565
+ return VFS.SQLITE_OK
566
+ }
567
+ }
568
+
569
+ function extractString(dataView, offset) {
570
+ const p = dataView.getUint32(offset, true);
571
+ if (p) {
572
+ const chars = new Uint8Array(dataView.buffer, p);
573
+ return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)));
574
+ }
575
+ return null;
576
+ }
577
+
578
+ export class IDBContext {
579
+ /** @type {IDBDatabase} */ #database;
580
+
581
+ /** @type {Promise} */ #chain = null;
582
+ /** @type {Promise<any>} */ #txComplete = Promise.resolve();
583
+ /** @type {IDBRequest?} */ #request = null;
584
+ /** @type {WeakSet<IDBTransaction>} */ #txPending = new WeakSet();
585
+
586
+ log = null;
587
+
588
+ static async create(name) {
589
+ const database = await new Promise((resolve, reject) => {
590
+ const request = indexedDB.open(name, 6);
591
+ request.onupgradeneeded = async event => {
592
+ const db = request.result;
593
+ if (event.oldVersion) {
594
+ console.log(`Upgrading IndexedDB from version ${event.oldVersion}`);
595
+ }
596
+ switch (event.oldVersion) {
597
+ case 0:
598
+ // Start with the original schema.
599
+ db.createObjectStore('blocks', { keyPath: ['path', 'offset', 'version']})
600
+ .createIndex('version', ['path', 'version']);
601
+ // fall through intentionally
602
+ case 5:
603
+ const tx = request.transaction;
604
+ const blocks = tx.objectStore('blocks');
605
+ blocks.deleteIndex('version');
606
+ const metadata = db.createObjectStore('metadata', { keyPath: 'name' });
607
+
608
+ await new Promise((resolve, reject) => {
609
+ // Iterate over all the blocks.
610
+ let lastBlock = {};
611
+ const request = tx.objectStore('blocks').openCursor();
612
+ request.onsuccess = () => {
613
+ const cursor = request.result;
614
+ if (cursor) {
615
+ const block = cursor.value;
616
+ if (typeof block.offset !== 'number' ||
617
+ (block.path === lastBlock.path && block.offset === lastBlock.offset)) {
618
+ // Remove superceded block (or the "purge" info).
619
+ cursor.delete();
620
+ } else if (block.offset === 0) {
621
+ // Move metadata to its own store.
622
+ metadata.put({
623
+ name: block.path,
624
+ fileSize: block.fileSize,
625
+ version: block.version
626
+ });
627
+
628
+ delete block.fileSize;
629
+ cursor.update(block);
630
+ }
631
+ lastBlock = block;
632
+ cursor.continue();
633
+ } else {
634
+ resolve();
635
+ }
636
+ };
637
+ request.onerror = () => reject(request.error);
638
+ });
639
+ break;
640
+ }
641
+ };
642
+ request.onsuccess = () => resolve(request.result);
643
+ request.onerror = () => reject(request.error);
644
+ });
645
+ return new IDBContext(database);
646
+ }
647
+
648
+ constructor(database) {
649
+ this.#database = database;
650
+ }
651
+
652
+ close() {
653
+ this.#database.close();
654
+ }
655
+
656
+ /**
657
+ * @param {(stores: Object.<string, IDBObjectStore>) => any} f
658
+ * @param {'ro'|'rw'} mode
659
+ * @returns {Promise<any>}
660
+ */
661
+ q(f, mode = 'ro', options = {}) {
662
+ /** @type {IDBTransactionMode} */
663
+ const txMode = mode === 'ro' ? 'readonly' : 'readwrite';
664
+ const txOptions = Object.assign({
665
+ /** @type {IDBTransactionDurability} */ durability: 'default'
666
+ }, options);
667
+
668
+ // Ensure that queries run sequentially. If any function rejects,
669
+ // or any request has an error, or the transaction does not commit,
670
+ // then no subsequent functions will run until sync() or reset().
671
+ this.#chain = (this.#chain || Promise.resolve())
672
+ .then(() => this.#q(f, txMode, txOptions));
673
+ return this.#chain;
674
+ }
675
+
676
+ /**
677
+ * @param {(stores: Object.<string, IDBObjectStore>) => any} f
678
+ * @param {IDBTransactionMode} mode
679
+ * @param {IDBTransactionOptions} options
680
+ * @returns {Promise<any>}
681
+ */
682
+ async #q(f, mode, options) {
683
+ /** @type {IDBTransaction} */ let tx;
684
+ if (this.#request &&
685
+ this.#txPending.has(this.#request.transaction) &&
686
+ this.#request.transaction.mode >= mode &&
687
+ this.#request.transaction.durability === options.durability) {
688
+ // The previous request transaction is compatible and has
689
+ // not yet completed.
690
+ tx = this.#request.transaction;
691
+
692
+ // If the previous request is pending, wait for it to complete.
693
+ // This ensures that the transaction will be active.
694
+ if (this.#request.readyState === 'pending') {
695
+ await new Promise(resolve => {
696
+ this.#request.addEventListener('success', resolve, { once: true });
697
+ this.#request.addEventListener('error', resolve, { once: true });
698
+ });
699
+ }
700
+ }
701
+
702
+ for (let i = 0; i < 2; ++i) {
703
+ if (!tx) {
704
+ // The current transaction is missing or doesn't match so
705
+ // replace it with a new one. wait for the previous
706
+ // transaction to complete so the lifetimes do not overlap.
707
+ await this.#txComplete;
708
+
709
+ // Create the new transaction.
710
+ // @ts-ignore
711
+ tx = this.#database.transaction(this.#database.objectStoreNames, mode, options);
712
+ this.log?.('IDBTransaction open', mode);
713
+ this.#txPending.add(tx);
714
+ this.#txComplete = new Promise((resolve, reject) => {
715
+ tx.addEventListener('complete', () => {
716
+ this.log?.('IDBTransaction complete');
717
+ this.#txPending.delete(tx);
718
+ resolve();
719
+ });
720
+ tx.addEventListener('abort', () => {
721
+ this.#txPending.delete(tx);
722
+ reject(new Error('transaction aborted'));
723
+ });
724
+ });
725
+ }
726
+
727
+ try {
728
+ // @ts-ignore
729
+ // Create object store proxies.
730
+ const objectStores = [...tx.objectStoreNames].map(name => {
731
+ return [name, this.proxyStoreOrIndex(tx.objectStore(name))];
732
+ });
733
+
734
+ // Execute the function.
735
+ return await f(Object.fromEntries(objectStores));
736
+ } catch (e) {
737
+ // Use a new transaction if this one was inactive. This will
738
+ // happen if the last request in the transaction completed
739
+ // in a previous task but the transaction has not yet committed.
740
+ if (!i && RETRYABLE_ERRORS.has(e.name)) {
741
+ this.log?.(`${e.name}, retrying`);
742
+ tx = null;
743
+ continue;
744
+ }
745
+ throw e;
746
+ }
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Object store methods that return an IDBRequest, except for cursor
752
+ * creation, are wrapped to return a Promise. In addition, the
753
+ * request is used internally for chaining.
754
+ * @param {IDBObjectStore} objectStore
755
+ * @returns
756
+ */
757
+ proxyStoreOrIndex(objectStore) {
758
+ return new Proxy(objectStore, {
759
+ get: (target, property, receiver) => {
760
+ const result = Reflect.get(target, property, receiver);
761
+ if (typeof result === 'function') {
762
+ return (...args) => {
763
+ const maybeRequest = Reflect.apply(result, target, args);
764
+ // @ts-ignore
765
+ if (maybeRequest instanceof IDBRequest && !property.endsWith('Cursor')) {
766
+ // // Debug logging.
767
+ // this.log?.(`${target.name}.${String(property)}`, args);
768
+ // maybeRequest.addEventListener('success', () => {
769
+ // this.log?.(`${target.name}.${String(property)} success`, maybeRequest.result);
770
+ // });
771
+ // maybeRequest.addEventListener('error', () => {
772
+ // this.log?.(`${target.name}.${String(property)} error`, maybeRequest.error);
773
+ // });
774
+
775
+ // Save the request.
776
+ this.#request = maybeRequest;
777
+
778
+ // Abort the transaction on error.
779
+ maybeRequest.addEventListener('error', () => {
780
+ console.error(maybeRequest.error);
781
+ maybeRequest.transaction.abort();
782
+ }, { once: true });
783
+
784
+ // Return a Promise.
785
+ return wrap(maybeRequest);
786
+ }
787
+ return maybeRequest;
788
+ }
789
+ }
790
+ return result;
791
+ }
792
+ });
793
+ }
794
+
795
+ /**
796
+ * @param {boolean} durable
797
+ */
798
+ async sync(durable) {
799
+ if (this.#chain) {
800
+ // This waits for all IndexedDB calls to be made.
801
+ await this.#chain;
802
+ if (durable) {
803
+ // This waits for the final transaction to commit.
804
+ await this.#txComplete;
805
+ }
806
+ this.reset();
807
+ }
808
+ }
809
+
810
+ reset() {
811
+ this.#chain = null;
812
+ this.#txComplete = Promise.resolve();
813
+ this.#request = null;
814
+ }
815
+ }
816
+
817
+ /**
818
+ * @param {IDBRequest} request
819
+ * @returns {Promise}
820
+ */
821
+ function wrap(request) {
822
+ return new Promise((resolve, reject) => {
823
+ request.onsuccess = () => resolve(request.result);
824
+ request.onerror = () => reject(request.error);
825
+ });
826
+ }
827
+