@livestore/wa-sqlite 0.4.0-dev.22 → 0.4.0-dev.23

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 (62) hide show
  1. package/README.md +46 -36
  2. package/dist/README.md +13 -13
  3. package/dist/fts5/wa-sqlite.mjs +1 -1
  4. package/dist/fts5/wa-sqlite.node.mjs +1 -1
  5. package/dist/fts5/wa-sqlite.node.wasm +0 -0
  6. package/dist/fts5/wa-sqlite.wasm +0 -0
  7. package/dist/wa-sqlite-async.mjs +1 -1
  8. package/dist/wa-sqlite-async.wasm +0 -0
  9. package/dist/wa-sqlite-jspi.mjs +1 -1
  10. package/dist/wa-sqlite-jspi.wasm +0 -0
  11. package/dist/wa-sqlite.mjs +1 -1
  12. package/dist/wa-sqlite.node.mjs +1 -1
  13. package/dist/wa-sqlite.node.wasm +0 -0
  14. package/dist/wa-sqlite.wasm +0 -0
  15. package/package.json +40 -29
  16. package/src/FacadeVFS.js +252 -261
  17. package/src/VFS.js +84 -85
  18. package/src/WebLocksMixin.js +357 -351
  19. package/src/examples/AccessHandlePoolVFS.js +185 -194
  20. package/src/examples/IDBBatchAtomicVFS.js +429 -409
  21. package/src/examples/IDBMirrorVFS.js +402 -409
  22. package/src/examples/MemoryAsyncVFS.js +32 -37
  23. package/src/examples/MemoryVFS.js +71 -75
  24. package/src/examples/OPFSAdaptiveVFS.js +206 -206
  25. package/src/examples/OPFSAnyContextVFS.js +141 -140
  26. package/src/examples/OPFSCoopSyncVFS.js +297 -299
  27. package/src/examples/OPFSPermutedVFS.js +529 -540
  28. package/src/examples/README.md +27 -15
  29. package/src/examples/tag.js +27 -27
  30. package/src/sqlite-api.js +910 -941
  31. package/src/sqlite-constants.js +246 -232
  32. package/src/types/globals.d.ts +52 -52
  33. package/src/types/index.d.ts +586 -576
  34. package/test/AccessHandlePoolVFS.test.js +21 -21
  35. package/test/IDBBatchAtomicVFS.test.js +69 -69
  36. package/test/IDBMirrorVFS.test.js +21 -21
  37. package/test/MemoryAsyncVFS.test.js +21 -21
  38. package/test/MemoryVFS.test.js +21 -21
  39. package/test/OPFSAdaptiveVFS.test.js +21 -21
  40. package/test/OPFSAnyContextVFS.test.js +21 -21
  41. package/test/OPFSCoopSyncVFS.test.js +21 -21
  42. package/test/OPFSPermutedVFS.test.js +21 -21
  43. package/test/TestContext.js +44 -41
  44. package/test/WebLocksMixin.test.js +369 -360
  45. package/test/api.test.js +23 -23
  46. package/test/api_exec.js +72 -61
  47. package/test/api_misc.js +53 -54
  48. package/test/api_statements.js +271 -279
  49. package/test/callbacks.test.js +492 -478
  50. package/test/data/idbv5.json +1135 -1
  51. package/test/sql.test.js +30 -30
  52. package/test/sql_0001.js +49 -33
  53. package/test/sql_0002.js +55 -34
  54. package/test/sql_0003.js +85 -49
  55. package/test/sql_0004.js +76 -47
  56. package/test/sql_0005.js +60 -44
  57. package/test/test-worker.js +171 -163
  58. package/test/vfs_xAccess.js +1 -2
  59. package/test/vfs_xClose.js +50 -49
  60. package/test/vfs_xOpen.js +73 -72
  61. package/test/vfs_xRead.js +31 -31
  62. package/test/vfs_xWrite.js +30 -29
@@ -1,19 +1,19 @@
1
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';
2
+ import { FacadeVFS } from '../FacadeVFS.js'
3
+ import * as VFS from '../VFS.js'
4
+ import { WebLocksMixin } from '../WebLocksMixin.js'
5
5
 
6
6
  // Options for navigator.locks.request().
7
- /** @type {LockOptions} */ const SHARED = { mode: 'shared' };
8
- /** @type {LockOptions} */ const POLL_SHARED = { ifAvailable: true, mode: 'shared' };
9
- /** @type {LockOptions} */ const POLL_EXCLUSIVE = { ifAvailable: true, mode: 'exclusive' };
7
+ /** @type {LockOptions} */ const SHARED = { mode: 'shared' }
8
+ /** @type {LockOptions} */ const POLL_SHARED = { ifAvailable: true, mode: 'shared' }
9
+ /** @type {LockOptions} */ const POLL_EXCLUSIVE = { ifAvailable: true, mode: 'exclusive' }
10
10
 
11
11
  // Default number of transactions between flushing the OPFS file and
12
12
  // reclaiming free page offsets. Used only when synchronous! = 'full'.
13
- const DEFAULT_FLUSH_INTERVAL = 64;
13
+ const DEFAULT_FLUSH_INTERVAL = 64
14
14
 
15
15
  // Used only for debug logging.
16
- const contextId = Math.random().toString(36).slice(2);
16
+ const contextId = Math.random().toString(36).slice(2)
17
17
 
18
18
  /**
19
19
  * @typedef {Object} Transaction
@@ -30,195 +30,195 @@ const contextId = Math.random().toString(36).slice(2);
30
30
  */
31
31
 
32
32
  class File {
33
- /** @type {string} */ path;
34
- /** @type {number} */ flags;
35
- /** @type {FileSystemSyncAccessHandle} */ accessHandle;
33
+ /** @type {string} */ path
34
+ /** @type {number} */ flags
35
+ /** @type {FileSystemSyncAccessHandle} */ accessHandle
36
36
 
37
37
  // Members below are only used for SQLITE_OPEN_MAIN_DB.
38
38
 
39
- /** @type {number} */ pageSize;
40
- /** @type {number} */ fileSize; // virtual file size exposed to SQLite
39
+ /** @type {number} */ pageSize
40
+ /** @type {number} */ fileSize // virtual file size exposed to SQLite
41
41
 
42
- /** @type {IDBDatabase} */ idb;
42
+ /** @type {IDBDatabase} */ idb
43
43
 
44
- /** @type {Transaction} */ viewTx; // last transaction incorporated
45
- /** @type {function?} */ viewReleaser;
44
+ /** @type {Transaction} */ viewTx // last transaction incorporated
45
+ /** @type {function?} */ viewReleaser
46
46
 
47
- /** @type {BroadcastChannel} */ broadcastChannel;
48
- /** @type {(Transaction|AccessRequest)[]} */ broadcastReceived;
47
+ /** @type {BroadcastChannel} */ broadcastChannel
48
+ /** @type {(Transaction|AccessRequest)[]} */ broadcastReceived
49
49
 
50
- /** @type {Map<number, number>} */ mapPageToOffset;
51
- /** @type {Map<number, Transaction>} */ mapTxToPending;
52
- /** @type {Set<number>} */ freeOffsets;
50
+ /** @type {Map<number, number>} */ mapPageToOffset
51
+ /** @type {Map<number, Transaction>} */ mapTxToPending
52
+ /** @type {Set<number>} */ freeOffsets
53
53
 
54
- /** @type {number} */ lockState;
55
- /** @type {{read?: function, write?: function, reserved?: function, hint?: function}} */ locks;
54
+ /** @type {number} */ lockState
55
+ /** @type {{read?: function, write?: function, reserved?: function, hint?: function}} */ locks
56
56
 
57
- /** @type {AbortController} */ abortController;
57
+ /** @type {AbortController} */ abortController
58
58
 
59
- /** @type {Transaction?} */ txActive; // transaction in progress
60
- /** @type {number} */ txRealFileSize; // physical file size
61
- /** @type {boolean} */ txIsOverwrite; // VACUUM in progress
62
- /** @type {boolean} */ txWriteHint;
59
+ /** @type {Transaction?} */ txActive // transaction in progress
60
+ /** @type {number} */ txRealFileSize // physical file size
61
+ /** @type {boolean} */ txIsOverwrite // VACUUM in progress
62
+ /** @type {boolean} */ txWriteHint
63
63
 
64
- /** @type {'full'|'normal'} */ synchronous;
65
- /** @type {number} */ flushInterval;
64
+ /** @type {'full'|'normal'} */ synchronous
65
+ /** @type {number} */ flushInterval
66
66
 
67
67
  /**
68
- * @param {string} pathname
69
- * @param {number} flags
68
+ * @param {string} pathname
69
+ * @param {number} flags
70
70
  */
71
71
  constructor(pathname, flags) {
72
- this.path = pathname;
73
- this.flags = flags;
72
+ this.path = pathname
73
+ this.flags = flags
74
74
  }
75
75
 
76
76
  /**
77
- * @param {string} pathname
78
- * @param {number} flags
79
- * @returns
77
+ * @param {string} pathname
78
+ * @param {number} flags
79
+ * @returns
80
80
  */
81
81
  static async create(pathname, flags) {
82
- const file = new File(pathname, flags);
82
+ const file = new File(pathname, flags)
83
83
 
84
- const create = !!(flags & VFS.SQLITE_OPEN_CREATE);
85
- const [directory, filename] = await getPathComponents(pathname, create);
86
- const handle = await directory.getFileHandle(filename, { create });
84
+ const create = !!(flags & VFS.SQLITE_OPEN_CREATE)
85
+ const [directory, filename] = await getPathComponents(pathname, create)
86
+ const handle = await directory.getFileHandle(filename, { create })
87
87
  // @ts-ignore
88
- file.accessHandle = await handle.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
88
+ file.accessHandle = await handle.createSyncAccessHandle({ mode: 'readwrite-unsafe' })
89
89
 
90
90
  if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
91
91
  file.idb = await new Promise((resolve, reject) => {
92
- const request = indexedDB.open(pathname);
92
+ const request = indexedDB.open(pathname)
93
93
  request.onupgradeneeded = () => {
94
- const db = request.result;
95
- db.createObjectStore('pages', { keyPath: 'i' });
96
- db.createObjectStore('pending', { keyPath: 'txId'});
97
- };
98
- request.onsuccess = () => resolve(request.result);
99
- request.onerror = () => reject(request.error);
100
- });
94
+ const db = request.result
95
+ db.createObjectStore('pages', { keyPath: 'i' })
96
+ db.createObjectStore('pending', { keyPath: 'txId' })
97
+ }
98
+ request.onsuccess = () => resolve(request.result)
99
+ request.onerror = () => reject(request.error)
100
+ })
101
101
  }
