@livestore/wa-sqlite 0.4.0-dev.21 → 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,24 +1,24 @@
1
1
  // Copyright 2024 Roy T. Hashimoto. All Rights Reserved.
2
- import { FacadeVFS } from '../FacadeVFS.js';
3
- import * as VFS from '../VFS.js';
2
+ import { FacadeVFS } from '../FacadeVFS.js'
3
+ import * as VFS from '../VFS.js'
4
4
 
5
- const DEFAULT_TEMPORARY_FILES = 10;
6
- const LOCK_NOTIFY_INTERVAL = 1000;
5
+ const DEFAULT_TEMPORARY_FILES = 10
6
+ const LOCK_NOTIFY_INTERVAL = 1000
7
7
 
8
- const DB_RELATED_FILE_SUFFIXES = ['', '-journal', '-wal'];
8
+ const DB_RELATED_FILE_SUFFIXES = ['', '-journal', '-wal']
9
9
 
10
- const finalizationRegistry = new FinalizationRegistry(releaser => releaser());
10
+ const finalizationRegistry = new FinalizationRegistry((releaser) => releaser())
11
11
 
12
12
  class File {
13
13
  /** @type {string} */ path
14
- /** @type {number} */ flags;
15
- /** @type {FileSystemSyncAccessHandle} */ accessHandle;
14
+ /** @type {number} */ flags
15
+ /** @type {FileSystemSyncAccessHandle} */ accessHandle
16
16
 
17
- /** @type {PersistentFile?} */ persistentFile;
17
+ /** @type {PersistentFile?} */ persistentFile
18
18
 
19
19
  constructor(path, flags) {
20
- this.path = path;
21
- this.flags = flags;
20
+ this.path = path
21
+ this.flags = flags
22
22
  }
23
23
  }
24
24
 
@@ -28,423 +28,419 @@ class PersistentFile {
28
28
 
29
29
  // The following properties are for main database files.
30
30
 
31
- /** @type {boolean} */ isLockBusy = false;
32
- /** @type {boolean} */ isFileLocked = false;
33
- /** @type {boolean} */ isRequestInProgress = false;
34
- /** @type {function} */ handleLockReleaser = null;
31
+ /** @type {boolean} */ isLockBusy = false
32
+ /** @type {boolean} */ isFileLocked = false
33
+ /** @type {boolean} */ isRequestInProgress = false
34
+ /** @type {function} */ handleLockReleaser = null
35
35
 
36
- /** @type {BroadcastChannel} */ handleRequestChannel;
37
- /** @type {boolean} */ isHandleRequested = false;
36
+ /** @type {BroadcastChannel} */ handleRequestChannel
37
+ /** @type {boolean} */ isHandleRequested = false
38
38
 
39
39
  constructor(fileHandle) {
40
- this.fileHandle = fileHandle;
40
+ this.fileHandle = fileHandle
41
41
  }
42
42
  }
43
43
 
