@ehegnes/wa-sqlite 2.0.0-beta.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/dist/wa-sqlite-async.mjs +2 -0
  4. package/dist/wa-sqlite-async.wasm +0 -0
  5. package/dist/wa-sqlite-jspi.mjs +2 -0
  6. package/dist/wa-sqlite-jspi.wasm +0 -0
  7. package/dist/wa-sqlite.mjs +2 -0
  8. package/dist/wa-sqlite.wasm +0 -0
  9. package/package.json +44 -0
  10. package/src/FacadeVFS.js +681 -0
  11. package/src/VFS.js +222 -0
  12. package/src/WebLocksMixin.js +411 -0
  13. package/src/examples/AccessHandlePoolVFS.js +458 -0
  14. package/src/examples/IDBBatchAtomicVFS.js +827 -0
  15. package/src/examples/IDBMirrorVFS.js +889 -0
  16. package/src/examples/LazyLock.js +90 -0
  17. package/src/examples/Lock.js +69 -0
  18. package/src/examples/MemoryAsyncVFS.js +100 -0
  19. package/src/examples/MemoryVFS.js +176 -0
  20. package/src/examples/OPFSAdaptiveVFS.js +437 -0
  21. package/src/examples/OPFSAnyContextVFS.js +300 -0
  22. package/src/examples/OPFSCoopSyncVFS.js +597 -0
  23. package/src/examples/OPFSPermutedVFS.js +1217 -0
  24. package/src/examples/OPFSWriteAheadVFS.js +960 -0
  25. package/src/examples/README.md +81 -0
  26. package/src/examples/WriteAhead.js +1174 -0
  27. package/src/examples/tag.js +82 -0
  28. package/src/sqlite-api.js +924 -0
  29. package/src/sqlite-constants.js +275 -0
  30. package/src/types/globals.d.ts +60 -0
  31. package/src/types/index.d.ts +1228 -0
  32. package/src/types/tsconfig.json +6 -0
  33. package/test/AccessHandlePoolVFS.test.js +27 -0
  34. package/test/IDBBatchAtomicVFS.test.js +97 -0
  35. package/test/IDBMirrorVFS.test.js +27 -0
  36. package/test/MemoryAsyncVFS.test.js +27 -0
  37. package/test/MemoryVFS.test.js +27 -0
  38. package/test/OPFSAdaptiveVFS.test.js +27 -0
  39. package/test/OPFSAnyContextVFS.test.js +27 -0
  40. package/test/OPFSCoopSyncVFS.test.js +27 -0
  41. package/test/OPFSWriteAheadVFS.test.js +27 -0
  42. package/test/TestContext.js +96 -0
  43. package/test/WebLocksMixin.test.js +521 -0
  44. package/test/api.test.js +49 -0
  45. package/test/api_exec.js +89 -0
  46. package/test/api_misc.js +63 -0
  47. package/test/api_statements.js +447 -0
  48. package/test/callbacks.test.js +581 -0
  49. package/test/data/idbv5.json +1 -0
  50. package/test/sql.test.js +64 -0
  51. package/test/sql_0001.js +49 -0
  52. package/test/sql_0002.js +52 -0
  53. package/test/sql_0003.js +83 -0
  54. package/test/sql_0004.js +81 -0
  55. package/test/sql_0005.js +76 -0
  56. package/test/test-worker.js +204 -0
  57. package/test/vfs_xAccess.js +2 -0
  58. package/test/vfs_xClose.js +52 -0
  59. package/test/vfs_xOpen.js +91 -0
  60. package/test/vfs_xRead.js +38 -0
  61. package/test/vfs_xWrite.js +36 -0