102
- return file;
102
+ return file
103
103
  }
104
104
  }
105
105
 
106
106
  export class OPFSPermutedVFS extends FacadeVFS {
107
- /** @type {Map<number, File>} */ #mapIdToFile = new Map();
108
- #lastError = null;
107
+ /** @type {Map<number, File>} */ #mapIdToFile = new Map()
108
+ #lastError = null
109
109
 
110
- log = null; // (...args) => console.debug(contextId, ...args);
110
+ log = null // (...args) => console.debug(contextId, ...args);
111
111
 
112
112
  /**
113
- * @param {string} name
114
- * @param {*} module
115
- * @returns
113
+ * @param {string} name
114
+ * @param {*} module
115
+ * @returns
116
116
  */
117
117
  static async create(name, module) {
118
- const vfs = new OPFSPermutedVFS(name, module);
119
- await vfs.isReady();
120
- return vfs;
118
+ const vfs = new OPFSPermutedVFS(name, module)
119
+ await vfs.isReady()
120
+ return vfs
121
121
  }
122
122
 
123
123
  /**
124
- * @param {string?} zName
125
- * @param {number} fileId
126
- * @param {number} flags
127
- * @param {DataView} pOutFlags
124
+ * @param {string?} zName
125
+ * @param {number} fileId
126
+ * @param {number} flags
127
+ * @param {DataView} pOutFlags
128
128
  * @returns {Promise<number>}
129
129
  */
130
130
  async jOpen(zName, fileId, flags, pOutFlags) {
131
- /** @type {(() => void)[]} */ const onFinally = [];
131
+ /** @type {(() => void)[]} */ const onFinally = []
132
132
  try {
133
- const url = new URL(zName || Math.random().toString(36).slice(2), 'file://');
134
- const path = url.pathname;
133
+ const url = new URL(zName || Math.random().toString(36).slice(2), 'file://')
134
+ const path = url.pathname
135
135
 
136
- const file = await File.create(path, flags);
136
+ const file = await File.create(path, flags)
137
137
  if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
138
- file.pageSize = 0;
139
- file.fileSize = 0;
140
- file.viewTx = { txId: 0 };
141
- file.broadcastChannel = new BroadcastChannel(`permuted:${path}`);
142
- file.broadcastReceived = [];
143
- file.mapPageToOffset = new Map();
144
- file.mapTxToPending = new Map();
145
- file.freeOffsets = new Set();
146
- file.lockState = VFS.SQLITE_LOCK_NONE;
147
- file.locks = {};
148
- file.abortController = new AbortController();
149
- file.txIsOverwrite = false;
150
- file.txActive = null;
151
- file.synchronous = 'full';
152
- file.flushInterval = DEFAULT_FLUSH_INTERVAL;
138
+ file.pageSize = 0
139
+ file.fileSize = 0
140
+ file.viewTx = { txId: 0 }
141
+ file.broadcastChannel = new BroadcastChannel(`permuted:${path}`)
142
+ file.broadcastReceived = []
143
+ file.mapPageToOffset = new Map()
144
+ file.mapTxToPending = new Map()
145
+ file.freeOffsets = new Set()
146
+ file.lockState = VFS.SQLITE_LOCK_NONE
147
+ file.locks = {}
148
+ file.abortController = new AbortController()
149
+ file.txIsOverwrite = false
150
+ file.txActive = null
151
+ file.synchronous = 'full'
152
+ file.flushInterval = DEFAULT_FLUSH_INTERVAL
153
153
 
154
154
  // Take the write lock so no other connection changes state
155
155
  // during our initialization.
156
- await this.#lock(file, 'write');
157
- onFinally.push(() => file.locks.write());
156
+ await this.#lock(file, 'write')
157
+ onFinally.push(() => file.locks.write())
158
158
 
159
159
  // Load the initial page map from the database.
160
- const tx = file.idb.transaction(['pages', 'pending']);
161
- const pages = await idbX(tx.objectStore('pages').getAll());
162
- file.pageSize = this.#getPageSize(file);
163
- file.fileSize = pages.length * file.pageSize;
160
+ const tx = file.idb.transaction(['pages', 'pending'])
161
+ const pages = await idbX(tx.objectStore('pages').getAll())
162
+ file.pageSize = this.#getPageSize(file)
163
+ file.fileSize = pages.length * file.pageSize
164
164
 
165
165
  // Begin with adding all file offsets to the free list.
166
- const opfsFileSize = file.accessHandle.getSize();
166
+ const opfsFileSize = file.accessHandle.getSize()
167
167
  for (let i = 0; i < opfsFileSize; i += file.pageSize) {
168
- file.freeOffsets.add(i);
168
+ file.freeOffsets.add(i)
169
169
  }
170
170
 
171
171
  // Incorporate the page map data.
172
172
  for (const { i, o } of pages) {
173
- file.mapPageToOffset.set(i, o);
174
- file.freeOffsets.delete(o);
173
+ file.mapPageToOffset.set(i, o)
174
+ file.freeOffsets.delete(o)
175
175
  }
176
176
 
177
177
  // Incorporate pending transactions.
178
178
  try {
179
179
  /** @type {Transaction[]} */
180
- const transactions = await idbX(tx.objectStore('pending').getAll());
180
+ const transactions = await idbX(tx.objectStore('pending').getAll())
181
181
  for (const transaction of transactions) {
182
182
  // Verify checksums for all pages in this transaction.
183
183
  for (const [index, { offset, digest }] of transaction.pages) {
184
- const data = new Uint8Array(file.pageSize);
185
- file.accessHandle.read(data, { at: offset });
184
+ const data = new Uint8Array(file.pageSize)
185
+ file.accessHandle.read(data, { at: offset })
186
186
  if (checksum(data).some((v, i) => v !== digest[i])) {
187
- throw Object.assign(new Error('checksum error'), { txId: transaction.txId });
187
+ throw Object.assign(new Error('checksum error'), { txId: transaction.txId })
188
188
  }
189
189
  }
190
- this.#acceptTx(file, transaction);
191
- file.viewTx = transaction;
190
+ this.#acceptTx(file, transaction)
191
+ file.viewTx = transaction
192
192
  }
193
193
  } catch (e) {
194
194
  if (e.message === 'checksum error') {
195
195
  console.warn(`Checksum error, removing tx ${e.txId}+`)
196
- const tx = file.idb.transaction('pending', 'readwrite');
196
+ const tx = file.idb.transaction('pending', 'readwrite')
197
197
  const txCommit = new Promise((resolve, reject) => {
198
- tx.oncomplete = resolve;
199
- tx.onabort = () => reject(tx.error);
200
- });
201
- const range = IDBKeyRange.lowerBound(e.txId);
202
- tx.objectStore('pending').delete(range);
203
- tx.commit();
204
- await txCommit;
198
+ tx.oncomplete = resolve
199
+ tx.onabort = () => reject(tx.error)
200
+ })
201
+ const range = IDBKeyRange.lowerBound(e.txId)
202
+ tx.objectStore('pending').delete(range)
203
+ tx.commit()
204
+ await txCommit
205
205
  } else {
206
- throw e;
206
+ throw e
207
207
  }
208
208
  }
209
209
 
210
210
  // Publish our view of the database. This prevents other connections
211
211
  // from overwriting file data we still need.
212
- await this.#setView(file, file.viewTx);
212
+ await this.#setView(file, file.viewTx)
213
213
 
214
214
  // Listen for broadcasts. Messages are cached until the database
215
215
  // is unlocked.
216
- file.broadcastChannel.addEventListener('message', event => {
217
- file.broadcastReceived.push(event.data);
216
+ file.broadcastChannel.addEventListener('message', (event) => {
217
+ file.broadcastReceived.push(event.data)
218
218
  if (file.lockState === VFS.SQLITE_LOCK_NONE) {
219
- this.#processBroadcasts(file);
219
+ this.#processBroadcasts(file)
220
220
  }
221
- });
221
+ })
222
222
 
223
223
  // Connections usually hold this shared read lock so they don't
224
224
  // acquire and release it for every transaction. The only time
@@ -227,354 +227,352 @@ export class OPFSPermutedVFS extends FacadeVFS {
227
227
  await this.#lock(file, 'read', SHARED)
228
228
  }
229
229
 
230
- pOutFlags.setInt32(0, flags, true);
231
- this.#mapIdToFile.set(fileId, file);
232
- return VFS.SQLITE_OK;
230
+ pOutFlags.setInt32(0, flags, true)
231
+ this.#mapIdToFile.set(fileId, file)
232
+ return VFS.SQLITE_OK
233
233
  } catch (e) {
234
- this.#lastError = e;
235
- return VFS.SQLITE_CANTOPEN;
234
+ this.#lastError = e
235
+ return VFS.SQLITE_CANTOPEN
236
236
  } finally {
237
237
  while (onFinally.length) {
238
- await onFinally.pop()();
238
+ await onFinally.pop()()
239
239
  }
240
240
  }
241
241
  }
242
242
 
243
243
  /**
244
- * @param {string} zName
245
- * @param {number} syncDir
244
+ * @param {string} zName
245
+ * @param {number} syncDir
246
246
  * @returns {Promise<number>}
247
247
  */