44
44
  export class OPFSCoopSyncVFS extends FacadeVFS {
45
- /** @type {Map<number, File>} */ mapIdToFile = new Map();
45
+ /** @type {Map<number, File>} */ mapIdToFile = new Map()
46
46
 
47
- lastError = null;
48
- log = null; //function(...args) { console.log(`[${contextName}]`, ...args) };
49
-
50
- /** @type {Map<string, PersistentFile>} */ persistentFiles = new Map();
51
- /** @type {Map<string, FileSystemSyncAccessHandle>} */ boundAccessHandles = new Map();
52
- /** @type {Set<FileSystemSyncAccessHandle>} */ unboundAccessHandles = new Set();
53
- /** @type {Set<string>} */ accessiblePaths = new Set();
54
- releaser = null;
47
+ lastError = null
48
+ log = null //function(...args) { console.log(`[${contextName}]`, ...args) };
55
49
 
56
- static async create(name, module) {
57
- const vfs = new OPFSCoopSyncVFS(name, module);
58
- await Promise.all([
59
- vfs.isReady(),
60
- vfs.#initialize(DEFAULT_TEMPORARY_FILES),
61
- ]);
62
- return vfs;
63
- }
50
+ /** @type {Map<string, PersistentFile>} */ persistentFiles = new Map()
51
+ /** @type {Map<string, FileSystemSyncAccessHandle>} */ boundAccessHandles = new Map()
52
+ /** @type {Set<FileSystemSyncAccessHandle>} */ unboundAccessHandles = new Set()
53
+ /** @type {Set<string>} */ accessiblePaths = new Set()
54
+ releaser = null
64
55
 
65
- constructor(name, module) {
66
- super(name, module);
56
+ static async create(name, module) {
57
+ const vfs = new OPFSCoopSyncVFS(name, module)
58
+ await Promise.all([vfs.isReady(), vfs.#initialize(DEFAULT_TEMPORARY_FILES)])
59
+ return vfs
67
60
  }
68
61
 
69
62
  async #initialize(nTemporaryFiles) {
70
63
  // Delete temporary directories no longer in use.
71
- const root = await navigator.storage.getDirectory();
64
+ const root = await navigator.storage.getDirectory()
72
65
  // @ts-ignore
73
66
  for await (const entry of root.values()) {
74
67
  if (entry.kind === 'directory' && entry.name.startsWith('.ahp-')) {
75
68
  // A lock with the same name as the directory protects it from
76
69
  // being deleted.
77
- await navigator.locks.request(entry.name, { ifAvailable: true }, async lock => {
70
+ await navigator.locks.request(entry.name, { ifAvailable: true }, async (lock) => {
78
71
  if (lock) {
79
- this.log?.(`Deleting temporary directory ${entry.name}`);
80
- await root.removeEntry(entry.name, { recursive: true });
72
+ this.log?.(`Deleting temporary directory ${entry.name}`)
73
+ await root.removeEntry(entry.name, { recursive: true })
81
74
  } else {
82
- this.log?.(`Temporary directory ${entry.name} is in use`);
75
+ this.log?.(`Temporary directory ${entry.name} is in use`)
83
76
  }
84
- });
77
+ })
85
78
  }
86
79
  }
87
80
 
88
81
  // Create our temporary directory.
89
- const tmpDirName = `.ahp-${Math.random().toString(36).slice(2)}`;
90
- this.releaser = await new Promise(resolve => {
82
+ const tmpDirName = `.ahp-${Math.random().toString(36).slice(2)}`
83
+ this.releaser = await new Promise((resolve) => {
91
84
  navigator.locks.request(tmpDirName, () => {
92
- return new Promise(release => {
93
- resolve(release);
94
- });
95
- });
96
- });
97
- finalizationRegistry.register(this, this.releaser);
98
- const tmpDir = await root.getDirectoryHandle(tmpDirName, { create: true });
85
+ return new Promise((release) => {
86
+ resolve(release)
87
+ })
88
+ })
89
+ })
90
+ finalizationRegistry.register(this, this.releaser)
91
+ const tmpDir = await root.getDirectoryHandle(tmpDirName, { create: true })
99
92
 
100
93
  // Populate temporary directory.
101
94
  for (let i = 0; i < nTemporaryFiles; i++) {
102
- const tmpFile = await tmpDir.getFileHandle(`${i}.tmp`, { create: true });
103
- const tmpAccessHandle = await tmpFile.createSyncAccessHandle();
104
- this.unboundAccessHandles.add(tmpAccessHandle);
95
+ const tmpFile = await tmpDir.getFileHandle(`${i}.tmp`, { create: true })
96
+ const tmpAccessHandle = await tmpFile.createSyncAccessHandle()
97
+ this.unboundAccessHandles.add(tmpAccessHandle)
105
98
  }
106
99
  }
107
100
 
108
101
  /**
109
- * @param {string?} zName
110
- * @param {number} fileId
111
- * @param {number} flags
112
- * @param {DataView} pOutFlags
102
+ * @param {string?} zName
103
+ * @param {number} fileId
104
+ * @param {number} flags
105
+ * @param {DataView} pOutFlags
113
106
  * @returns {number}
114
107
  */
115
108
  jOpen(zName, fileId, flags, pOutFlags) {
116
109
  try {
117
- const url = new URL(zName || Math.random().toString(36).slice(2), 'file://');
118
- const path = url.pathname;
110
+ const url = new URL(zName || Math.random().toString(36).slice(2), 'file://')
111
+ const path = url.pathname
119
112
 
120
113
  if (flags & VFS.SQLITE_OPEN_MAIN_DB) {
121
- const persistentFile = this.persistentFiles.get(path);
114
+ const persistentFile = this.persistentFiles.get(path)
122
115
  if (persistentFile?.isRequestInProgress) {
123
116
  // Should not reach here unless SQLite itself retries an open.
124
117
  // Otherwise, asynchronous operations started on a previous
125
118
  // open try should have completed.
126
- return VFS.SQLITE_BUSY;
119
+ return VFS.SQLITE_BUSY
127
120
  } else if (!persistentFile) {
128
121
  // This is the usual starting point for opening a database.
129
122
  // Register a Promise that resolves when the database and related
130
123
  // files are ready to be used.
131
- this.log?.(`creating persistent file for ${path}`);
132
- const create = !!(flags & VFS.SQLITE_OPEN_CREATE);
133
- this._module.retryOps.push((async () => {
134
- try {
135
- // Get the path directory handle.
136
- let dirHandle = await navigator.storage.getDirectory();
137
- const directories = path.split('/').filter(d => d);
138
- const filename = directories.pop();
139
- for (const directory of directories) {
140
- dirHandle = await dirHandle.getDirectoryHandle(directory, { create });
141
- }
142
-
143
- // Get file handles for the database and related files,
144
- // and create persistent file instances.
145
- for (const suffix of DB_RELATED_FILE_SUFFIXES) {
146
- const fileHandle = await dirHandle.getFileHandle(filename + suffix, { create });
147
- await this.#createPersistentFile(fileHandle);
124
+ this.log?.(`creating persistent file for ${path}`)
125
+ const create = !!(flags & VFS.SQLITE_OPEN_CREATE)
126
+ this._module.retryOps.push(
127
+ (async () => {
128
+ try {
129
+ // Get the path directory handle.
130
+ let dirHandle = await navigator.storage.getDirectory()
131
+ const directories = path.split('/').filter((d) => d)
132
+ const filename = directories.pop()
133
+ for (const directory of directories) {
134
+ dirHandle = await dirHandle.getDirectoryHandle(directory, { create })
135
+ }
136
+
137
+ // Get file handles for the database and related files,
138
+ // and create persistent file instances.
139
+ for (const suffix of DB_RELATED_FILE_SUFFIXES) {
140
+ const fileHandle = await dirHandle.getFileHandle(filename + suffix, { create })
141
+ await this.#createPersistentFile(fileHandle)
142
+ }
143
+
144
+ // Get access handles for the files.
145
+ const file = new File(path, flags)
146
+ file.persistentFile = this.persistentFiles.get(path)
147
+ await this.#requestAccessHandle(file)
148
+ } catch (e) {
149
+ // Use an invalid persistent file to signal this error
150
+ // for the retried open.
151
+ const persistentFile = new PersistentFile(null)
152
+ this.persistentFiles.set(path, persistentFile)
153
+ console.error(e)
148
154
  }
149
-
150
- // Get access handles for the files.
151
- const file = new File(path, flags);
152
- file.persistentFile = this.persistentFiles.get(path);
153
- await this.#requestAccessHandle(file);
154
- } catch (e) {
155
- // Use an invalid persistent file to signal this error
156
- // for the retried open.
157
- const persistentFile = new PersistentFile(null);
158
- this.persistentFiles.set(path, persistentFile);
159
- console.error(e);
160
- }
161
- })());
162
- return VFS.SQLITE_BUSY;
155
+ })(),
156
+ )
157
+ return VFS.SQLITE_BUSY
163
158
  } else if (!persistentFile.fileHandle) {
164
159
  // The asynchronous open operation failed.
165
- this.persistentFiles.delete(path);
166
- return VFS.SQLITE_CANTOPEN;
160
+ this.persistentFiles.delete(path)
161
+ return VFS.SQLITE_CANTOPEN
167
162
  } else if (!persistentFile.accessHandle) {
168
163
  // This branch is reached if the database was previously opened
169
164
  // and closed.
170
- this._module.retryOps.push((async () => {
171
- const file = new File(path, flags);
172
- file.persistentFile = this.persistentFiles.get(path);
173
- await this.#requestAccessHandle(file);
174
- })());
175
- return VFS.SQLITE_BUSY;
165
+ this._module.retryOps.push(
166
+ (async () => {
167
+ const file = new File(path, flags)
168
+ file.persistentFile = this.persistentFiles.get(path)
169
+ await this.#requestAccessHandle(file)
170
+ })(),
171
+ )
172
+ return VFS.SQLITE_BUSY
176
173
  }
177
174
  }
178
175
 
179
- if (!this.accessiblePaths.has(path) &&
180
- !(flags & VFS.SQLITE_OPEN_CREATE)) {
181
- throw new Error(`File ${path} not found`);
176
+ if (!this.accessiblePaths.has(path) && !(flags & VFS.SQLITE_OPEN_CREATE)) {
177
+ throw new Error(`File ${path} not found`)
182
178
  }
183
179
 
184
- const file = new File(path, flags);
185
- this.mapIdToFile.set(fileId, file);
180
+ const file = new File(path, flags)
181
+ this.mapIdToFile.set(fileId, file)
186
182
 
187
183
  if (this.persistentFiles.has(path)) {
188
- file.persistentFile = this.persistentFiles.get(path);
184
+ file.persistentFile = this.persistentFiles.get(path)
189
185
  } else if (this.boundAccessHandles.has(path)) {
190
186
  // This temporary file was previously created and closed. Reopen
191
187
  // the same access handle.
192
- file.accessHandle = this.boundAccessHandles.get(path);
188
+ file.accessHandle = this.boundAccessHandles.get(path)
193
189
  } else if (this.unboundAccessHandles.size) {
194
190
  // Associate an unbound access handle to this file.
195
- file.accessHandle = this.unboundAccessHandles.values().next().value;
196
- file.accessHandle.truncate(0);
197
- this.unboundAccessHandles.delete(file.accessHandle);
198
- this.boundAccessHandles.set(path, file.accessHandle);
191
+ file.accessHandle = this.unboundAccessHandles.values().next().value
192
+ file.accessHandle.truncate(0)
193
+ this.unboundAccessHandles.delete(file.accessHandle)
194
+ this.boundAccessHandles.set(path, file.accessHandle)
199
195
  }
200
- this.accessiblePaths.add(path);
201
-
202
- pOutFlags.setInt32(0, flags, true);
203
- return VFS.SQLITE_OK;
196
+ this.accessiblePaths.add(path)
197
+
198
+ pOutFlags.setInt32(0, flags, true)
199
+ return VFS.SQLITE_OK
204
200
  } catch (e) {
205
- this.lastError = e;
206
- return VFS.SQLITE_CANTOPEN;
201
+ this.lastError = e
202
+ return VFS.SQLITE_CANTOPEN
207
203
  }
208
204
  }
209
205
 
210
206
  /**
211
- * @param {string} zName
212
- * @param {number} syncDir
207
+ * @param {string} zName
208
+ * @param {number} syncDir
213
209
  * @returns {number}
214
210
  */
215
211
  jDelete(zName, syncDir) {
216
212
  try {
217
- const url = new URL(zName, 'file://');
218
- const path = url.pathname;
213
+ const url = new URL(zName, 'file://')
214
+ const path = url.pathname
219
215
  if (this.persistentFiles.has(path)) {
220
- const persistentFile = this.persistentFiles.get(path);
221
- persistentFile.accessHandle.truncate(0);
216
+ const persistentFile = this.persistentFiles.get(path)
217
+ persistentFile.accessHandle.truncate(0)
222
218
  } else {
223
- this.boundAccessHandles.get(path)?.truncate(0);
219
+ this.boundAccessHandles.get(path)?.truncate(0)
224
220
  }
225
- this.accessiblePaths.delete(path);
226
- return VFS.SQLITE_OK;
221
+ this.accessiblePaths.delete(path)
222
+ return VFS.SQLITE_OK
227
223
  } catch (e) {
228
- this.lastError = e;
229
- return VFS.SQLITE_IOERR_DELETE;
224
+ this.lastError = e
225
+ return VFS.SQLITE_IOERR_DELETE
230
226
  }
231
227
  }
232
228
 
233
229
  /**
234
- * @param {string} zName
235
- * @param {number} flags
236
- * @param {DataView} pResOut
230
+ * @param {string} zName
231
+ * @param {number} flags
232
+ * @param {DataView} pResOut
237
233
  * @returns {number}
238
234
  */
239
235
  jAccess(zName, flags, pResOut) {
240
236
  try {
241
- const url = new URL(zName, 'file://');
242
- const path = url.pathname;
243
- pResOut.setInt32(0, this.accessiblePaths.has(path) ? 1 : 0, true);
244
- return VFS.SQLITE_OK;
237
+ const url = new URL(zName, 'file://')
238
+ const path = url.pathname
239
+ pResOut.setInt32(0, this.accessiblePaths.has(path) ? 1 : 0, true)
240
+ return VFS.SQLITE_OK
245
241
  } catch (e) {
246
- this.lastError = e;
247
- return VFS.SQLITE_IOERR_ACCESS;
248
- }
242
+ this.lastError = e
243
+ return VFS.SQLITE_IOERR_ACCESS
244
+ }
249
245
  }
250
246
 
251
247
  /**
252
- * @param {number} fileId
248
+ * @param {number} fileId
253
249
  * @returns {number}
254
250
  */
255
251
  jClose(fileId) {
256
252
  try {
257
- const file = this.mapIdToFile.get(fileId);
258
- this.mapIdToFile.delete(fileId);
253
+ const file = this.mapIdToFile.get(fileId)
254
+ this.mapIdToFile.delete(fileId)
259
255
 
260
256
  if (file?.flags & VFS.SQLITE_OPEN_MAIN_DB) {
261
257
  if (file.persistentFile?.handleLockReleaser) {
262
- this.#releaseAccessHandle(file);
258
+ this.#releaseAccessHandle(file)
263
259
  }
264
260
  } else if (file?.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
265
- file.accessHandle.truncate(0);
266
- this.accessiblePaths.delete(file.path);
261
+ file.accessHandle.truncate(0)
262
+ this.accessiblePaths.delete(file.path)
267
263
  if (!this.persistentFiles.has(file.path)) {
268
- this.boundAccessHandles.delete(file.path);
269
- this.unboundAccessHandles.add(file.accessHandle);
264
+ this.boundAccessHandles.delete(file.path)
265
+ this.unboundAccessHandles.add(file.accessHandle)
270
266
  }
271
267
  }
272
- return VFS.SQLITE_OK;
268
+ return VFS.SQLITE_OK
273
269
  } catch (e) {
274
- this.lastError = e;
275
- return VFS.SQLITE_IOERR_CLOSE;
270
+ this.lastError = e
271
+ return VFS.SQLITE_IOERR_CLOSE
276
272
  }
277
273
  }
278
274
 
279
275
  /**
280
- * @param {number} fileId
281
- * @param {Uint8Array} pData
276
+ * @param {number} fileId
277
+ * @param {Uint8Array} pData
282
278
  * @param {number} iOffset
283
279
  * @returns {number}
284
280
  */
285
281
  jRead(fileId, pData, iOffset) {
286
282
  try {
287
- const file = this.mapIdToFile.get(fileId);
283
+ const file = this.mapIdToFile.get(fileId)
288
284
 
289
285
  // On Chrome (at least), passing pData to accessHandle.read() is
290
286
  // an error because pData is a Proxy of a Uint8Array. Calling
291
287
  // subarray() produces a real Uint8Array and that works.
292
- const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
293
- const bytesRead = accessHandle.read(pData.subarray(), { at: iOffset });
288
+ const accessHandle = file.accessHandle || file.persistentFile.accessHandle
289
+ const bytesRead = accessHandle.read(pData.subarray(), { at: iOffset })
294
290
 
295
291
  // Opening a database file performs one read without a xLock call.
296
- if ((file.flags & VFS.SQLITE_OPEN_MAIN_DB) && !file.persistentFile.isFileLocked) {
297
- this.#releaseAccessHandle(file);
292
+ if (file.flags & VFS.SQLITE_OPEN_MAIN_DB && !file.persistentFile.isFileLocked) {
293
+ this.#releaseAccessHandle(file)
298
294
  }
299
295
 
300
296
  if (bytesRead < pData.byteLength) {
301
- pData.fill(0, bytesRead);
302
- return VFS.SQLITE_IOERR_SHORT_READ;
297
+ pData.fill(0, bytesRead)
298
+ return VFS.SQLITE_IOERR_SHORT_READ
303
299
  }
304
- return VFS.SQLITE_OK;
300
+ return VFS.SQLITE_OK
305
301
  } catch (e) {
306
- this.lastError = e;
307
- return VFS.SQLITE_IOERR_READ;
302
+ this.lastError = e
303
+ return VFS.SQLITE_IOERR_READ
308
304
  }
309
305
  }
310
306
 
311
307
  /**
312
- * @param {number} fileId
313
- * @param {Uint8Array} pData
308
+ * @param {number} fileId
309
+ * @param {Uint8Array} pData
314
310
  * @param {number} iOffset
315
311
  * @returns {number}
316
312
  */
317
313
  jWrite(fileId, pData, iOffset) {
318
314
  try {
319
- const file = this.mapIdToFile.get(fileId);
315
+ const file = this.mapIdToFile.get(fileId)
320
316
 
321
317
  // On Chrome (at least), passing pData to accessHandle.write() is
322
318
  // an error because pData is a Proxy of a Uint8Array. Calling
323
319
  // subarray() produces a real Uint8Array and that works.
324
- const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
325
- const nBytes = accessHandle.write(pData.subarray(), { at: iOffset });
326
- if (nBytes !== pData.byteLength) throw new Error('short write');
327
- return VFS.SQLITE_OK;
320
+ const accessHandle = file.accessHandle || file.persistentFile.accessHandle
321
+ const nBytes = accessHandle.write(pData.subarray(), { at: iOffset })
322
+ if (nBytes !== pData.byteLength) throw new Error('short write')
323
+ return VFS.SQLITE_OK
328
324
  } catch (e) {
329
- this.lastError = e;
330
- return VFS.SQLITE_IOERR_WRITE;
325
+ this.lastError = e
326
+ return VFS.SQLITE_IOERR_WRITE
331
327
  }
332
328
  }
333
329
 
334
330
  /**
335
- * @param {number} fileId
336
- * @param {number} iSize
331
+ * @param {number} fileId
332
+ * @param {number} iSize
337
333
  * @returns {number}
338
334
  */
339
335
  jTruncate(fileId, iSize) {
340
336
  try {
341
- const file = this.mapIdToFile.get(fileId);
342
- const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
343
- accessHandle.truncate(iSize);
344
- return VFS.SQLITE_OK;
337
+ const file = this.mapIdToFile.get(fileId)
338
+ const accessHandle = file.accessHandle || file.persistentFile.accessHandle
339
+ accessHandle.truncate(iSize)
340
+ return VFS.SQLITE_OK
345
341
  } catch (e) {
346
- this.lastError = e;
347
- return VFS.SQLITE_IOERR_TRUNCATE;
342
+ this.lastError = e
343
+ return VFS.SQLITE_IOERR_TRUNCATE
348
344
  }
349
345
  }
350
346
 
351
347
  /**
352
- * @param {number} fileId
353
- * @param {number} flags
348
+ * @param {number} fileId
349
+ * @param {number} flags
354
350
  * @returns {number}
355
351
  */
356
352
  jSync(fileId, flags) {
357
353
  try {
358
- const file = this.mapIdToFile.get(fileId);
359
- const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
360
- accessHandle.flush();
361
- return VFS.SQLITE_OK;
354
+ const file = this.mapIdToFile.get(fileId)
355
+ const accessHandle = file.accessHandle || file.persistentFile.accessHandle
356
+ accessHandle.flush()
357
+ return VFS.SQLITE_OK
362
358
  } catch (e) {
363
- this.lastError = e;
364
- return VFS.SQLITE_IOERR_FSYNC;
359
+ this.lastError = e
360
+ return VFS.SQLITE_IOERR_FSYNC
365
361
  }
366
362
  }
367
363
 
368
364
  /**
369
- * @param {number} fileId
370
- * @param {DataView} pSize64
365
+ * @param {number} fileId
366
+ * @param {DataView} pSize64
371
367
  * @returns {number}
372
368
  */
373
369
  jFileSize(fileId, pSize64) {
374
370
  try {
375
- const file = this.mapIdToFile.get(fileId);
376
- const accessHandle = file.accessHandle || file.persistentFile.accessHandle;
377
- const size = accessHandle.getSize();
378
- pSize64.setBigInt64(0, BigInt(size), true);
379
- return VFS.SQLITE_OK;
371
+ const file = this.mapIdToFile.get(fileId)
372
+ const accessHandle = file.accessHandle || file.persistentFile.accessHandle
373
+ const size = accessHandle.getSize()
374
+ pSize64.setBigInt64(0, BigInt(size), true)
375
+ return VFS.SQLITE_OK
380
376
  } catch (e) {
381
- this.lastError = e;
382
- return VFS.SQLITE_IOERR_FSTAT;
377
+ this.lastError = e
378
+ return VFS.SQLITE_IOERR_FSTAT
383
379
  }
384
380
  }
385
381
 
386
382
  /**
387
- * @param {number} fileId
388
- * @param {number} lockType
383
+ * @param {number} fileId
384
+ * @param {number} lockType
389
385
  * @returns {number}
390
386
  */
391
387
  jLock(fileId, lockType) {
392
- const file = this.mapIdToFile.get(fileId);
388
+ const file = this.mapIdToFile.get(fileId)
393
389
  if (file.persistentFile.isRequestInProgress) {
394
- file.persistentFile.isLockBusy = true;
395
- return VFS.SQLITE_BUSY;
390
+ file.persistentFile.isLockBusy = true
391
+ return VFS.SQLITE_BUSY
396
392
  }
397
393
 
398
- file.persistentFile.isFileLocked = true;
394
+ file.persistentFile.isFileLocked = true
399
395
  if (!file.persistentFile.handleLockReleaser) {
400
396
  // Start listening for notifications from other connections.
401
397
  // This is before we actually get access handles, but waiting to
402
398
  // listen until then allows a race condition where notifications
403
- // are missed.
399
+ // are missed.
404
400
  file.persistentFile.handleRequestChannel.onmessage = () => {
405
- this.log?.(`received notification for ${file.path}`);
401
+ this.log?.(`received notification for ${file.path}`)
406
402
  if (file.persistentFile.isFileLocked) {
407
403
  // We're still using the access handle, so mark it to be
408
404
  // released when we're done.
409
- file.persistentFile.isHandleRequested = true;
405
+ file.persistentFile.isHandleRequested = true
410
406
  } else {
411
407
  // Release the access handles immediately.
412
- this.#releaseAccessHandle(file);
408
+ this.#releaseAccessHandle(file)
413
409
  }
414
- file.persistentFile.handleRequestChannel.onmessage = null;
415
- };
410
+ file.persistentFile.handleRequestChannel.onmessage = null
411
+ }
416
412
 
417
- this.#requestAccessHandle(file);
418
- this.log?.('returning SQLITE_BUSY');
419
- file.persistentFile.isLockBusy = true;
420
- return VFS.SQLITE_BUSY;
413
+ this.#requestAccessHandle(file)
414
+ this.log?.('returning SQLITE_BUSY')
415
+ file.persistentFile.isLockBusy = true
416
+ return VFS.SQLITE_BUSY
421
417
  }
422
- file.persistentFile.isLockBusy = false;
423
- return VFS.SQLITE_OK;
418
+ file.persistentFile.isLockBusy = false
419
+ return VFS.SQLITE_OK
424
420
  }
425
421
 
426
422
  /**
427
- * @param {number} fileId
428
- * @param {number} lockType
423
+ * @param {number} fileId
424
+ * @param {number} lockType
429
425
  * @returns {number}
430
426
  */
431
427
  jUnlock(fileId, lockType) {
432
- const file = this.mapIdToFile.get(fileId);
428
+ const file = this.mapIdToFile.get(fileId)
433
429
  if (lockType === VFS.SQLITE_LOCK_NONE) {
434
430
  // Don't change any state if this unlock is because xLock returned
435
431
  // SQLITE_BUSY.
436
432
  if (!file.persistentFile.isLockBusy) {
437
433
  if (file.persistentFile.isHandleRequested) {
438
- // Another connection wants the access handle.
439
- this.#releaseAccessHandle(file);
440
- file.persistentFile.isHandleRequested = false;
434
+ // Another connection wants the access handle.
435
+ this.#releaseAccessHandle(file)
436
+ file.persistentFile.isHandleRequested = false
441
437
  }
442
- file.persistentFile.isFileLocked = false;
438
+ file.persistentFile.isFileLocked = false
443
439
  }
444
440
  }
445
- return VFS.SQLITE_OK;
441
+ return VFS.SQLITE_OK
446
442
  }
447
-
443
+
448
444
  /**
449
445
  * @param {number} fileId
450
446
  * @param {number} op
@@ -453,138 +449,140 @@ export class OPFSCoopSyncVFS extends FacadeVFS {
453
449
  */
454
450
  jFileControl(fileId, op, pArg) {
455
451
  try {
456
- const file = this.mapIdToFile.get(fileId);
452
+ const file = this.mapIdToFile.get(fileId)
457
453
  switch (op) {
458
454
  case VFS.SQLITE_FCNTL_PRAGMA:
459
- const key = extractString(pArg, 4);
460
- const value = extractString(pArg, 8);
461
- this.log?.('xFileControl', file.path, 'PRAGMA', key, value);
455
+ const key = extractString(pArg, 4)
456
+ const value = extractString(pArg, 8)
457
+ this.log?.('xFileControl', file.path, 'PRAGMA', key, value)
462
458
  switch (key.toLowerCase()) {
463
459
  case 'journal_mode':
464
- if (value &&
465
- !['off', 'memory', 'delete', 'wal'].includes(value.toLowerCase())) {
466
- throw new Error('journal_mode must be "off", "memory", "delete", or "wal"');
460
+ if (value && !['off', 'memory', 'delete', 'wal'].includes(value.toLowerCase())) {
461
+ throw new Error('journal_mode must be "off", "memory", "delete", or "wal"')
467
462
  }
468
- break;
463
+ break
469
464
  }
470
- break;
465
+ break
471
466
  }
472
467
  } catch (e) {
473
- this.lastError = e;
474
- return VFS.SQLITE_IOERR;
468
+ this.lastError = e
469
+ return VFS.SQLITE_IOERR
475
470
  }
476
- return VFS.SQLITE_NOTFOUND;
471
+ return VFS.SQLITE_NOTFOUND
477
472
  }
478
473
 
479
474
  /**
480
- * @param {Uint8Array} zBuf
481
- * @returns
475
+ * @param {Uint8Array} zBuf
476
+ * @returns
482
477
  */
483
478
  jGetLastError(zBuf) {
484
479
  if (this.lastError) {
485
- console.error(this.lastError);
486
- const outputArray = zBuf.subarray(0, zBuf.byteLength - 1);
487
- const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray);
488
- zBuf[written] = 0;
480
+ console.error(this.lastError)
481
+ const outputArray = zBuf.subarray(0, zBuf.byteLength - 1)
482
+ const { written } = new TextEncoder().encodeInto(this.lastError.message, outputArray)
483
+ zBuf[written] = 0
489
484
  }
490
485
  return VFS.SQLITE_OK
491
486
  }
492
487
 
493
488
  /**
494
- * @param {FileSystemFileHandle} fileHandle
489
+ * @param {FileSystemFileHandle} fileHandle
495
490
  * @returns {Promise<PersistentFile>}
496
491
  */
497
492
  async #createPersistentFile(fileHandle) {
498
- const persistentFile = new PersistentFile(fileHandle);
499
- const root = await navigator.storage.getDirectory();
500
- const relativePath = await root.resolve(fileHandle);
501
- const path = `/${relativePath.join('/')}`;
502
- persistentFile.handleRequestChannel = new BroadcastChannel(`ahp:${path}`);
503
- this.persistentFiles.set(path, persistentFile);
504
-
505
- const f = await fileHandle.getFile();
493
+ const persistentFile = new PersistentFile(fileHandle)
494
+ const root = await navigator.storage.getDirectory()
495
+ const relativePath = await root.resolve(fileHandle)
496
+ const path = `/${relativePath.join('/')}`
497
+ persistentFile.handleRequestChannel = new BroadcastChannel(`ahp:${path}`)
498
+ this.persistentFiles.set(path, persistentFile)
499
+
500
+ const f = await fileHandle.getFile()
506
501
  if (f.size) {
507
- this.accessiblePaths.add(path);
502
+ this.accessiblePaths.add(path)
508
503
  }
509
- return persistentFile;
504
+ return persistentFile
510
505
  }
511
506
 
512
507
  /**
513
- * @param {File} file
508
+ * @param {File} file
514
509
  */
515
510
  #requestAccessHandle(file) {
516
- console.assert(!file.persistentFile.handleLockReleaser);
511
+ console.assert(!file.persistentFile.handleLockReleaser)
517
512
  if (!file.persistentFile.isRequestInProgress) {
518
- file.persistentFile.isRequestInProgress = true;
519
- this._module.retryOps.push((async () => {
520
- // Acquire the Web Lock.
521
- file.persistentFile.handleLockReleaser = await this.#acquireLock(file.persistentFile);
522
-
523
- // Get access handles for the database and releated files in parallel.
524
- this.log?.(`creating access handles for ${file.path}`)
525
- await Promise.all(DB_RELATED_FILE_SUFFIXES.map(async suffix => {
526
- const persistentFile = this.persistentFiles.get(file.path + suffix);
527
- if (persistentFile) {
528
- persistentFile.accessHandle =
529
- await persistentFile.fileHandle.createSyncAccessHandle();
530
- }
531
- }));
532
- file.persistentFile.isRequestInProgress = false;
533
- })());
534
- return this._module.retryOps.at(-1);
513
+ file.persistentFile.isRequestInProgress = true
514
+ this._module.retryOps.push(
515
+ (async () => {
516
+ // Acquire the Web Lock.
517
+ file.persistentFile.handleLockReleaser = await this.#acquireLock(file.persistentFile)
518
+
519
+ // Get access handles for the database and releated files in parallel.
520
+ this.log?.(`creating access handles for ${file.path}`)
521
+ await Promise.all(
522
+ DB_RELATED_FILE_SUFFIXES.map(async (suffix) => {
523
+ const persistentFile = this.persistentFiles.get(file.path + suffix)
524
+ if (persistentFile) {
525
+ persistentFile.accessHandle = await persistentFile.fileHandle.createSyncAccessHandle()
526
+ }
527
+ }),
528
+ )
529
+ file.persistentFile.isRequestInProgress = false
530
+ })(),
531
+ )
532
+ return this._module.retryOps.at(-1)
535
533
  }
536
- return Promise.resolve();
534
+ return Promise.resolve()
537
535
  }