@@ -0,0 +1,960 @@
1
+ import { FacadeVFS } from "../FacadeVFS.js";
2
+ import * as VFS from '../VFS.js';
3
+ import { LazyLock } from "./LazyLock.js";
4
+ import { WriteAhead } from "./WriteAhead.js";
5
+
6
+ const LIBRARY_FILES_ROOT = '.wa-sqlite';
7
+ const DEFAULT_TEMP_FILES = 6;
8
+
9
+ const finalizationRegistry = new FinalizationRegistry((/** @type {() => void} */ f) => f());
10
+
11
+ /**
12
+ * @typedef FileEntry
13
+ * @property {string} zName
14
+ * @property {number} flags
15
+ * @property {FileSystemSyncAccessHandle} [accessHandle]
16
+
17
+ * Main database file properties:
18
+ * @property {*} [retryResult]
19
+ * @property {FileSystemSyncAccessHandle[]} [waHandles]
20
+ *
21
+ * @property {'reserved'|'exclusive'|null} [writeHint]
22
+ * @property {'normal'|'exclusive'} [lockingMode]
23
+ * @property {number} [lockState] SQLITE_LOCK_*
24
+ * @property {LazyLock} [readLock]
25
+ * @property {LazyLock} [writeLock]
26
+ * @property {'none'|'read'|'write'|'readwrite'} [useLazyLock]
27
+ * @property {number} [timeout]
28
+ * @property {0|1|2|3} [synchronous]
29
+ * @property {number?} [pageSize]
30
+ * @property {boolean} [overwrite]
31
+ *
32
+ * @property {WriteAhead} [writeAhead]
33
+ */
34
+
35
+ /**
36
+ * @typedef OPFSWriteAheadOptions
37
+ * @property {number} [nTmpFiles]
38
+ * @property {number} [autoCheckpoint]
39
+ * @property {number} [backstopInterval]
40
+ */
41
+
42
+ export class OPFSWriteAheadVFS extends FacadeVFS {
43
+ lastError = null;
44
+ log = null;
45
+
46
+ /** @type {Map<number, FileEntry>} */ mapIdToFile = new Map();
47
+ /** @type {Map<string, FileEntry>} */ mapPathToFile = new Map();
48
+
49
+ /** @type {Map<string, FileSystemSyncAccessHandle>} */ boundTempFiles = new Map();
50
+ /** @type {Set<FileSystemSyncAccessHandle>} */ unboundTempFiles = new Set();
51
+ /** @type {OPFSWriteAheadOptions} */ options = {
52
+ nTmpFiles: DEFAULT_TEMP_FILES
53
+ };
54
+
55
+ _ready;
56
+
57
+ static async create(name, module, options) {
58
+ const vfs = new OPFSWriteAheadVFS(name, module);
59
+ Object.assign(vfs.options, options);
60
+ await vfs.isReady();
61
+ return vfs;
62
+ }
63
+
64
+ constructor(name, module) {
65
+ super(name, module);
66
+ this._ready = (async () => {
67
+ // Ensure the library files root directory exists.
68
+ let dirHandle = await navigator.storage.getDirectory();
69
+ dirHandle = await dirHandle.getDirectoryHandle(LIBRARY_FILES_ROOT, { create: true });
70
+
71
+ // Clean up any stale session directories.
72
+ // @ts-ignore
73
+ for await (const name of dirHandle.keys()) {
74
+ if (name.startsWith('.session-')) {
75
+ // Acquire a lock on the session directory to ensure it is not in use.
76
+ await navigator.locks.request(name, { ifAvailable: true }, async lock => {
77
+ if (lock) {
78
+ // This directory is not in use.
79
+ try {
80
+ await dirHandle.removeEntry(name, { recursive: true });
81
+ } catch (e) {
82
+ // Ignore errors, will try again next time.
83
+ }
84
+ }
85
+ });
86
+ }
87
+ }
88
+
89
+ // Create our session directory.
90
+ const dirName = `.session-${Math.random().toString(16).slice(2)}`;
91
+ await new Promise(resolve => {
92
+ navigator.locks.request(dirName, () => {
93
+ // @ts-ignore
94
+ resolve();
95
+ return new Promise(release => {
96
+ // @ts-ignore
97
+ finalizationRegistry.register(this, release);
98
+ });
99
+ });
100
+ });
101
+ dirHandle = await dirHandle.getDirectoryHandle(dirName, { create: true });
102
+
103
+ // Create temporary files.
104
+ for (let i = 0; i < this.options.nTmpFiles; i++) {
105
+ const fileHandle= await dirHandle.getFileHandle(i.toString(), { create: true });
106
+ const accessHandle = await fileHandle.createSyncAccessHandle();
107
+ finalizationRegistry.register(this, () => accessHandle.close());
108
+ this.unboundTempFiles.add(accessHandle);
109
+ }
110
+ })();
111
+ }
112
+
113
+ isReady() {
114
+ return Promise.all([super.isReady(), this._ready]).then(() => true);
115
+ }
116
+
117
+ /**
118
+ * @param {string?} zName
119
+ * @param {number} fileId
120
+ * @param {number} flags
121
+ * @param {DataView} pOutFlags
122
+ * @returns {number}
123
+ */
124
+ jOpen(zName, fileId, flags, pOutFlags) {
125
+ try {
126
+ if (zName === null) {
127
+ // Generate a temporary filename. This will only be used as a
128
+ // key to map to a pre-opened temporary file access handle.
129
+ zName = Math.random().toString(16).slice(2);
130
+ }
131
+
132
+ const file = this.mapPathToFile.get(zName) ?? {
133
+ zName,
134
+ flags,
135
+ retryResult: null,
136
+ };
137
+ this.mapPathToFile.set(zName, file);
138
+
139
+ if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
140
+ // Open database and journal files with a retry operation.
141
+ if (file.retryResult === null) {
142
+ // This is the initial open attempt. Start the asynchronous task
143
+ // and return SQLITE_BUSY to force a retry.
144
+ this._module.retryOps.push(this.#retryOpen(zName, flags, fileId, pOutFlags));
145
+ return VFS.SQLITE_BUSY;
146
+ } else if (file.retryResult instanceof Error) {
147
+ const e = file.retryResult;
148
+ file.retryResult = null;
149
+ throw e;
150
+ }
151
+
152
+ // Initialize database file state.
153
+ file.accessHandle = file.retryResult.accessHandle;
154
+ file.waHandles = file.retryResult.waHandles;
155
+ file.writeAhead = file.retryResult.writeAhead;
156
+ file.retryResult = null;
157
+
158
+ file.lockState = VFS.SQLITE_LOCK_NONE;
159
+ file.lockingMode = 'normal';
160
+ file.readLock = new LazyLock(`${zName}#read`);
161
+ file.writeLock = new LazyLock(`${zName}#write`);
162
+ file.useLazyLock = 'readwrite';
163
+ file.timeout = -1;
164
+ file.synchronous = 1; // NORMAL
165
+ file.writeHint = null;
166
+ file.pageSize = null;
167
+ file.overwrite = false;
168
+ } else if (flags & (VFS.SQLITE_OPEN_WAL | VFS.SQLITE_OPEN_SUPER_JOURNAL)) {
169
+ throw new Error('WAL and super-journal files are not supported');
170
+ } else if (file.accessHandle) {
171
+ // This temporary file already has an access handle, which happens
172
+ // only for tests. Just use it as is.
173
+ } else {
174
+ // This is a temporary file. Use an unbound pre-opened accessHandle.
175
+ if (!(flags & VFS.SQLITE_OPEN_CREATE)) throw new Error('file not found');
176
+ file.accessHandle = this.#openTemporaryFile(zName);
177
+ }
178
+
179
+ this.mapIdToFile.set(fileId, file);
180
+ pOutFlags.setInt32(0, flags, true);
181
+ return VFS.SQLITE_OK;
182
+ } catch (e) {
183
+ console.error(e.stack);
184
+ this.lastError = e;
185
+ this.mapPathToFile.delete(zName);
186
+ return VFS.SQLITE_CANTOPEN;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * @param {string} zName
192
+ * @param {number} syncDir
193
+ * @returns {number}
194
+ */
195
+ jDelete(zName, syncDir) {
196
+ try {
197
+ if (this.boundTempFiles.has(zName)) {
198
+ const file = this.mapPathToFile.get(zName);
199
+ this.#deleteTemporaryFile(file);
200
+ } else {
201
+ throw new Error(`unexpected file deletion: ${zName}`);
202
+ }
203
+ return VFS.SQLITE_OK;
204
+ } catch (e) {
205
+ console.error(e.stack);
206
+ this.lastError = e;
207
+ return VFS.SQLITE_IOERR_DELETE;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * @param {string} zName
213
+ * @param {number} flags
214
+ * @param {DataView} pResOut
215
+ * @returns {number}
216
+ */
217
+ jAccess(zName, flags, pResOut) {
218
+ try {
219
+ const file = this.mapPathToFile.get(zName);
220
+ pResOut.setInt32(0, file ? 1 : 0, true);
221
+ return VFS.SQLITE_OK;
222
+ } catch (e) {
223
+ console.error(e.stack);
224
+ this.lastError = e;
225
+ return VFS.SQLITE_IOERR_ACCESS;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * @param {number} fileId
231
+ * @returns {number}
232
+ */
233
+ jClose(fileId) {
234
+ try {
235
+ const file = this.mapIdToFile.get(fileId);
236
+ if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) {
237
+ file.writeAhead.close();
238
+ file.accessHandle.close();
239
+ file.waHandles.forEach(handle => handle.close());
240
+ this.mapPathToFile.delete(file?.zName);
241
+
242
+ file.readLock.close();
243
+ file.writeLock.close();
244
+ } else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
245
+ this.#deleteTemporaryFile(file);
246
+ }
247
+
248
+ // Disassociate fileId from file entry.
249
+ this.mapIdToFile.delete(fileId);
250
+ return VFS.SQLITE_OK;
251
+ } catch (e) {
252
+ console.error(e.stack);
253
+ this.lastError = e;
254
+ return VFS.SQLITE_IOERR_CLOSE;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * @param {number} fileId
260
+ * @param {Uint8Array} pData
261
+ * @param {number} iOffset
262
+ * @returns {number}
263
+ */
264
+ jRead(fileId, pData, iOffset) {
265
+ try {
266
+ const file = this.mapIdToFile.get(fileId);
267
+
268
+ let bytesRead = null;
269
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
270
+ // Try reading from the write-ahead overlays first. A read on the
271
+ // database file is always a complete page, except when reading
272
+ // from the 100-byte header.
273
+ const pageOffset = iOffset < 100 ? iOffset : 0;
274
+ const page = file.writeAhead.read(iOffset - pageOffset);
275
+ if (page) {
276
+ const readData = page.subarray(pageOffset, pageOffset + pData.byteLength);
277
+ pData.set(readData);
278
+ bytesRead = readData.byteLength;
279
+ }
280
+ }
281
+
282
+ if (bytesRead === null) {
283
+ // Read directly from the OPFS file.
284
+
285
+ // On Chrome (at least), passing pData to accessHandle.read() is
286
+ // an error because pData is a Proxy of a Uint8Array. Calling
287
+ // subarray() produces a real Uint8Array and that works.
288
+ bytesRead = file.accessHandle.read(pData.subarray(), { at: iOffset });
289
+ }
290
+
291
+ if (bytesRead < pData.byteLength) {
292
+ pData.fill(0, bytesRead);
293
+ return VFS.SQLITE_IOERR_SHORT_READ;
294
+ }
295
+ return VFS.SQLITE_OK;
296
+ } catch (e) {
297
+ console.error(e.stack);
298
+ this.lastError = e;
299
+ return VFS.SQLITE_IOERR_READ;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * @param {number} fileId
305
+ * @param {Uint8Array} pData
306
+ * @param {number} iOffset
307
+ * @returns {number}
308
+ */
309
+ jWrite(fileId, pData, iOffset) {
310
+ try {
311
+ const file = this.mapIdToFile.get(fileId);
312
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
313
+ // Write to the write-ahead overlay.
314
+ const isPageResize = file.overwrite && file.pageSize !== pData.byteLength;
315
+ file.writeAhead.write(iOffset, pData, {
316
+ dstPageSize: isPageResize ? file.pageSize : null
317
+ });
318
+ return VFS.SQLITE_OK;
319
+ }
320
+
321
+ // On Chrome (at least), passing pData to accessHandle.write() is
322
+ // an error because pData is a Proxy of a Uint8Array. Calling
323
+ // subarray() produces a real Uint8Array and that works.
324
+ file.accessHandle.write(pData.subarray(), { at: iOffset });
325
+ return VFS.SQLITE_OK;
326
+ } catch (e) {
327
+ console.error(e.stack);
328
+ this.lastError = e;
329
+ return VFS.SQLITE_IOERR_WRITE;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * @param {number} fileId
335
+ * @param {number} iSize
336
+ * @returns {number}
337
+ */
338
+ jTruncate(fileId, iSize) {
339
+ try {
340
+ const file = this.mapIdToFile.get(fileId);
341
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
342
+ file.writeAhead.truncate(iSize);
343
+ return VFS.SQLITE_OK;
344
+ }
345
+ file.accessHandle.truncate(iSize);
346
+ return VFS.SQLITE_OK;
347
+ } catch (e) {
348
+ console.error(e.stack);
349
+ this.lastError = e;
350
+ return VFS.SQLITE_IOERR_TRUNCATE;
351
+ }
352
+ }
353
+
354
+ /**
355
+ * @param {number} fileId
356
+ * @param {number} flags
357
+ * @returns {number}
358
+ */
359
+ jSync(fileId, flags) {
360
+ try {
361
+ const file = this.mapIdToFile.get(fileId);
362
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
363
+ const durability = file.synchronous > 1 ? 'strict' : 'relaxed';
364
+ file.writeAhead.sync({ durability });
365
+ } else {
366
+ // This is a temporary file so sync is not needed.
367
+ // Temporary journals are only used for rollback by the
368
+ // connection that created them, not for recovery.
369
+ }
370
+ return VFS.SQLITE_OK;
371
+ } catch (e) {
372
+ console.error(e.stack);
373
+ this.lastError = e;
374
+ return VFS.SQLITE_IOERR_FSYNC;
375
+ }
376
+ }
377
+
378
+ /**
379
+ * @param {number} fileId
380
+ * @param {DataView} pSize64
381
+ * @returns {number}
382
+ */
383
+ jFileSize(fileId, pSize64) {
384
+ try {
385
+ const file = this.mapIdToFile.get(fileId);
386
+
387
+ let size;
388
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
389
+ size = file.writeAhead.getFileSize() || file.accessHandle.getSize();
390
+ } else {
391
+ size = file.accessHandle.getSize();
392
+ }
393
+ pSize64.setBigInt64(0, BigInt(size), true);
394
+ return VFS.SQLITE_OK;
395
+ } catch (e) {
396
+ console.error(e.stack);
397
+ this.lastError = e;
398
+ return VFS.SQLITE_IOERR_FSTAT;
399
+ }
400
+ }
401
+
402
+ /**
403
+ * @param {number} pFile
404
+ * @param {number} lockType
405
+ * @returns {number|Promise<number>}
406
+ */
407
+ jLock(pFile, lockType) {
408
+ try {
409
+ const file = this.mapIdToFile.get(pFile);
410
+ if (file.lockState === VFS.SQLITE_LOCK_NONE && lockType === VFS.SQLITE_LOCK_SHARED) {
411
+ // We do all our locking work in this transition.
412
+ if (file.retryResult === null) {
413
+ if (file.lockingMode === 'exclusive') {
414
+ // Exclusive locking mode is treated as a write, and the
415
+ // read lock is also acquired to block readers.
416
+ file.retryResult = {};
417
+ this._module.retryOps.push(this.#retryLockWrite(file));
418
+ return VFS.SQLITE_BUSY;
419
+ }
420
+
421
+ // With WAL, read and write transactions use separate locks. In
422
+ // each case if the required lock is already held then we can
423
+ // proceed synchronously. Otherwise we need to acquire state
424
+ // asynchronously and retry.
425
+ if (file.writeHint) {
426
+ // Write transaction.
427
+ if (!file.writeLock.acquireIfHeld('exclusive')) {
428
+ file.retryResult = {};
429
+ this._module.retryOps.push(this.#retryLockWrite(file));
430
+ return VFS.SQLITE_BUSY;
431
+ } else {
432
+ file.writeAhead.isolateForWrite();
433
+ }
434
+ } else {
435
+ // Read transaction.
436
+ if (!file.readLock.acquireIfHeld('shared')) {
437
+ file.retryResult = {};
438
+ this._module.retryOps.push(this.#retryLockRead(file));
439
+ return VFS.SQLITE_BUSY;
440
+ } else {
441
+ file.writeAhead.isolateForRead();
442
+ }
443
+ }
444
+ } else if (file.retryResult instanceof Error) {
445
+ const e = file.retryResult;
446
+ file.retryResult = null;
447
+ throw e;
448
+ }
449
+
450
+ // We have acquired the needed locks, either synchronously or
451
+ // via retry.
452
+ file.retryResult = null;
453
+ } else if (lockType >= VFS.SQLITE_LOCK_RESERVED && !file.writeLock.mode) {
454
+ // This is a write transaction but we don't already have the write
455
+ // lock. This happens when the write hint was not used, which this
456
+ // VFS treats as an error.
457
+ throw new Error('Write transaction cannot use BEGIN DEFERRED');
458
+ }
459
+ file.lockState = lockType;
460
+ return VFS.SQLITE_OK;
461
+ } catch (e) {
462
+ if (e.name === 'TimeoutError') {
463
+ return VFS.SQLITE_BUSY;
464
+ }
465
+
466
+ console.error(e.stack);
467
+ this.lastError = e;
468
+ return VFS.SQLITE_IOERR_LOCK;
469
+ }
470
+ }
471
+
472
+ /**
473
+ * @param {number} pFile
474
+ * @param {number} lockType
475
+ * @returns {number}
476
+ */
477
+ jUnlock(pFile, lockType) {
478
+ try {
479
+ const file = this.mapIdToFile.get(pFile);
480
+
481
+ // If retryResult is non-null, an asynchronous lock operation is in
482
+ // progress. In that case, don't change any locks.
483
+ if (!file.retryResult && lockType === VFS.SQLITE_LOCK_NONE) {
484
+ // In this VFS, this is the only unlock transition that matters.
485
+ // Exit write-ahead isolation.
486
+ file.writeAhead.rejoin();
487
+
488
+ // Release any locks.
489
+ switch (file.useLazyLock) {
490
+ case 'none':
491
+ file.writeLock.release();
492
+ file.readLock.release();
493
+ break;
494
+ case 'read':
495
+ file.writeLock.release();
496
+ file.readLock.releaseLazy();
497
+ break;
498
+ case 'write':
499
+ file.writeLock.releaseLazy();
500
+ file.readLock.release();
501
+ break;
502
+ case 'readwrite':
503
+ file.writeLock.releaseLazy();
504
+ file.readLock.releaseLazy();
505
+ break;
506
+ }
507
+
508
+ // Reset state for the next transaction.
509
+ file.writeHint = null;
510
+ }
511
+ file.lockState = lockType;
512
+ } catch (e) {
513
+ console.error(e.stack);
514
+ this.lastError = e;
515
+ return VFS.SQLITE_IOERR_UNLOCK;
516
+ }
517
+ }
518
+
519
+ /**
520
+ * @param {number} pFile
521
+ * @param {DataView} pResOut
522
+ * @returns {number}
523
+ */
524
+ jCheckReservedLock(pFile, pResOut) {
525
+ // A hot journal cannot exist so this method should never be called.
526
+ console.assert(false, 'unexpected');
527
+ pResOut.setInt32(0, 0, true);
528
+ return VFS.SQLITE_OK;
529
+ }
530
+
531
+ /**
532
+ * @param {number} pFile
533
+ * @param {number} op
534
+ * @param {DataView} pArg
535
+ * @returns {number}
536
+ */
537
+ jFileControl(pFile, op, pArg) {
538
+ try {
539
+ const file = this.mapIdToFile.get(pFile);
540
+ switch (op) {
541
+ case VFS.SQLITE_FCNTL_PRAGMA:
542
+ const key = this._module.UTF8ToString(pArg.getUint32(4, true));
543
+ const valueAddress = pArg.getUint32(8, true);
544
+ const value = valueAddress ? this._module.UTF8ToString(valueAddress) : null;
545
+ this.log?.(`PRAGMA ${key} ${value}`);
546
+ switch (key.toLowerCase()) {
547
+ case 'experimental_pragma_20251114':
548
+ // After entering the SHARED locking state on the next
549
+ // transaction, SQLite intends to immediately transition to
550
+ // RESERVED if value is '1', or EXCLUSIVE if value is '2'.
551
+ switch (value) {
552
+ case '1':
553
+ file.writeHint = 'reserved';
554
+ break;
555
+ case '2':
556
+ file.writeHint = 'exclusive';
557
+ break;
558
+ default:
559
+ throw new Error(`unexpected write hint value: ${value}`);
560
+ }
561
+ break;
562
+ case 'backstop_interval':
563
+ if (value !== null) {
564
+ const millis = parseInt(value);
565
+ file.writeAhead.setBackstopInterval(millis);
566
+ } else {
567
+ // Return current interval.
568
+ const s = file.writeAhead.options.backstopInterval.toString();
569
+ const ptr = this._module._sqlite3_malloc64(s.length + 1);
570
+ this._module.stringToUTF8(s, ptr, s.length + 1);
571
+ pArg.setUint32(0, ptr, true);
572
+ }
573
+ return VFS.SQLITE_OK;
574
+ case 'busy_timeout':
575
+ // Override SQLite's handling of busy timeouts with our
576
+ // blocking lock timeouts.
577
+ if (value !== null) {
578
+ file.timeout = parseInt(value);
579
+ } else {
580
+ // Return current timeout.
581
+ const s = file.timeout.toString();
582
+ const ptr = this._module._sqlite3_malloc64(s.length + 1);
583
+ this._module.stringToUTF8(s, ptr, s.length + 1);
584
+ pArg.setUint32(0, ptr, true);
585
+ }
586
+ return VFS.SQLITE_OK;
587
+ case 'journal_size_limit':
588
+ if (value !== null) {
589
+ const nPages = parseInt(value);
590
+ file.writeAhead.options.journalSizeLimit = nPages;
591
+ }
592
+ break;
593
+ case 'locking_mode':
594
+ // Track SQLite locking mode. Exclusive mode requires a
595
+ // write lock.
596
+ switch (value?.toLowerCase()) {
597
+ case 'normal':
598
+ file.lockingMode = 'normal';
599
+ break;
600
+ case 'exclusive':
601
+ file.lockingMode = 'exclusive';
602
+ break;
603
+ }
604
+ break;
605
+ case 'page_size':
606
+ if (value !== null) {
607
+ // Valid page sizes are 1 (which maps to 65536) or powers of
608
+ // two from 512 to 32768.
609
+ const n = parseInt(value);
610
+ if (n === 1 || (n >= 512 && n <= 32768 && (n & (n - 1)) === 0)) {
611
+ file.pageSize = n === 1 ? 65536 : n;
612
+ }
613
+ }
614
+ break;
615
+ case 'synchronous':
616
+ // Track SQLite synchronous mode. Write-ahead transactions
617
+ // trade durability for performance on values 1 (NORMAL) or
618
+ // lower.
619
+ if (value !== null) {
620
+ switch (value.toLowerCase()) {
621
+ case 'off':
622
+ case '0':
623
+ file.synchronous = 0;
624
+ break;
625
+ case 'normal':
626
+ case '1':
627
+ file.synchronous = 1;
628
+ break;
629
+ case 'full':
630
+ case '2':
631
+ file.synchronous = 2;
632
+ break;
633
+ case 'extra':
634
+ case '3':
635
+ file.synchronous = 3;
636
+ break;
637
+ default:
638
+ throw new Error(`unexpected synchronous value: ${value}`);
639
+ }
640
+ }
641
+ break;
642
+ case 'vfs_trace':
643
+ // This is a trace feature for debugging only.
644
+ if (value !== null) {
645
+ this.log = parseInt(value) !== 0 ? console.debug : null;
646
+ file.writeAhead.log = this.log;
647
+ }
648
+ return VFS.SQLITE_OK;
649
+ case 'wal_autocheckpoint':
650
+ // A setting greater than zero enables automatic checkpoints
651
+ // with this connection (enabled by default).
652
+ if (value !== null) {
653
+ file.writeAhead.options.autoCheckpoint = parseInt(value);
654
+ }
655
+ break;
656
+ case 'wal_checkpoint':
657
+ const checkpointMode = (value ?? 'passive').toLowerCase();
658
+ switch (checkpointMode) {
659
+ case 'passive':
660
+ this._module.pendingOps.push(this.#pendingCheckpoint(file, checkpointMode));
661
+ break;
662
+ case 'full':
663
+ case 'restart':
664
+ case 'truncate':
665
+ if (file.writeAhead.isTransactionPending()) {
666
+ throw new Error('invalid while a transaction is in progress');
667
+ }
668
+ this._module.pendingOps.push(this.#pendingCheckpoint(file, checkpointMode));
669
+ break;
670
+ case 'noop':
671
+ break;
672
+ default:
673
+ throw new Error(`unexpected wal_checkpoint mode: ${value}`);
674
+ }
675
+
676
+ // Return the approximate number of pages in the WAL before
677
+ // checkpointing. SQLite returns different information, but
678
+ // that is not feasible from a VFS.
679
+ {
680
+ const s = file.writeAhead.getWriteAheadSize().toString();
681
+ const ptr = this._module._sqlite3_malloc64(s.length + 1);
682
+ this._module.stringToUTF8(s, ptr, s.length + 1);
683
+ pArg.setUint32(0, ptr, true);
684
+ }
685
+ return VFS.SQLITE_OK;
686
+ case 'lazy_lock':
687
+ // Lazy locks don't actually release their Web Lock until
688
+ // they receive a message requesting it. Typically a setting
689
+ // of 'readwrite' (default) or 'read' is best.
690
+ if (value !== null) {
691
+ const useLazyLock = value.toLowerCase();
692
+ switch (useLazyLock) {
693
+ case 'read':
694
+ case 'write':
695
+ case 'readwrite':
696
+ case 'none':
697
+ file.useLazyLock = useLazyLock;
698
+ break;
699
+ default:
700
+ throw new Error(`unexpected value for lazy_lock: ${value}`);
701
+ }
702
+ }
703
+ {
704
+ const s = file.useLazyLock;
705
+ const ptr = this._module._sqlite3_malloc64(s.length + 1);
706
+ this._module.stringToUTF8(s, ptr, s.length + 1);
707
+ pArg.setUint32(0, ptr, true);
708
+ }
709
+ return VFS.SQLITE_OK;
710
+ }
711
+ break;
712
+
713
+ // Support SQLite batch atomic write transactions.
714
+ case VFS.SQLITE_FCNTL_BEGIN_ATOMIC_WRITE:
715
+ case VFS.SQLITE_FCNTL_COMMIT_ATOMIC_WRITE:
716
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
717
+ return VFS.SQLITE_OK;
718
+ }
719
+ break;
720
+ case VFS.SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE:
721
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
722
+ file.writeAhead.rollback();
723
+ return VFS.SQLITE_OK;
724
+ }
725
+ break;
726
+
727
+ case VFS.SQLITE_FCNTL_SYNC:
728
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
729
+ file.writeAhead.commit();
730
+ }
731
+ break;
732
+
733
+ case VFS.SQLITE_FCNTL_OVERWRITE:
734
+ file.overwrite = true;
735
+ break;
736
+ }
737
+ } catch (e) {
738
+ console.error(e.stack);
739
+ this.lastError = e;
740
+ return VFS.SQLITE_IOERR;
741
+ }
742
+ return VFS.SQLITE_NOTFOUND;
743
+ }
744
+
745
+ /**
746
+ * @param {number} pFile
747
+ * @returns {number}
748
+ */
749
+ jDeviceCharacteristics(pFile) {
750
+ return VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
751
+ | VFS.SQLITE_IOCAP_BATCH_ATOMIC;
752
+ }
753
+
754
+ /**
755
+ * @param {Uint8Array} zBuf
756
+ * @returns {number}
757
+ */
758
+ jGetLastError(zBuf) {
759
+ if (this.lastError) {
760
+ console.error(this.lastError);
761
+ const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
762
+ const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray);
763
+ zBuf[written] = 0;
764
+ }
765
+ return VFS.SQLITE_OK
766
+ }
767
+
768
+ /**
769
+ * @param {string} zName
770
+ * @returns {FileSystemSyncAccessHandle}
771
+ */
772
+ #openTemporaryFile(zName) {
773
+ if (this.unboundTempFiles.size === 0) {
774
+ throw new Error('no temporary files available');
775
+ }
776
+
777
+ // Bind an access handle from the temporary pool.
778
+ const accessHandle = this.unboundTempFiles.values().next().value;
779
+ this.unboundTempFiles.delete(accessHandle);
780
+ this.boundTempFiles.set(zName, accessHandle);
781
+ return accessHandle;
782
+ }
783
+
784
+ /**
785
+ * @param {FileEntry} file
786
+ */
787
+ #deleteTemporaryFile(file) {
788
+ file.accessHandle.truncate(0);
789
+
790
+ // Temporary files are not actually deleted, just returned to the pool.
791
+ this.mapPathToFile.delete(file.zName);
792
+ this.unboundTempFiles.add(file.accessHandle);
793
+ this.boundTempFiles.delete(file.zName);
794
+ }
795
+
796
+ /**
797
+ * @param {string} dbName
798
+ * @param {number} i
799
+ * @returns {string}
800
+ */
801
+ #getWriteAheadNameFromDbName(dbName, i) {
802
+ // Our WAL file is not compatible with SQLite WAL, so use a distinct name.
803
+ return `${dbName}-wa${i}`;
804
+ }
805
+
806
+ /**
807
+ * Asynchronous PRAGMA operation to checkpoint the write-ahead log.
808
+ * @param {FileEntry} file
809
+ * @param {'passive'|'full'|'restart'|'truncate'} mode
810
+ */
811
+ async #pendingCheckpoint(file, mode) {
812
+ const onFinally = [];
813
+ try {
814
+ if (mode !== 'passive' && file.lockState === VFS.SQLITE_LOCK_NONE) {
815
+ await file.writeLock.acquire('exclusive');
816
+ onFinally.push(() => file.writeLock.release());
817
+
818
+ file.writeAhead.isolateForWrite();
819
+ onFinally.push(() => file.writeAhead.rejoin());
820
+ }
821
+
822
+ await file.writeAhead.checkpoint({ isPassive: mode === 'passive' });
823
+ } catch (e) {
824
+ if (e.name === 'AbortError') {
825
+ e.code = VFS.SQLITE_BUSY;
826
+ }
827
+ throw e;
828
+ } finally {
829
+ while (onFinally.length) {
830
+ onFinally.pop()();
831
+ }
832
+ }
833
+ }
834
+
835
+ /**
836
+ * @param {FileEntry} file
837
+ */
838
+ async #retryLockRead(file) {
839
+ const onError = [];
840
+ try {
841
+ await file.readLock.acquire('shared', file.timeout);
842
+ onError.push(() => file.readLock.release());
843
+
844
+ file.writeAhead.isolateForRead();
845
+ file.retryResult = {};
846
+ } catch (e) {
847
+ while (onError.length) {
848
+ onError.pop()();
849
+ }
850
+ file.retryResult = e;
851
+ }
852
+ }
853
+
854
+ /**
855
+ * @param {FileEntry} file
856
+ */
857
+ async #retryLockWrite(file) {
858
+ const onError = [];
859
+ try {
860
+ // Exclusive locking mode requires both read and write locks.
861
+ // Otherwise, only the write lock is needed.
862
+ if (file.lockingMode === 'exclusive') {
863
+ await file.readLock.acquire('exclusive', file.timeout);
864
+ onError.push(() => file.readLock.release());
865
+ }
866
+
867
+ await file.writeLock.acquire('exclusive', file.timeout);
868
+ onError.push(() => file.writeLock.release());
869
+
870
+ file.writeAhead.isolateForWrite();
871
+ file.retryResult = {};
872
+ } catch (e) {
873
+ while (onError.length) {
874
+ onError.pop()();
875
+ }
876
+ file.retryResult = e;
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Handle asynchronous jOpen() tasks.
882
+ * @param {string} zName
883
+ * @param {number} flags
884
+ * @param {number} fileId
885
+ * @param {DataView} pOutFlags
886
+ * @returns {Promise<void>}
887
+ */
888
+ async #retryOpen(zName, flags, fileId, pOutFlags) {
889
+ /** @type {(() => void)[]} */ const onError = [];
890
+ const file = this.mapPathToFile.get(zName);
891
+ try {
892
+ await navigator.locks.request(`${zName}#ckpt`, async lock => {
893
+ // Parse the path components.
894
+ const directoryNames = zName.split('/').filter(d => d);
895
+ const dbName = directoryNames.pop();
896
+
897
+ // Get the OPFS directory handle.
898
+ let dirHandle = await navigator.storage.getDirectory();
899
+ const create = !!(flags & VFS.SQLITE_OPEN_CREATE);
900
+ for (const directoryName of directoryNames) {
901
+ dirHandle = await dirHandle.getDirectoryHandle(directoryName, { create });
902
+ }
903
+
904
+ const isNewDatabase = create && await (async function() {
905
+ try {
906
+ await dirHandle.getFileHandle(dbName);
907
+ return false;
908
+ } catch (e) {
909
+ if (e.name === 'NotFoundError') {
910
+ return true;
911
+ }
912
+ throw e;
913
+ }
914
+ })();
915
+
916
+ // Convenience function for opening access handles.
917
+ async function openFile(
918
+ /** @type {string} */ filename,
919
+ /** @type {FileSystemGetFileOptions} */ options) {
920
+ const fileHandle = await dirHandle.getFileHandle(filename, options);
921
+ // @ts-ignore
922
+ const accessHandle = await fileHandle.createSyncAccessHandle({
923
+ mode: 'readwrite-unsafe'
924
+ });
925
+ onError.push(() => {
926
+ accessHandle.close();
927
+ if (isNewDatabase) {
928
+ dirHandle.removeEntry(filename);
929
+ }
930
+ });
931
+ return accessHandle;
932
+ }
933
+
934
+ // Open the main database OPFS file.
935
+ const accessHandle = await openFile(dbName, { create });
936
+
937
+ // Open WAL files.
938
+ const waHandles = await Promise.all([0, 1].map(async i => {
939
+ const waName = this.#getWriteAheadNameFromDbName(dbName, i);
940
+ const waHandle = await openFile(waName, { create: true });
941
+ if (isNewDatabase) {
942
+ waHandle.truncate(0);
943
+ }
944
+ return waHandle;
945
+ }));
946
+
947
+ // Create the write-ahead manager.
948
+ const writeAhead = new WriteAhead(zName, accessHandle, waHandles);
949
+ await writeAhead.ready();
950
+
951
+ file.retryResult = { accessHandle, waHandles, writeAhead };
952
+ });
953
+ } catch (e) {
954
+ while (onError.length) {
955
+ onError.pop()();
956
+ }
957
+ file.retryResult = e;
958
+ }
959
+ }
960
+ }