248
248
  async jDelete(zName, syncDir) {
249
249
  try {
250
- const url = new URL(zName, 'file://');
251
- const pathname = url.pathname;
252
-
253
- const [directoryHandle, name] = await getPathComponents(pathname, false);
254
- const result = directoryHandle.removeEntry(name, { recursive: false });
250
+ const url = new URL(zName, 'file://')
251
+ const pathname = url.pathname
252
+
253
+ const [directoryHandle, name] = await getPathComponents(pathname, false)
254
+ const result = directoryHandle.removeEntry(name, { recursive: false })
255
255
  if (syncDir) {
256
- await result;
256
+ await result
257
257
  }
258
- return VFS.SQLITE_OK;
258
+ return VFS.SQLITE_OK
259
259
  } catch (e) {
260
- return VFS.SQLITE_IOERR_DELETE;
260
+ return VFS.SQLITE_IOERR_DELETE
261
261
  }
262
262
  }
263
263
 
264
264
  /**
265
- * @param {string} zName
266
- * @param {number} flags
267
- * @param {DataView} pResOut
265
+ * @param {string} zName
266
+ * @param {number} flags
267
+ * @param {DataView} pResOut
268
268
  * @returns {Promise<number>}
269
269
  */
270
270
  async jAccess(zName, flags, pResOut) {
271
271
  try {
272
- const url = new URL(zName, 'file://');
273
- const pathname = url.pathname;
272
+ const url = new URL(zName, 'file://')
273
+ const pathname = url.pathname
274
274
 
275
- const [directoryHandle, dbName] = await getPathComponents(pathname, false);
276
- await directoryHandle.getFileHandle(dbName, { create: false });
277
- pResOut.setInt32(0, 1, true);
278
- return VFS.SQLITE_OK;
275
+ const [directoryHandle, dbName] = await getPathComponents(pathname, false)
276
+ await directoryHandle.getFileHandle(dbName, { create: false })
277
+ pResOut.setInt32(0, 1, true)
278
+ return VFS.SQLITE_OK
279
279
  } catch (e) {
280
280
  if (e.name === 'NotFoundError') {
281
- pResOut.setInt32(0, 0, true);
282
- return VFS.SQLITE_OK;
281
+ pResOut.setInt32(0, 0, true)
282
+ return VFS.SQLITE_OK
283
283
  }
284
- this.#lastError = e;
285
- return VFS.SQLITE_IOERR_ACCESS;
284
+ this.#lastError = e
285
+ return VFS.SQLITE_IOERR_ACCESS
286
286
  }
287
287
  }
288
288
 
289
289
  /**
290
- * @param {number} fileId
290
+ * @param {number} fileId
291
291
  * @returns {Promise<number>}
292
292
  */
293
293
  async jClose(fileId) {
294
294
  try {
295
- const file = this.#mapIdToFile.get(fileId);
296
- this.#mapIdToFile.delete(fileId);
297
- file?.accessHandle?.close();
295
+ const file = this.#mapIdToFile.get(fileId)
296
+ this.#mapIdToFile.delete(fileId)
297
+ file?.accessHandle?.close()
298
298
 
299
299
  if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) {
300
- file.broadcastChannel.close();
301
- file.viewReleaser?.();
300
+ file.broadcastChannel.close()
301
+ file.viewReleaser?.()
302
302
  }
303
303
 
304
304
  if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
305
- const [directoryHandle, name] = await getPathComponents(file.path, false);
306
- await directoryHandle.removeEntry(name, { recursive: false });
305
+ const [directoryHandle, name] = await getPathComponents(file.path, false)
306
+ await directoryHandle.removeEntry(name, { recursive: false })
307
307
  }
308
- return VFS.SQLITE_OK;
308
+ return VFS.SQLITE_OK
309
309
  } catch (e) {
310
- return VFS.SQLITE_IOERR_CLOSE;
310
+ return VFS.SQLITE_IOERR_CLOSE
311
311
  }
312
312
  }
313
313
 
314
314
  /**
315
- * @param {number} fileId
316
- * @param {Uint8Array} pData
315
+ * @param {number} fileId
316
+ * @param {Uint8Array} pData
317
317
  * @param {number} iOffset
318
318
  * @returns {number}
319
319
  */
320
320
  jRead(fileId, pData, iOffset) {
321
321
  try {
322
- const file = this.#mapIdToFile.get(fileId);
322
+ const file = this.#mapIdToFile.get(fileId)
323
323
 
324
- let bytesRead = 0;
324
+ let bytesRead = 0
325
325
  if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
326
- file.abortController.signal.throwIfAborted();
326
+ file.abortController.signal.throwIfAborted()
327
327
 
328
328
  // Look up the page location in the file. Check the pages in
329
329
  // any active write transaction first, then the main map.
330
- const pageIndex = file.pageSize ?
331
- Math.trunc(iOffset / file.pageSize) + 1:
332
- 1;
333
- const pageOffset = file.txActive?.pages.has(pageIndex) ?
334
- file.txActive.pages.get(pageIndex).offset :
335
- file.mapPageToOffset.get(pageIndex);
330
+ const pageIndex = file.pageSize ? Math.trunc(iOffset / file.pageSize) + 1 : 1
331
+ const pageOffset = file.txActive?.pages.has(pageIndex)
332
+ ? file.txActive.pages.get(pageIndex).offset
333
+ : file.mapPageToOffset.get(pageIndex)
336
334
  if (pageOffset >= 0) {
337
- this.log?.(`read page ${pageIndex} at ${pageOffset}`);
338
- bytesRead = file.accessHandle.read(
339
- pData.subarray(),
340
- { at: pageOffset + (file.pageSize ? iOffset % file.pageSize : 0) });
335
+ this.log?.(`read page ${pageIndex} at ${pageOffset}`)
336
+ bytesRead = file.accessHandle.read(pData.subarray(), {
337
+ at: pageOffset + (file.pageSize ? iOffset % file.pageSize : 0),
338
+ })
341
339
  }
342
340
 
343
341
  // Get page size if not already known.
344
342
  if (!file.pageSize && iOffset <= 16 && iOffset + bytesRead >= 18) {
345
- const dataView = new DataView(pData.slice(16 - iOffset, 18 - iOffset).buffer);
346
- file.pageSize = dataView.getUint16(0);
343
+ const dataView = new DataView(pData.slice(16 - iOffset, 18 - iOffset).buffer)
344
+ file.pageSize = dataView.getUint16(0)
347
345
  if (file.pageSize === 1) {
348
- file.pageSize = 65536;
346
+ file.pageSize = 65536
349
347
  }
350
- this.log?.(`set page size ${file.pageSize}`);
348
+ this.log?.(`set page size ${file.pageSize}`)
351
349
  }
352
350
  } else {
353
351
  // On Chrome (at least), passing pData to accessHandle.read() is
354
352
  // an error because pData is a Proxy of a Uint8Array. Calling
355
353
  // subarray() produces a real Uint8Array and that works.
356
- bytesRead = file.accessHandle.read(pData.subarray(), { at: iOffset });
354
+ bytesRead = file.accessHandle.read(pData.subarray(), { at: iOffset })
357
355
  }
358
356
 
359
357
  if (bytesRead < pData.byteLength) {
360
- pData.fill(0, bytesRead);
361
- return VFS.SQLITE_IOERR_SHORT_READ;
358
+ pData.fill(0, bytesRead)
359
+ return VFS.SQLITE_IOERR_SHORT_READ
362
360
  }
363
- return VFS.SQLITE_OK;
361
+ return VFS.SQLITE_OK
364
362
  } catch (e) {
365
- this.#lastError = e;
366
- return VFS.SQLITE_IOERR_READ;
363
+ this.#lastError = e
364
+ return VFS.SQLITE_IOERR_READ
367
365
  }
368
366
  }
369
367
 
370
368
  /**
371
- * @param {number} fileId
372
- * @param {Uint8Array} pData
369
+ * @param {number} fileId
370
+ * @param {Uint8Array} pData
373
371
  * @param {number} iOffset
374
372
  * @returns {number}
375
373
  */