538
536
 
539
537
  /**
540
- * @param {File} file
538
+ * @param {File} file
541
539
  */
542
540
  async #releaseAccessHandle(file) {
543
- DB_RELATED_FILE_SUFFIXES.forEach(async suffix => {
544
- const persistentFile = this.persistentFiles.get(file.path + suffix);
541
+ DB_RELATED_FILE_SUFFIXES.forEach(async (suffix) => {
542
+ const persistentFile = this.persistentFiles.get(file.path + suffix)
545
543
  if (persistentFile) {
546
- persistentFile.accessHandle?.close();
547
- persistentFile.accessHandle = null;
544
+ persistentFile.accessHandle?.close()
545
+ persistentFile.accessHandle = null
548
546
  }
549
- });
547
+ })
550
548
  this.log?.(`access handles closed for ${file.path}`)
551
549
 
552
- file.persistentFile.handleLockReleaser?.();
553
- file.persistentFile.handleLockReleaser = null;
550
+ file.persistentFile.handleLockReleaser?.()
551
+ file.persistentFile.handleLockReleaser = null
554
552
  this.log?.(`lock released for ${file.path}`)
555
553
  }
556
554
 
557
555
  /**
558
- * @param {PersistentFile} persistentFile
556
+ * @param {PersistentFile} persistentFile
559
557
  * @returns {Promise<function>} lock releaser
560
558
  */