376
374
  jWrite(fileId, pData, iOffset) {
377
375
  try {
378
- const file = this.#mapIdToFile.get(fileId);
376
+ const file = this.#mapIdToFile.get(fileId)
379
377
 
380
378
  if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
381
- file.abortController.signal.throwIfAborted();
379
+ file.abortController.signal.throwIfAborted()
382
380
  if (!file.pageSize) {
383
381
  this.log?.(`set page size ${pData.byteLength}`)
384
- file.pageSize = pData.byteLength;
382
+ file.pageSize = pData.byteLength
385
383
  }
386
384
 
387
385
  // The first write begins a transaction. Note that xLock/xUnlock
388
386
  // is not a good way to determine transaction boundaries because
389
387
  // PRAGMA locking_mode can change the behavior.
390
388
  if (!file.txActive) {
391
- this.#beginTx(file);
389
+ this.#beginTx(file)
392
390
  }
393
391
 
394
392
  // Choose the offset in the file to write this page.
395
- let pageOffset;
396
- const pageIndex = Math.trunc(iOffset / file.pageSize) + 1;
393
+ let pageOffset
394
+ const pageIndex = Math.trunc(iOffset / file.pageSize) + 1
397
395
  if (file.txIsOverwrite) {
398
396
  // For VACUUM, use the identity mapping to write each page
399
397
  // at its canonical offset.
400
- pageOffset = iOffset;
398
+ pageOffset = iOffset
401
399
  } else if (file.txActive.pages.has(pageIndex)) {
402
400
  // This page has already been written in this transaction.
403
401
  // Use the same offset.
404
- pageOffset = file.txActive.pages.get(pageIndex).offset;
405
- this.log?.(`overwrite page ${pageIndex} at ${pageOffset}`);
402
+ pageOffset = file.txActive.pages.get(pageIndex).offset
403
+ this.log?.(`overwrite page ${pageIndex} at ${pageOffset}`)
406
404
  } else if (pageIndex === 1 && file.freeOffsets.delete(0)) {
407
405
  // Offset 0 is available for page 1.
408
- pageOffset = 0;
409
- this.log?.(`write page ${pageIndex} at ${pageOffset}`);
406
+ pageOffset = 0
407
+ this.log?.(`write page ${pageIndex} at ${pageOffset}`)
410
408
  } else {
411
409
  // Use the first unused non-zero offset within the file.
412
410
  for (const maybeOffset of file.freeOffsets) {
413
411
  if (maybeOffset) {
414
412
  if (maybeOffset < file.txRealFileSize) {
415
- pageOffset = maybeOffset;
416
- file.freeOffsets.delete(pageOffset);
417
- this.log?.(`write page ${pageIndex} at ${pageOffset}`);
418
- break;
413
+ pageOffset = maybeOffset
414
+ file.freeOffsets.delete(pageOffset)
415
+ this.log?.(`write page ${pageIndex} at ${pageOffset}`)
416
+ break
419
417
  } else {
420
418
  // This offset is beyond the end of the file.
421
- file.freeOffsets.delete(maybeOffset);
419
+ file.freeOffsets.delete(maybeOffset)
422
420
  }
423
421
  }
424
422
  }
425
423
 
426
424
  if (pageOffset === undefined) {
427
425
  // Write to the end of the file.
428
- pageOffset = file.txRealFileSize;
429
- this.log?.(`append page ${pageIndex} at ${pageOffset}`);
426
+ pageOffset = file.txRealFileSize
427
+ this.log?.(`append page ${pageIndex} at ${pageOffset}`)
430
428
  }
431
429
  }
432
- file.accessHandle.write(pData.subarray(), { at: pageOffset });
430
+ file.accessHandle.write(pData.subarray(), { at: pageOffset })
433
431
 
434
432
  // Update the transaction.
435
433
  file.txActive.pages.set(pageIndex, {
436
434
  offset: pageOffset,
437
- digest: checksum(pData.subarray())
438
- });
439
- file.txActive.fileSize = Math.max(file.txActive.fileSize, pageIndex * file.pageSize);
435
+ digest: checksum(pData.subarray()),
436
+ })
437
+ file.txActive.fileSize = Math.max(file.txActive.fileSize, pageIndex * file.pageSize)
440
438
 
441
439
  // Track the actual file size.
442
- file.txRealFileSize = Math.max(file.txRealFileSize, pageOffset + pData.byteLength);
440
+ file.txRealFileSize = Math.max(file.txRealFileSize, pageOffset + pData.byteLength)
443
441
  } else {
444
442
  // On Chrome (at least), passing pData to accessHandle.write() is
445
443
  // an error because pData is a Proxy of a Uint8Array. Calling
446
444
  // subarray() produces a real Uint8Array and that works.
447
- file.accessHandle.write(pData.subarray(), { at: iOffset });
445
+ file.accessHandle.write(pData.subarray(), { at: iOffset })
448
446
  }
449
- return VFS.SQLITE_OK;
447
+ return VFS.SQLITE_OK
450
448
  } catch (e) {
451
- this.#lastError = e;
452
- return VFS.SQLITE_IOERR_WRITE;
449
+ this.#lastError = e
450
+ return VFS.SQLITE_IOERR_WRITE
453
451
  }
454
452
  }
455
453
 
456
454
  /**
457
- * @param {number} fileId
458
- * @param {number} iSize
455
+ * @param {number} fileId
456
+ * @param {number} iSize
459
457
  * @returns {number}
460
458
  */
461
459
  jTruncate(fileId, iSize) {
462
460
  try {
463
- const file = this.#mapIdToFile.get(fileId);
464
- if ((file.flags & VFS.SQLITE_OPEN_MAIN_DB) && !file.txIsOverwrite) {
465
- file.abortController.signal.throwIfAborted();
461
+ const file = this.#mapIdToFile.get(fileId)
462
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB && !file.txIsOverwrite) {
463
+ file.abortController.signal.throwIfAborted()
466
464
  if (!file.txActive) {
467
- this.#beginTx(file);
465
+ this.#beginTx(file)
468
466
  }
469
- file.txActive.fileSize = iSize;
467
+ file.txActive.fileSize = iSize
470
468
 
471
469
  // Remove now obsolete pages from file.txActive.pages
472
470
  for (const [index, { offset }] of file.txActive.pages) {
473
471
  // Page indices are 1-based.
474
472
  if (index * file.pageSize > iSize) {
475
- file.txActive.pages.delete(index);
476
- file.freeOffsets.add(offset);
473
+ file.txActive.pages.delete(index)
474
+ file.freeOffsets.add(offset)
477
475
  }
478
476
  }
479
- return VFS.SQLITE_OK;
477
+ return VFS.SQLITE_OK
480
478
  }
481
- file.accessHandle.truncate(iSize);
482
- return VFS.SQLITE_OK;
479
+ file.accessHandle.truncate(iSize)
480
+ return VFS.SQLITE_OK
483
481
  } catch (e) {
484
- console.error(e);
485
- this.lastError = e;
486
- return VFS.SQLITE_IOERR_TRUNCATE;
482
+ console.error(e)
483
+ this.lastError = e
484
+ return VFS.SQLITE_IOERR_TRUNCATE
487
485
  }
488
486
  }
489
487
 
490
488
  /**
491
- * @param {number} fileId
492
- * @param {number} flags
489
+ * @param {number} fileId
490
+ * @param {number} flags
493
491
  * @returns {number}
494
492
  */
495
493
  jSync(fileId, flags) {
496
494
  try {
497
495
  // Main DB sync is handled by SQLITE_FCNTL_SYNC.
498
- const file = this.#mapIdToFile.get(fileId);
496
+ const file = this.#mapIdToFile.get(fileId)
499
497
  if (!(file.flags & VFS.SQLITE_OPEN_MAIN_DB)) {
500
- file.accessHandle.flush();
498
+ file.accessHandle.flush()
501
499
  }
502
- return VFS.SQLITE_OK;
500
+ return VFS.SQLITE_OK
503
501
  } catch (e) {
504
- this.#lastError = e;
505
- return VFS.SQLITE_IOERR_FSYNC;
502
+ this.#lastError = e
503
+ return VFS.SQLITE_IOERR_FSYNC
506
504
  }
507
505
  }
508
506
 
509
507
  /**
510
- * @param {number} fileId
511
- * @param {DataView} pSize64
508
+ * @param {number} fileId
509
+ * @param {DataView} pSize64
512
510
  * @returns {number}
513
511
  */
514
512
  jFileSize(fileId, pSize64) {
515
513
  try {
516
- const file = this.#mapIdToFile.get(fileId);
514
+ const file = this.#mapIdToFile.get(fileId)
517
515
 
518
- let size;
516
+ let size
519
517
  if (file.flags & VFS.SQLITE_OPEN_MAIN_DB) {
520
- file.abortController.signal.throwIfAborted();
521
- size = file.txActive?.fileSize ?? file.fileSize;
518
+ file.abortController.signal.throwIfAborted()
519
+ size = file.txActive?.fileSize ?? file.fileSize
522
520
  } else {
523
- size = file.accessHandle.getSize();
521
+ size = file.accessHandle.getSize()
524
522
  }
525
523
 
526
- pSize64.setBigInt64(0, BigInt(size), true);
527
- return VFS.SQLITE_OK;
524
+ pSize64.setBigInt64(0, BigInt(size), true)
525
+ return VFS.SQLITE_OK
528
526
  } catch (e) {
529
- this.#lastError = e;
530
- return VFS.SQLITE_IOERR_FSTAT;
527
+ this.#lastError = e
528
+ return VFS.SQLITE_IOERR_FSTAT
531
529
  }
532
530
  }
533
531
 
534
532
  /**
535
- * @param {number} fileId
536
- * @param {number} lockType
533
+ * @param {number} fileId
534
+ * @param {number} lockType
537
535
  * @returns {Promise<number>}
538
536
  */
539
537
  async jLock(fileId, lockType) {
540
- const file = this.#mapIdToFile.get(fileId);
541
- if (lockType <= file.lockState) return VFS.SQLITE_OK;
538
+ const file = this.#mapIdToFile.get(fileId)
539
+ if (lockType <= file.lockState) return VFS.SQLITE_OK
542
540
  switch (lockType) {
543
541
  case VFS.SQLITE_LOCK_SHARED:
544
542
  if (file.txWriteHint) {
545
- // xFileControl() has hinted that this transaction will
546
- // write. Acquire the hint lock, which is required to reach
547
- // the RESERVED state.
548
- if (!await this.#lock(file, 'hint')) {
549
- return VFS.SQLITE_BUSY;
550
- }
543
+ // xFileControl() has hinted that this transaction will
544
+ // write. Acquire the hint lock, which is required to reach
545
+ // the RESERVED state.
546
+ if (!(await this.#lock(file, 'hint'))) {
547
+ return VFS.SQLITE_BUSY
548
+ }
551
549
  }
552
550
 
553
551
  if (!file.locks.read) {
554
552
  // Reacquire lock if it was released by a broadcast request.
555
- await this.#lock(file, 'read', SHARED);
553
+ await this.#lock(file, 'read', SHARED)
556
554
  }
557
- break;
555
+ break
558
556
  case VFS.SQLITE_LOCK_RESERVED:
559
557
  // Ideally we should already have the hint lock, but if not
560
558
  // poll for it here.
561
- if (!file.locks.hint && !await this.#lock(file, 'hint', POLL_EXCLUSIVE)) {
562
- return VFS.SQLITE_BUSY;
559
+ if (!file.locks.hint && !(await this.#lock(file, 'hint', POLL_EXCLUSIVE))) {
560
+ return VFS.SQLITE_BUSY
563
561
  }
564
562
 
565
- if (!await this.#lock(file, 'reserved', POLL_EXCLUSIVE)) {
566
- file.locks.hint();
567
- return VFS.SQLITE_BUSY;
563
+ if (!(await this.#lock(file, 'reserved', POLL_EXCLUSIVE))) {
564
+ file.locks.hint()
565
+ return VFS.SQLITE_BUSY
568
566
  }
569
567
 
570
568
  // In order to write, our view of the database must be up to date.
571
569
  // To check this, first fetch all transactions in IndexedDB equal to
572
570
  // or greater than our view.
573
- const tx = file.idb.transaction(['pending']);
574
- const range = IDBKeyRange.lowerBound(file.viewTx.txId);
571
+ const tx = file.idb.transaction(['pending'])
572
+ const range = IDBKeyRange.lowerBound(file.viewTx.txId)
575
573
 
576
574
  /** @type {Transaction[]} */
577
- const entries = await idbX(tx.objectStore('pending').getAll(range));
575
+ const entries = await idbX(tx.objectStore('pending').getAll(range))
578
576
 
579
577
  // Ideally the fetched list of transactions should contain one
580
578
  // entry matching our view. If not then our view is out of date.
@@ -582,68 +580,68 @@ export class OPFSPermutedVFS extends FacadeVFS {
582
580
  // There are newer transactions in IndexedDB that we haven't
583
581
  // seen via broadcast. Ensure that they are incorporated on unlock,
584
582
  // and force the application to retry.
585
- file.broadcastReceived.push(...entries);
586
- file.locks.reserved();
583
+ file.broadcastReceived.push(...entries)
584
+ file.locks.reserved()
587
585
  return VFS.SQLITE_BUSY
588
586
  }
589
- break;
587
+ break
590
588
  case VFS.SQLITE_LOCK_EXCLUSIVE:
591
- await this.#lock(file, 'write');
592
- break;
589
+ await this.#lock(file, 'write')
590
+ break
593
591
  }
594
- file.lockState = lockType;
595
- return VFS.SQLITE_OK;
592
+ file.lockState = lockType
593
+ return VFS.SQLITE_OK
596
594
  }
597
595
 
598
596
  /**
599
- * @param {number} fileId
600
- * @param {number} lockType
597
+ * @param {number} fileId
598
+ * @param {number} lockType
601
599
  * @returns {number}
602
600
  */
603
601
  jUnlock(fileId, lockType) {
604
- const file = this.#mapIdToFile.get(fileId);
605
- if (lockType >= file.lockState) return VFS.SQLITE_OK;
602
+ const file = this.#mapIdToFile.get(fileId)
603
+ if (lockType >= file.lockState) return VFS.SQLITE_OK
606
604
  switch (lockType) {
607
605
  case VFS.SQLITE_LOCK_SHARED:
608
- file.locks.write?.();
609
- file.locks.reserved?.();
610
- file.locks.hint?.();
611
- break;
606
+ file.locks.write?.()
607
+ file.locks.reserved?.()
608
+ file.locks.hint?.()
609
+ break
612
610
  case VFS.SQLITE_LOCK_NONE:
613
611
  // Don't release the read lock here. It will be released on demand
614
612
  // when a broadcast notifies us that another connections wants to
615
613
  // VACUUM.
616
- this.#processBroadcasts(file);
617
- file.locks.write?.();
618
- file.locks.reserved?.();
619
- file.locks.hint?.();
620
- break;
614
+ this.#processBroadcasts(file)
615
+ file.locks.write?.()
616
+ file.locks.reserved?.()
617
+ file.locks.hint?.()
618
+ break
621
619
  }
622
- file.lockState = lockType;
623
- return VFS.SQLITE_OK;
620
+ file.lockState = lockType
621
+ return VFS.SQLITE_OK
624
622
  }
625
623
 
626
624
  /**
627
625
  * @param {number} fileId
628
- * @param {DataView} pResOut
626
+ * @param {DataView} pResOut
629
627
  * @returns {Promise<number>}
630
628
  */
631
629
  async jCheckReservedLock(fileId, pResOut) {
632
630
  try {
633
- const file = this.#mapIdToFile.get(fileId);
631
+ const file = this.#mapIdToFile.get(fileId)
634
632
  if (await this.#lock(file, 'reserved', POLL_SHARED)) {
635
633
  // This looks backwards, but if we get the lock then no one
636
634
  // else had it.
637
- pResOut.setInt32(0, 0, true);
638
- file.locks.reserved();
635
+ pResOut.setInt32(0, 0, true)
636
+ file.locks.reserved()
639
637
  } else {
640
- pResOut.setInt32(0, 1, true);
638
+ pResOut.setInt32(0, 1, true)
641
639
  }
642
- return VFS.SQLITE_OK;
640
+ return VFS.SQLITE_OK
643
641
  } catch (e) {
644
- console.error(e);
645
- this.lastError = e;
646
- return VFS.SQLITE_IOERR_LOCK;
642
+ console.error(e)
643
+ this.lastError = e
644
+ return VFS.SQLITE_IOERR_LOCK
647
645
  }
648
646
  }
649
647
 
@@ -655,19 +653,19 @@ export class OPFSPermutedVFS extends FacadeVFS {
655
653
  */
656
654
  async jFileControl(fileId, op, pArg) {
657
655
  try {
658
- const file = this.#mapIdToFile.get(fileId);
656
+ const file = this.#mapIdToFile.get(fileId)
659
657
  switch (op) {
660
658
  case VFS.SQLITE_FCNTL_PRAGMA:
661
- const key = cvtString(pArg, 4);
662
- const value = cvtString(pArg, 8);
663
- this.log?.('xFileControl', file.path, 'PRAGMA', key, value);
659
+ const key = cvtString(pArg, 4)
660
+ const value = cvtString(pArg, 8)
661
+ this.log?.('xFileControl', file.path, 'PRAGMA', key, value)
664
662
  switch (key.toLowerCase()) {
665
663
  case 'page_size':
666
664
  // Don't allow changing the page size.
667
665
  if (value && file.pageSize && Number(value) !== file.pageSize) {
668
- return VFS.SQLITE_ERROR;
666
+ return VFS.SQLITE_ERROR
669
667
  }
670
- break;
668
+ break
671
669
  case 'synchronous':
672
670
  // This VFS only recognizes 'full' and not 'full'.
673
671
  if (value) {
@@ -676,71 +674,69 @@ export class OPFSPermutedVFS extends FacadeVFS {
676
674
  case '2':
677
675
  case 'extra':
678
676
  case '3':
679
- file.synchronous = 'full';
680
- break;
677
+ file.synchronous = 'full'
678
+ break
681
679
  default:
682
- file.synchronous = 'normal';
683
- break;
680
+ file.synchronous = 'normal'
681
+ break
684
682
  }
685
683
  }
686
- break;
684
+ break
687
685
  case 'flush_interval':
688
686
  if (value) {
689
- const interval = Number(value);
687
+ const interval = Number(value)
690
688
  if (interval > 0) {
691
- file.flushInterval = Number(value);
689
+ file.flushInterval = Number(value)
692
690
  } else {
693
- return VFS.SQLITE_ERROR;
691
+ return VFS.SQLITE_ERROR
694
692
  }
695
693
  } else {
696
694
  // Report current value.
697
- const buffer = new TextEncoder().encode(file.flushInterval.toString());
698
- const s = this._module._sqlite3_malloc64(buffer.byteLength + 1);
699
- new Uint8Array(this._module.HEAPU8.buffer, s, buffer.byteLength + 1)
700
- .fill(0)
701
- .set(buffer);
702
-
703
- pArg.setUint32(0, s, true);
704
- return VFS.SQLITE_OK;
695
+ const buffer = new TextEncoder().encode(file.flushInterval.toString())
696
+ const s = this._module._sqlite3_malloc64(buffer.byteLength + 1)
697
+ new Uint8Array(this._module.HEAPU8.buffer, s, buffer.byteLength + 1).fill(0).set(buffer)
698
+
699
+ pArg.setUint32(0, s, true)
700
+ return VFS.SQLITE_OK
705
701
  }
706
- break;
702
+ break
707
703
  case 'write_hint':
708
- return this.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null);
709
- }
710
- break;
704
+ return this.jFileControl(fileId, WebLocksMixin.WRITE_HINT_OP_CODE, null)
705
+ }
706
+ break
711
707
  case VFS.SQLITE_FCNTL_BEGIN_ATOMIC_WRITE:
712
- this.log?.('xFileControl', 'BEGIN_ATOMIC_WRITE', file.path);
713
- return VFS.SQLITE_OK;
708
+ this.log?.('xFileControl', 'BEGIN_ATOMIC_WRITE', file.path)
709
+ return VFS.SQLITE_OK
714
710
  case VFS.SQLITE_FCNTL_COMMIT_ATOMIC_WRITE:
715
- this.log?.('xFileControl', 'COMMIT_ATOMIC_WRITE', file.path);
716
- return VFS.SQLITE_OK;
711
+ this.log?.('xFileControl', 'COMMIT_ATOMIC_WRITE', file.path)
712
+ return VFS.SQLITE_OK
717
713
  case VFS.SQLITE_FCNTL_ROLLBACK_ATOMIC_WRITE:
718
- this.log?.('xFileControl', 'ROLLBACK_ATOMIC_WRITE', file.path);
719
- this.#rollbackTx(file);
720
- return VFS.SQLITE_OK;
714
+ this.log?.('xFileControl', 'ROLLBACK_ATOMIC_WRITE', file.path)
715
+ this.#rollbackTx(file)
716
+ return VFS.SQLITE_OK
721
717
  case VFS.SQLITE_FCNTL_OVERWRITE:
722
718
  // This is a VACUUM.
723
- this.log?.('xFileControl', 'OVERWRITE', file.path);
724
- await this.#prepareOverwrite(file);
725
- break;
719
+ this.log?.('xFileControl', 'OVERWRITE', file.path)
720
+ await this.#prepareOverwrite(file)
721
+ break
726
722
  case VFS.SQLITE_FCNTL_COMMIT_PHASETWO:
727
723
  // Finish any transaction. Note that the transaction may not
728
724
  // exist if there is a BEGIN IMMEDIATE...COMMIT block that
729
725
  // does not actually call xWrite.
730
- this.log?.('xFileControl', 'COMMIT_PHASETWO', file.path);
726
+ this.log?.('xFileControl', 'COMMIT_PHASETWO', file.path)
731
727
  if (file.txActive) {
732
- await this.#commitTx(file);
728
+ await this.#commitTx(file)
733
729
  }
734
- break;
730
+ break
735
731
  case WebLocksMixin.WRITE_HINT_OP_CODE:
736
- file.txWriteHint = true;
737
- break;
732
+ file.txWriteHint = true
733
+ break
738
734
  }
739
735
  } catch (e) {
740
- this.#lastError = e;
741
- return VFS.SQLITE_IOERR;
736
+ this.#lastError = e
737
+ return VFS.SQLITE_IOERR
742
738
  }
743
- return VFS.SQLITE_NOTFOUND;
739
+ return VFS.SQLITE_NOTFOUND
744
740
  }
745
741
 
746
742
  /**
@@ -748,109 +744,109 @@ export class OPFSPermutedVFS extends FacadeVFS {
748
744
  * @returns {number|Promise<number>}
749
745
  */
750
746
  jDeviceCharacteristics(fileId) {
751
- return 0
752
- | VFS.SQLITE_IOCAP_BATCH_ATOMIC
753
- | VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN;
747
+ return 0 | VFS.SQLITE_IOCAP_BATCH_ATOMIC | VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
754
748
  }
755
749
 
756
750
  /**
757
- * @param {Uint8Array} zBuf
751
+ * @param {Uint8Array} zBuf
758
752
  * @returns {number}
759
753
  */
760
754
  jGetLastError(zBuf) {
761
755
  if (this.#lastError) {
762
- console.error(this.#lastError);
763
- const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
764
- const { written } = new TextEncoder().encodeInto(this.#lastError.message, outputArray);
765
- zBuf[written] = 0;
756
+ console.error(this.#lastError)
757
+ const outputArray = zBuf.subarray(0, zBuf.byteLength - 1)
758
+ const { written } = new TextEncoder().encodeInto(this.#lastError.message, outputArray)
759
+ zBuf[written] = 0
766
760
  }
767
761
  return VFS.SQLITE_OK
768
762
  }
769
763
 
770
764
  /**
771
765
  * Return the database page size, or 0 if not yet known.
772
- * @param {File} file
766
+ * @param {File} file
773
767
  * @returns {number}
774
768
  */
775
769
  #getPageSize(file) {
776
770
  // Offset 0 will always contain a page 1. Even if it is out of
777
771
  // date it will have a valid page size.
778
772
  // https://sqlite.org/fileformat.html#page_size
779
- const header = new DataView(new ArrayBuffer(2));
780
- const n = file.accessHandle.read(header, { at: 16 });
781
- if (n !== header.byteLength) return 0;
782
- const pageSize = header.getUint16(0);
773
+ const header = new DataView(new ArrayBuffer(2))
774
+ const n = file.accessHandle.read(header, { at: 16 })
775
+ if (n !== header.byteLength) return 0
776
+ const pageSize = header.getUint16(0)
783
777
  switch (pageSize) {
784
778
  case 1:
785
- return 65536;
779
+ return 65536
786
780
  default:
787
- return pageSize;
781
+ return pageSize
788
782
  }
789
783
  }
790
784
 
791
785
  /**
792
786
  * Acquire one of the database file internal Web Locks.
793
- * @param {File} file
794
- * @param {'read'|'write'|'reserved'|'hint'} name
795
- * @param {LockOptions} options
787
+ * @param {File} file
788
+ * @param {'read'|'write'|'reserved'|'hint'} name
789
+ * @param {LockOptions} options
796
790
  * @returns {Promise<boolean>}
797
791
  */
798
792
  #lock(file, name, options = {}) {
799
- return new Promise(resolve => {
800
- const lockName = `${file.path}@@${name}`;
801
- navigator.locks.request(lockName, options, lock => {
802
- if (lock) {
803
- return new Promise(release => {
804
- file.locks[name] = () => {
805
- release();
806
- file.locks[name] = null;
807
- };
808
- resolve(true);
809
- });
810
- } else {
811
- file.locks[name] = null;
812
- resolve(false);
813
- }
814
- }).catch(e => {
815
- if (e.name !== 'AbortError') throw e;
816
- });
817
- });
793
+ return new Promise((resolve) => {
794
+ const lockName = `${file.path}@@${name}`
795
+ navigator.locks
796
+ .request(lockName, options, (lock) => {
797
+ if (lock) {
798
+ return new Promise((release) => {
799
+ file.locks[name] = () => {
800
+ release()
801
+ file.locks[name] = null
802
+ }
803
+ resolve(true)
804
+ })
805
+ } else {
806
+ file.locks[name] = null
807
+ resolve(false)
808
+ }
809
+ })
810
+ .catch((e) => {
811
+ if (e.name !== 'AbortError') throw e
812
+ })
813
+ })
818
814
  }
819
815
 
820
816
  /**
821
- * @param {File} file
822
- * @param {Transaction} tx
817
+ * @param {File} file
818
+ * @param {Transaction} tx
823
819
  */
824
820
  async #setView(file, tx) {
825
821
  // Publish our view of the database with a lock name that includes
826
822
  // the transaction id. As long as we hold the lock, no other connection
827
823
  // will overwrite data we are using.
828
- file.viewTx = tx;
829
- const lockName = `${file.path}@@[${tx.txId}]`;
830
- const newReleaser = await new Promise(resolve => {
831
- navigator.locks.request(lockName, SHARED, lock => {
832
- return new Promise(release => {
833
- resolve(release);
834
- });
835
- });
836
- });
824
+ file.viewTx = tx
825
+ const lockName = `${file.path}@@[${tx.txId}]`
826
+ const newReleaser = await new Promise((resolve) => {
827
+ navigator.locks.request(lockName, SHARED, (lock) => {
828
+ return new Promise((release) => {
829
+ resolve(release)
830
+ })
831
+ })
832
+ })
837
833
 
838
834
  // The new lock is acquired so release the old one.
839
- file.viewReleaser?.();
840
- file.viewReleaser = newReleaser;
835
+ file.viewReleaser?.()
836
+ file.viewReleaser = newReleaser
841
837
  }
842
838
 
843
839
  /**
844
840
  * Handle prevously received messages from other connections.
845
- * @param {File} file
841
+ * @param {File} file
846
842
  */
847
843
  #processBroadcasts(file) {
848
844
  // Sort transaction messages by id. Move other messages to the front.
849
845
  // @ts-ignore
850
- file.broadcastReceived.sort((a, b) => (a.txId ?? -1) - (b.txId ?? -1));
846
+ file.broadcastReceived.sort((a, b) => (a.txId ?? -1) - (b.txId ?? -1))
851
847
 
852
- let nHandled = 0;
853
- let newTx = file.viewTx;
848
+ let nHandled = 0
849
+ let newTx = file.viewTx
854
850
  for (const message of file.broadcastReceived) {
855
851
  if (Object.hasOwn(message, 'txId')) {
856
852
  const messageTx = /** @type {Transaction} */ (message)
@@ -858,280 +854,276 @@ export class OPFSPermutedVFS extends FacadeVFS {
858
854
  // This transaction is already incorporated into our view.
859
855
  } else if (messageTx.txId === newTx.txId + 1) {
860
856
  // This is the next expected transaction.
861
- this.log?.(`accept tx ${messageTx.txId}`);
862
- this.#acceptTx(file, messageTx);
863
- newTx = messageTx;
857
+ this.log?.(`accept tx ${messageTx.txId}`)
858
+ this.#acceptTx(file, messageTx)
859
+ newTx = messageTx
864
860
  } else {
865
861
  // There is a gap in the transaction sequence.
866
- console.warn(`missing tx ${newTx.txId + 1} (got ${messageTx.txId})`);
867
- break;
862
+ console.warn(`missing tx ${newTx.txId + 1} (got ${messageTx.txId})`)
863
+ break
868
864
  }
869
865
  } else if (Object.hasOwn(message, 'exclusive')) {
870
866
  // Release the read lock if we have it.
871
- this.log?.('releasing read lock');
872
- console.assert(file.lockState === VFS.SQLITE_LOCK_NONE);
873
- file.locks.read?.();
867
+ this.log?.('releasing read lock')
868
+ console.assert(file.lockState === VFS.SQLITE_LOCK_NONE)
869
+ file.locks.read?.()
874
870
  }
875
- nHandled++;
871
+ nHandled++
876
872
  }
877
873
 
878
874
  // Remove handled messages from the list.
879
- file.broadcastReceived.splice(0, nHandled);
875
+ file.broadcastReceived.splice(0, nHandled)
880
876
 
881
877
  // Tell other connections about a change in our view.
882
878
  if (newTx.txId > file.viewTx.txId) {
883
879
  // No need to await here.
884
- this.#setView(file, newTx);
880
+ this.#setView(file, newTx)
885
881
  }
886
882
  }
887
883
 
888
884
  /**
889
- * @param {File} file
890
- * @param {Transaction} message
885
+ * @param {File} file
886
+ * @param {Transaction} message
891
887
  */
892
888
  #acceptTx(file, message) {
893
- file.pageSize = file.pageSize || this.#getPageSize(file);
889
+ file.pageSize = file.pageSize || this.#getPageSize(file)
894
890
 
895
891
  // Add list of pages made obsolete by this transaction. These pages
896
892
  // can be moved to the free list when all connections have reached
897
893
  // this point.
898
- message.reclaimable = [];
894
+ message.reclaimable = []
899
895
 
900
896
  // Update page mapping with transaction pages.
901
897
  for (const [index, { offset }] of message.pages) {
902
898
  if (file.mapPageToOffset.has(index)) {
903
899
  // Remember overwritten pages that can be reused when all
904
900
  // connections have seen this transaction.
905
- message.reclaimable.push(file.mapPageToOffset.get(index));
901
+ message.reclaimable.push(file.mapPageToOffset.get(index))
906
902
  }
907
- file.mapPageToOffset.set(index, offset);
908
- file.freeOffsets.delete(offset);
903
+ file.mapPageToOffset.set(index, offset)
904
+ file.freeOffsets.delete(offset)
909
905
  }
910
906
 
911
907
  // Remove mappings for truncated pages.
912
- const oldPageCount = file.fileSize / file.pageSize;
913
- const newPageCount = message.fileSize / file.pageSize;
908
+ const oldPageCount = file.fileSize / file.pageSize
909
+ const newPageCount = message.fileSize / file.pageSize
914
910
  for (let index = newPageCount + 1; index <= oldPageCount; index++) {
915
- message.reclaimable.push(file.mapPageToOffset.get(index));
916
- file.mapPageToOffset.delete(index);
911
+ message.reclaimable.push(file.mapPageToOffset.get(index))
912
+ file.mapPageToOffset.delete(index)
917
913
  }
918
914
 
919
- file.fileSize = message.fileSize;
920
- file.mapTxToPending.set(message.txId, message);
915
+ file.fileSize = message.fileSize
916
+ file.mapTxToPending.set(message.txId, message)
921
917
  if (message.oldestTxId) {
922
918
  // Finalize pending transactions that are no longer needed.
923
919
  for (const tx of file.mapTxToPending.values()) {
924
- if (tx.txId > message.oldestTxId) break;
920
+ if (tx.txId > message.oldestTxId) break
925
921
 
926
922
  // Return no longer referenced pages to the free list.
927
923
  for (const offset of tx.reclaimable) {
928
- this.log?.(`reclaim offset ${offset}`);
929
- file.freeOffsets.add(offset);
924
+ this.log?.(`reclaim offset ${offset}`)
925
+ file.freeOffsets.add(offset)
930
926
  }
931
- file.mapTxToPending.delete(tx.txId);
927
+ file.mapTxToPending.delete(tx.txId)
932
928
  }
933
929
  }
934
930
  }
935
931
 
936
932
  /**
937
- * @param {File} file
933
+ * @param {File} file
938
934
  */
939
935
  #beginTx(file) {
940
936
  // Start a new transaction.
941
937
  file.txActive = {
942
938
  txId: file.viewTx.txId + 1,
943
939
  pages: new Map(),
944
- fileSize: file.fileSize
945
- };
946
- file.txRealFileSize = file.accessHandle.getSize();
947
- this.log?.(`begin transaction ${file.txActive.txId}`);
940
+ fileSize: file.fileSize,
941
+ }
942
+ file.txRealFileSize = file.accessHandle.getSize()
943
+ this.log?.(`begin transaction ${file.txActive.txId}`)
948
944
  }
949
945
 
950
946
  /**
951
- * @param {File} file
947
+ * @param {File} file
952
948
  */
953
949
  async #commitTx(file) {
954
950
  // Determine whether to finalize pending transactions, i.e. transfer
955
951
  // them to the IndexedDB pages store.
956
- if (file.synchronous === 'full' ||
957
- file.txIsOverwrite ||
958
- (file.txActive.txId % file.flushInterval) === 0) {
959
- file.txActive.oldestTxId = await this.#getOldestTxInUse(file);
952
+ if (file.synchronous === 'full' || file.txIsOverwrite || file.txActive.txId % file.flushInterval === 0) {
953
+ file.txActive.oldestTxId = await this.#getOldestTxInUse(file)
960
954
  }
961
955
 
962
- const tx = file.idb.transaction(
963
- ['pages', 'pending'],
964
- 'readwrite',
965
- { durability: file.synchronous === 'full' ? 'strict' : 'relaxed'});
956
+ const tx = file.idb.transaction(['pages', 'pending'], 'readwrite', {
957
+ durability: file.synchronous === 'full' ? 'strict' : 'relaxed',
958
+ })
966
959
 
967
960
  if (file.txActive.oldestTxId) {
968
961
  // Ensure that all pending data is safely on storage.
969
962
  if (file.txIsOverwrite) {
970
- file.accessHandle.truncate(file.txActive.fileSize);
963
+ file.accessHandle.truncate(file.txActive.fileSize)
971
964
  }
972
- file.accessHandle.flush();
973
-
965
+ file.accessHandle.flush()
966
+
974
967
  // Transfer page mappings to the pages store for all pending
975
968
  // transactions that are no longer in use.
976
- const pageStore = tx.objectStore('pages');
969
+ const pageStore = tx.objectStore('pages')
977
970
  for (const tx of file.mapTxToPending.values()) {
978
- if (tx.txId > file.txActive.oldestTxId) break;
971
+ if (tx.txId > file.txActive.oldestTxId) break
979
972
 
980
973
  for (const [index, { offset }] of tx.pages) {
981
- pageStore.put({ i: index, o: offset });
974
+ pageStore.put({ i: index, o: offset })
982
975
  }
983
976
  }
984
977
 
985
978
  // Delete pending store entries that are no longer needed.
986
- tx.objectStore('pending')
987
- .delete(IDBKeyRange.upperBound(file.txActive.oldestTxId));
979
+ tx.objectStore('pending').delete(IDBKeyRange.upperBound(file.txActive.oldestTxId))
988
980
  }
989
981
 
990
982
  // Publish the transaction via broadcast and IndexedDB.
991
- this.log?.(`commit transaction ${file.txActive.txId}`);
992
- tx.objectStore('pending').put(file.txActive);
983
+ this.log?.(`commit transaction ${file.txActive.txId}`)
984
+ tx.objectStore('pending').put(file.txActive)
993
985
 
994
986
  const txComplete = new Promise((resolve, reject) => {
995
- const message = file.txActive;
987
+ const message = file.txActive
996
988
  tx.oncomplete = () => {
997
- file.broadcastChannel.postMessage(message);
998
- resolve();
999
- };
989
+ file.broadcastChannel.postMessage(message)
990
+ resolve()
991
+ }
1000
992
  tx.onabort = () => {
1001
- file.abortController.abort();
1002
- reject(tx.error);
1003
- };
1004
- tx.commit();
1005
- });
993
+ file.abortController.abort()
994
+ reject(tx.error)
995
+ }
996
+ tx.commit()
997
+ })
1006
998
 