561
559
  #acquireLock(persistentFile) {
562
- return new Promise(resolve => {
560
+ return new Promise((resolve) => {
563
561
  // Tell other connections we want the access handle.
564
- const lockName = persistentFile.handleRequestChannel.name;
562
+ const lockName = persistentFile.handleRequestChannel.name
565
563
  const notify = () => {
566
- this.log?.(`notifying for ${lockName}`);
567
- persistentFile.handleRequestChannel.postMessage(null);
564
+ this.log?.(`notifying for ${lockName}`)
565
+ persistentFile.handleRequestChannel.postMessage(null)
568
566
  }
569
- const notifyId = setInterval(notify, LOCK_NOTIFY_INTERVAL);
570
- setTimeout(notify);
567
+ const notifyId = setInterval(notify, LOCK_NOTIFY_INTERVAL)
568
+ setTimeout(notify)
571
569
 
572
570
  this.log?.(`lock requested: ${lockName}`)
573
- navigator.locks.request(lockName, lock => {
571
+ navigator.locks.request(lockName, (lock) => {
574
572
  // We have the lock. Stop asking other connections for it.
575
- this.log?.(`lock acquired: ${lockName}`, lock);
576
- clearInterval(notifyId);
577
- return new Promise(resolve);
578
- });
579
- });
573
+ this.log?.(`lock acquired: ${lockName}`, lock)
574
+ clearInterval(notifyId)
575
+ return new Promise(resolve)
576
+ })
577
+ })
580
578
  }
581
579
  }
582
580
 
583
581
  function extractString(dataView, offset) {
584
- const p = dataView.getUint32(offset, true);
582
+ const p = dataView.getUint32(offset, true)
585
583
  if (p) {
586
- const chars = new Uint8Array(dataView.buffer, p);
587
- return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)));
584
+ const chars = new Uint8Array(dataView.buffer, p)
585
+ return new TextDecoder().decode(chars.subarray(0, chars.indexOf(0)))
588
586
  }
589
- return null;
590
- }
587
+ return null
588
+ }