1007
999
  if (file.synchronous === 'full') {
1008
- await txComplete;
1000
+ await txComplete
1009
1001
  }
1010
1002
 
1011
1003
  // Advance our own view. Even if we received our own broadcasts (we
1012
1004
  // don't), we want our view to be updated synchronously.
1013
- this.#acceptTx(file, file.txActive);
1014
- this.#setView(file, file.txActive);
1015
- file.txActive = null;
1016
- file.txWriteHint = false;
1005
+ this.#acceptTx(file, file.txActive)
1006
+ this.#setView(file, file.txActive)
1007
+ file.txActive = null
1008
+ file.txWriteHint = false
1017
1009
 
1018
1010
  if (file.txIsOverwrite) {
1019
1011
  // Wait until all connections have seen the transaction.
1020
- while (file.viewTx.txId !== await this.#getOldestTxInUse(file)) {
1021
- await new Promise(resolve => setTimeout(resolve, 10));
1012
+ while (file.viewTx.txId !== (await this.#getOldestTxInUse(file))) {
1013
+ await new Promise((resolve) => setTimeout(resolve, 10))
1022
1014
  }
1023
1015
 
1024
1016
  // Downgrade the exclusive read lock to a shared lock.
1025
- file.locks.read();
1026
- await this.#lock(file, 'read', SHARED);
1017
+ file.locks.read()
1018
+ await this.#lock(file, 'read', SHARED)
1027
1019
 
1028
1020
  // There should be no extra space in the file now.
1029
- file.freeOffsets.clear();
1021
+ file.freeOffsets.clear()
1030
1022
 
1031
- file.txIsOverwrite = false;
1023
+ file.txIsOverwrite = false
1032
1024
  }
1033
1025
  }
1034
1026
 
1035
1027
  /**
1036
- * @param {File} file
1028
+ * @param {File} file
1037
1029
  */
1038
1030
  #rollbackTx(file) {
1039
1031
  // Return offsets to the free list.
1040
- this.log?.(`rollback transaction ${file.txActive.txId}`);
1032
+ this.log?.(`rollback transaction ${file.txActive.txId}`)
1041
1033
  for (const { offset } of file.txActive.pages.values()) {
1042
- file.freeOffsets.add(offset);
1034
+ file.freeOffsets.add(offset)
1043
1035
  }
1044
- file.txActive = null;
1045
- file.txWriteHint = false;
1036
+ file.txActive = null
1037
+ file.txWriteHint = false
1046
1038
  }
1047
1039
 
1048
1040
  /**
1049
- * @param {File} file
1041
+ * @param {File} file
1050
1042
  */
1051
1043
  async #prepareOverwrite(file) {
1052
1044
  // Get an exclusive read lock to prevent other connections from
1053
1045
  // seeing the database in an inconsistent state.
1054
- file.locks.read?.();
1055
- if (!await this.#lock(file, 'read', POLL_EXCLUSIVE)) {
1046
+ file.locks.read?.()
1047
+ if (!(await this.#lock(file, 'read', POLL_EXCLUSIVE))) {
1056
1048
  // We didn't get the read lock because other connections have
1057
1049
  // it. Notify them that we want the lock and wait.
1058
- const lockRequest = this.#lock(file, 'read');
1059
- file.broadcastChannel.postMessage({ exclusive: true });
1060
- await lockRequest;
1050
+ const lockRequest = this.#lock(file, 'read')
1051
+ file.broadcastChannel.postMessage({ exclusive: true })
1052
+ await lockRequest
1061
1053
  }
1062
1054
 
1063
1055
  // Create a intermediate transaction to copy all current page data to
1064
- // an offset past fileSize.
1056
+ // an offset past fileSize.
1065
1057
  file.txActive = {
1066
1058
  txId: file.viewTx.txId + 1,
1067
1059
  pages: new Map(),
1068
- fileSize: file.fileSize
1069
- };
1060
+ fileSize: file.fileSize,
1061
+ }
1070
1062
 
1071
1063
  // This helper generator provides offsets above fileSize.
1072
- const offsetGenerator = (function*() {
1064
+ const offsetGenerator = (function* () {
1073
1065
  for (const offset of file.freeOffsets) {
1074
1066
  if (offset >= file.fileSize) {
1075
- yield offset;
1067
+ yield offset
1076
1068
  }
1077
1069
  }
1078
1070
 
1079
1071
  while (true) {
1080
- yield file.accessHandle.getSize();
1072
+ yield file.accessHandle.getSize()
1081
1073
  }
1082
- })();
1074
+ })()
1083
1075
 
1084
- const pageBuffer = new Uint8Array(file.pageSize);
1076
+ const pageBuffer = new Uint8Array(file.pageSize)
1085
1077
  for (let offset = 0; offset < file.fileSize; offset += file.pageSize) {
1086
- const pageIndex = offset / file.pageSize + 1;
1087
- const oldOffset = file.mapPageToOffset.get(pageIndex);
1078
+ const pageIndex = offset / file.pageSize + 1
1079
+ const oldOffset = file.mapPageToOffset.get(pageIndex)
1088
1080
  if (oldOffset < file.fileSize) {
1089
1081
  // This page is stored below fileSize. Read it into memory.
1090
1082
  if (file.accessHandle.read(pageBuffer, { at: oldOffset }) !== file.pageSize) {
1091
- throw new Error('Failed to read page');
1083
+ throw new Error('Failed to read page')
1092
1084
  }
1093
-
1085
+
1094
1086
  // Perform the copy.
1095
- const newOffset = offsetGenerator.next().value;
1087
+ const newOffset = offsetGenerator.next().value
1096
1088
  if (file.accessHandle.write(pageBuffer, { at: newOffset }) !== file.pageSize) {
1097
- throw new Error('Failed to write page');
1089
+ throw new Error('Failed to write page')
1098
1090
  }
1099
1091
 
1100
1092
  file.txActive.pages.set(pageIndex, {
1101
1093
  offset: newOffset,
1102
- digest: checksum(pageBuffer)
1103
- });
1094
+ digest: checksum(pageBuffer),
1095
+ })
1104
1096
  }
1105
1097
  }
1106
- file.accessHandle.flush();
1107
- file.freeOffsets.clear();
1108
-
1098
+ file.accessHandle.flush()
1099
+ file.freeOffsets.clear()
1100
+
1109
1101
  // Publish transaction for others.
1110
- file.broadcastChannel.postMessage(file.txActive);
1111
- const tx = file.idb.transaction('pending', 'readwrite');
1102
+ file.broadcastChannel.postMessage(file.txActive)
1103
+ const tx = file.idb.transaction('pending', 'readwrite')
1112
1104
  const txComplete = new Promise((resolve, reject) => {
1113
- tx.oncomplete = resolve;
1114
- tx.onabort = () => reject(tx.error);
1115
- });
1116
- tx.objectStore('pending').put(file.txActive);
1117
- tx.commit();
1118
- await txComplete;
1105
+ tx.oncomplete = resolve
1106
+ tx.onabort = () => reject(tx.error)
1107
+ })
1108
+ tx.objectStore('pending').put(file.txActive)
1109
+ tx.commit()
1110
+ await txComplete
1119
1111
 
1120
1112
  // Incorporate the transaction into our view.
1121
- this.#acceptTx(file, file.txActive);
1122
- this.#setView(file, file.txActive);
1123
- file.txActive = null;
1113
+ this.#acceptTx(file, file.txActive)
1114
+ this.#setView(file, file.txActive)
1115
+ file.txActive = null
1124
1116
 
1125
1117
  // Now all pages are in the file above fileSize. The VACUUM operation
1126
1118
  // will now copy the pages below fileSize in the proper order. After
1127
1119
  // that once all connections are up to date the file can be truncated.
1128
1120
 
1129
1121
  // This flag tells xWrite to write pages at their canonical offset.
1130
- file.txIsOverwrite = true;
1122
+ file.txIsOverwrite = true
1131
1123
  }
1132
1124
 
1133
1125
  /**
1134
- * @param {File} file
1126
+ * @param {File} file
1135
1127
  * @returns {Promise<number>}
1136
1128
  */
1137
1129
  async #getOldestTxInUse(file) {
@@ -1139,79 +1131,76 @@ export class OPFSPermutedVFS extends FacadeVFS {
1139
1131
  // the latest transaction it knows about. We can find the oldest
1140
1132
  // transaction by listing the those locks and extracting the earliest
1141
1133
  // transaction id.
1142
- const TX_LOCK_REGEX = /^(.*)@@\[(\d+)\]$/;
1143
- let oldestTxId = file.viewTx.txId;
1144
- const locks = await navigator.locks.query();
1134
+ const TX_LOCK_REGEX = /^(.*)@@\[(\d+)\]$/
1135
+ let oldestTxId = file.viewTx.txId
1136
+ const locks = await navigator.locks.query()
1145
1137
  for (const { name } of locks.held) {
1146
- const m = TX_LOCK_REGEX.exec(name);
1138
+ const m = TX_LOCK_REGEX.exec(name)
1147
1139
  if (m && m[1] === file.path) {
1148
- oldestTxId = Math.min(oldestTxId, Number(m[2]));
1140
+ oldestTxId = Math.min(oldestTxId, Number(m[2]))
1149
1141
  }
1150
1142
  }
1151
- return oldestTxId;
1143
+ return oldestTxId
1152
1144
  }
1153
1145
  }
1154
1146
 
1155
1147
  /**
1156
1148
  * Wrap IndexedDB request with a Promise.
1157
- * @param {IDBRequest} request
1158
- * @returns
1149
+ * @param {IDBRequest} request
1150
+ * @returns
1159
1151
  */
1160
1152
  function idbX(request) {
1161
1153
  return new Promise((resolve, reject) => {
1162
- request.onsuccess = () => resolve(request.result);
1163
- request.onerror = () => reject(request.error);
1164
- });
1154
+ request.onsuccess = () => resolve(request.result)
1155
+ request.onerror = () => reject(request.error)
1156
+ })
1165
1157
  }
1166
1158
 
1167
1159
  /**
1168
1160
  * Given a path, return the directory handle and filename.
1169
- * @param {string} path
1170
- * @param {boolean} create
1161
+ * @param {string} path
1162
+ * @param {boolean} create
1171
1163
  * @returns {Promise<[FileSystemDirectoryHandle, string]>}
1172
1164
  */
1173
1165
  async function getPathComponents(path, create) {
1174
- const components = path.split('/');
1175
- const filename = components.pop();
1176
- let directory = await navigator.storage.getDirectory();
1177
- for (const component of components.filter(s => s)) {
1178
- directory = await directory.getDirectoryHandle(component, { create });
1166
+ const components = path.split('/')
1167
+ const filename = components.pop()
1168
+ let directory = await navigator.storage.getDirectory()
1169
+ for (const component of components.filter((s) => s)) {
1170
+ directory = await directory.getDirectoryHandle(component, { create })
1179
1171
  }
1180
- return [directory, filename];
1172
+ return [directory, filename]
1181
1173
  }
1182
1174
 
1183
1175
  /**
1184
1176
  * Extract a C string from WebAssembly memory.
1185
- * @param {DataView} dataView
1186
- * @param {number} offset
1187
- * @returns
1177
+ * @param {DataView} dataView
1178
+ * @param {number} offset
1179
+ * @returns
1188
1180
  */
1189
1181
  function cvtString(dataView, offset) {
1190
- const p = dataView.getUint32(offset, true);
1182
+ const p = dataView.getUint32(offset, true)
1191
1183
  if (p) {
1192
- const chars = new Uint8Array(dataView.buffer, p);
1193
- return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)));
1184
+ const chars = new Uint8Array(dataView.buffer, p)
1185
+ return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)))
1194
1186
  }
1195
- return null;
1187
+ return null
1196
1188
  }
1197
1189
 
1198
1190
  /**
1199
1191
  * Compute a page checksum.
1200
- * @param {ArrayBufferView} data
1192
+ * @param {ArrayBufferView} data
1201
1193
  * @returns {Uint32Array}
1202
1194
  */
1203
1195
  function checksum(data) {
1204
- const array = new Uint32Array(
1205
- data.buffer,
1206
- data.byteOffset,
1207
- data.byteLength / Uint32Array.BYTES_PER_ELEMENT);
1196
+ const array = new Uint32Array(data.buffer, data.byteOffset, data.byteLength / Uint32Array.BYTES_PER_ELEMENT)
1208
1197
 
1209
1198
  // https://en.wikipedia.org/wiki/Fletcher%27s_checksum
1210
- let h1 = 0;
1211
- let h2 = 0;
1199
+ let h1 = 0
1200
+ let h2 = 0
1212
1201
  for (const value of array) {
1213
- h1 = (h1 + value) % 4294967295;
1214
- h2 = (h2 + h1) % 4294967295;
1202
+ h1 = (h1 + value) % 4294967295
1203
+ h2 = (h2 + h1) % 4294967295
1215
1204
  }
1216
- return new Uint32Array([h1, h2]);
1217
- }
1205
+ return new Uint32Array([h1, h2])
1206
+ }