@livestore/sqlite-wasm 0.0.0-snapshot-9507e455a5c1ff8ca4b9414bde007fe51bb2bcd0 → 0.0.0-snapshot-d3260202d4a0ef658630c9a759c396d7ea3b055d

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.
@@ -1,5 +1,5 @@
1
1
  // Based on https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/AccessHandlePoolVFS.js
2
- import { Effect, Opfs, Runtime, Schedule, Schema, type Scope, Stream } from '@livestore/utils/effect'
2
+ import { Effect, Opfs, Runtime, Schedule, type Scope, Stream, type WebError } from '@livestore/utils/effect'
3
3
  import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
4
4
  import { FacadeVFS } from '../../FacadeVFS.ts'
5
5
 
@@ -105,11 +105,13 @@ export class AccessHandlePoolVFS extends FacadeVFS {
105
105
  * It's not the same as the SQLite file name. It's a randomly-generated
106
106
  * string that is not meaningful to the application.
107
107
  */
108
- getOpfsFileName(zName: string) {
109
- const path = this.#getPath(zName)
110
- const accessHandle = this.#mapPathToAccessHandle.get(path)!
111
- return this.#mapAccessHandleToName.get(accessHandle)!
112
- }
108
+ getOpfsFileName = Effect.fn((zName: string) =>
109
+ Effect.gen(this, function* () {
110
+ const path = this.#getPath(zName)
111
+ const accessHandle = this.#mapPathToAccessHandle.get(path)!
112
+ return this.#mapAccessHandleToName.get(accessHandle)!
113
+ }),
114
+ )
113
115
 
114
116
  /**
115
117
  * Reads the SQLite payload (without the OPFS header) for the given file.
@@ -120,51 +122,45 @@ export class AccessHandlePoolVFS extends FacadeVFS {
120
122
  * acquires an exclusive lock — we don't need to handle short reads as
121
123
  * the file cannot be modified by other threads.
122
124
  */
123
- readFilePayload(zName: string): ArrayBuffer {
124
- const path = this.#getPath(zName)
125
- const accessHandle = this.#mapPathToAccessHandle.get(path)
126
-
127
- if (accessHandle === undefined) {
128
- throw new OpfsError({
129
- path,
130
- cause: new Error('Cannot read payload for untracked OPFS path'),
131
- })
132
- }
125
+ readFilePayload = Effect.fn((zName: string) =>
126
+ Effect.gen(this, function* () {
127
+ const path = this.#getPath(zName)
128
+ const accessHandle = this.#mapPathToAccessHandle.get(path)
133
129
 
134
- const fileSize = Opfs.Opfs.syncGetSize(accessHandle).pipe(Runtime.runSync(this.#runtime))
135
- if (fileSize <= HEADER_OFFSET_DATA) {
136
- throw new OpfsError({
137
- path,
138
- cause: new Error(
130
+ if (accessHandle === undefined) {
131
+ return yield* Effect.dieMessage('Cannot read payload for untracked OPFS path')
132
+ }
133
+
134
+ const fileSize = yield* Opfs.Opfs.syncGetSize(accessHandle)
135
+ if (fileSize <= HEADER_OFFSET_DATA) {
136
+ return yield* Effect.dieMessage(
139
137
  `OPFS file too small to contain header and payload: size ${fileSize} < HEADER_OFFSET_DATA ${HEADER_OFFSET_DATA}`,
140
- ),
141
- })
142
- }
138
+ )
139
+ }
143
140
 
144
- const payloadSize = fileSize - HEADER_OFFSET_DATA
145
- const payload = new Uint8Array(payloadSize)
146
- const bytesRead = Opfs.Opfs.syncRead(accessHandle, payload, { at: HEADER_OFFSET_DATA }).pipe(
147
- Runtime.runSync(this.#runtime),
148
- )
149
- if (bytesRead !== payloadSize) {
150
- throw new OpfsError({
151
- path,
152
- cause: new Error(`Failed to read full payload from OPFS file: read ${bytesRead}/${payloadSize}`),
153
- })
154
- }
155
- return payload.buffer
156
- }
141
+ const payloadSize = fileSize - HEADER_OFFSET_DATA
142
+ const payload = new Uint8Array(payloadSize)
143
+ const bytesRead = yield* Opfs.Opfs.syncRead(accessHandle, payload, { at: HEADER_OFFSET_DATA })
144
+ if (bytesRead !== payloadSize) {
145
+ return yield* Effect.dieMessage(`Failed to read full payload from OPFS file: read ${bytesRead}/${payloadSize}`)
146
+ }
157
147
 
158
- resetAccessHandle(zName: string) {
159
- const path = this.#getPath(zName)
160
- const accessHandle = this.#mapPathToAccessHandle.get(path)!
161
- Opfs.Opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA).pipe(Runtime.runSync(this.#runtime))
162
- // accessHandle.write(new Uint8Array(), { at: HEADER_OFFSET_DATA })
163
- // accessHandle.flush()
164
- }
148
+ return payload.buffer
149
+ }),
150
+ )
151
+
152
+ resetAccessHandle = Effect.fn((zName: string) =>
153
+ Effect.gen(this, function* () {
154
+ const path = this.#getPath(zName)
155
+ const accessHandle = this.#mapPathToAccessHandle.get(path)!
156
+ yield* Opfs.Opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA)
157
+ // accessHandle.write(new Uint8Array(), { at: HEADER_OFFSET_DATA })
158
+ // accessHandle.flush()
159
+ }),
160
+ )
165
161
 
166
162
  jOpen(zName: string, fileId: number, flags: number, pOutFlags: DataView): number {
167
- try {
163
+ return Effect.gen(this, function* () {
168
164
  // First try to open a path that already exists in the file system.
169
165
  const path = zName ? this.#getPath(zName) : Math.random().toString(36)
170
166
  let accessHandle = this.#mapPathToAccessHandle.get(path)
@@ -173,16 +169,15 @@ export class AccessHandlePoolVFS extends FacadeVFS {
173
169
  if (this.getSize() < this.getCapacity()) {
174
170
  // Choose an unassociated OPFS file from the pool.
175
171
  ;[accessHandle] = this.#availableAccessHandles.keys()
176
- this.#setAssociatedPath(accessHandle!, path, flags)
172
+ yield* this.#setAssociatedPath(accessHandle!, path, flags)
177
173
  } else {
178
174
  // Out of unassociated files. This can be fixed by calling
179
175
  // addCapacity() from the application.
180
- throw new Error('cannot create file')
176
+ return yield* Effect.dieMessage('cannot create file')
181
177
  }
182
178
  }
183
- if (!accessHandle) {
184
- throw new Error('file not found')
185
- }
179
+ if (!accessHandle) return yield* Effect.dieMessage('file not found')
180
+
186
181
  // Subsequent methods are only passed the fileId, so make sure we have
187
182
  // a way to get the file resources.
188
183
  const file = { path, flags, accessHandle }
@@ -190,61 +185,102 @@ export class AccessHandlePoolVFS extends FacadeVFS {
190
185
 
191
186
  pOutFlags.setInt32(0, flags, true)
192
187
  return VFS.SQLITE_OK
193
- } catch (e: any) {
194
- console.error(e.message)
195
- return VFS.SQLITE_CANTOPEN
196
- }
188
+ }).pipe(
189
+ Effect.tapCauseLogPretty,
190
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_CANTOPEN)),
191
+ Runtime.runSync(this.#runtime),
192
+ )
197
193
  }
198
194
 
199
195
  jClose(fileId: number): number {
200
- const file = this.#mapIdToFile.get(fileId)
201
- if (file) {
202
- Opfs.Opfs.syncFlush(file.accessHandle).pipe(Runtime.runSync(this.#runtime))
203
- this.#mapIdToFile.delete(fileId)
204
- if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
205
- this.#deletePath(file.path)
196
+ return Effect.gen(this, function* () {
197
+ const file = this.#mapIdToFile.get(fileId)
198
+ if (file) {
199
+ yield* Opfs.Opfs.syncFlush(file.accessHandle)
200
+ this.#mapIdToFile.delete(fileId)
201
+ if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
202
+ yield* this.#deletePath(file.path)
203
+ }
206
204
  }
207
- }
208
- return VFS.SQLITE_OK
205
+ return VFS.SQLITE_OK
206
+ }).pipe(
207
+ Effect.tapCauseLogPretty,
208
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_IOERR_CLOSE)),
209
+ Runtime.runSync(this.#runtime),
210
+ )
209
211
  }
210
212
 
211
213
  jRead(fileId: number, pData: Uint8Array<ArrayBuffer>, iOffset: number): number {
212
- const file = this.#mapIdToFile.get(fileId)!
213
- const nBytes = Opfs.Opfs.syncRead(file.accessHandle, pData.subarray(), {
214
- at: HEADER_OFFSET_DATA + iOffset,
215
- }).pipe(Runtime.runSync(this.#runtime))
216
- if (nBytes < pData.byteLength) {
217
- pData.fill(0, nBytes, pData.byteLength)
218
- return VFS.SQLITE_IOERR_SHORT_READ
219
- }
220
- return VFS.SQLITE_OK
214
+ return Effect.gen(this, function* () {
215
+ const file = this.#mapIdToFile.get(fileId)!
216
+ const nBytes = yield* Opfs.Opfs.syncRead(file.accessHandle, pData.subarray(), {
217
+ at: HEADER_OFFSET_DATA + iOffset,
218
+ })
219
+ if (nBytes < pData.byteLength) {
220
+ pData.fill(0, nBytes, pData.byteLength)
221
+ return VFS.SQLITE_IOERR_SHORT_READ
222
+ }
223
+ return VFS.SQLITE_OK
224
+ }).pipe(
225
+ Effect.tapCauseLogPretty,
226
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_IOERR_READ)),
227
+ Runtime.runSync(this.#runtime),
228
+ )
221
229
  }
222
230
 
223
231
  jWrite(fileId: number, pData: Uint8Array<ArrayBuffer>, iOffset: number): number {
224
- const file = this.#mapIdToFile.get(fileId)!
225
- const nBytes = Opfs.Opfs.syncWrite(file.accessHandle, pData.subarray(), {
226
- at: HEADER_OFFSET_DATA + iOffset,
227
- }).pipe(Runtime.runSync(this.#runtime))
228
- return nBytes === pData.byteLength ? VFS.SQLITE_OK : VFS.SQLITE_IOERR
232
+ return Effect.gen(this, function* () {
233
+ const file = this.#mapIdToFile.get(fileId)!
234
+ const nBytes = yield* Opfs.Opfs.syncWrite(file.accessHandle, pData.subarray(), {
235
+ at: HEADER_OFFSET_DATA + iOffset,
236
+ })
237
+ if (nBytes !== pData.byteLength) {
238
+ return yield* Effect.dieMessage(`Wrote ${nBytes} bytes, expected ${pData.byteLength}`)
239
+ }
240
+ return VFS.SQLITE_OK
241
+ }).pipe(
242
+ Effect.tapCauseLogPretty,
243
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_IOERR_WRITE)),
244
+ Runtime.runSync(this.#runtime),
245
+ )
229
246
  }
230
247
 
231
248
  jTruncate(fileId: number, iSize: number): number {
232
- const file = this.#mapIdToFile.get(fileId)!
233
- Opfs.Opfs.syncTruncate(file.accessHandle, HEADER_OFFSET_DATA + iSize).pipe(Runtime.runSync(this.#runtime))
234
- return VFS.SQLITE_OK
249
+ return Effect.gen(this, function* () {
250
+ const file = this.#mapIdToFile.get(fileId)!
251
+ yield* Opfs.Opfs.syncTruncate(file.accessHandle, HEADER_OFFSET_DATA + iSize)
252
+ return VFS.SQLITE_OK
253
+ }).pipe(
254
+ Effect.tapCauseLogPretty,
255
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_IOERR_TRUNCATE)),
256
+ Runtime.runSync(this.#runtime),
257
+ )
235
258
  }
236
259
 
237
260
  jSync(fileId: number, _flags: number): number {
238
- const file = this.#mapIdToFile.get(fileId)!
239
- Opfs.Opfs.syncFlush(file.accessHandle).pipe(Runtime.runSync(this.#runtime))
240
- return VFS.SQLITE_OK
261
+ return Effect.gen(this, function* () {
262
+ const file = this.#mapIdToFile.get(fileId)!
263
+ yield* Opfs.Opfs.syncFlush(file.accessHandle)
264
+ return VFS.SQLITE_OK
265
+ }).pipe(
266
+ Effect.tapCauseLogPretty,
267
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_IOERR_FSYNC)),
268
+ Runtime.runSync(this.#runtime),
269
+ )
241
270
  }
242
271
 
243
272
  jFileSize(fileId: number, pSize64: DataView): number {
244
- const file = this.#mapIdToFile.get(fileId)!
245
- const size = Opfs.Opfs.syncGetSize(file.accessHandle).pipe(Runtime.runSync(this.#runtime)) - HEADER_OFFSET_DATA
246
- pSize64.setBigInt64(0, BigInt(size), true)
247
- return VFS.SQLITE_OK
273
+ return Effect.gen(this, function* () {
274
+ const file = this.#mapIdToFile.get(fileId)!
275
+ const opfsFileSize = yield* Opfs.Opfs.syncGetSize(file.accessHandle)
276
+ const size = opfsFileSize - HEADER_OFFSET_DATA
277
+ pSize64.setBigInt64(0, BigInt(size), true)
278
+ return VFS.SQLITE_OK
279
+ }).pipe(
280
+ Effect.tapCauseLogPretty,
281
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_IOERR_FSTAT)),
282
+ Runtime.runSync(this.#runtime),
283
+ )
248
284
  }
249
285
 
250
286
  jSectorSize(_fileId: number): number {
@@ -256,34 +292,46 @@ export class AccessHandlePoolVFS extends FacadeVFS {
256
292
  }
257
293
 
258
294
  jAccess(zName: string, _flags: number, pResOut: DataView): number {
259
- const path = this.#getPath(zName)
260
- pResOut.setInt32(0, this.#mapPathToAccessHandle.has(path) ? 1 : 0, true)
261
- return VFS.SQLITE_OK
295
+ return Effect.gen(this, function* () {
296
+ const path = this.#getPath(zName)
297
+ pResOut.setInt32(0, this.#mapPathToAccessHandle.has(path) ? 1 : 0, true)
298
+ return VFS.SQLITE_OK
299
+ }).pipe(
300
+ Effect.tapCauseLogPretty,
301
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_IOERR_ACCESS)),
302
+ Runtime.runSync(this.#runtime),
303
+ )
262
304
  }
263
305
 
264
306
  jDelete(zName: string, _syncDir: number): number {
265
- const path = this.#getPath(zName)
266
- this.#deletePath(path)
267
- return VFS.SQLITE_OK
307
+ return Effect.gen(this, function* () {
308
+ const path = this.#getPath(zName)
309
+ yield* this.#deletePath(path)
310
+ return VFS.SQLITE_OK
311
+ }).pipe(
312
+ Effect.tapCauseLogPretty,
313
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_IOERR_DELETE)),
314
+ Runtime.runSync(this.#runtime),
315
+ )
268
316
  }
269
317
 
270
318
  close() {
271
- this.#releaseAccessHandles()
319
+ this.#releaseAccessHandles().pipe(Runtime.runPromise(this.#runtime))
272
320
  }
273
321
 
274
322
  async isReady() {
275
- if (!this.#directoryHandle) {
276
- // All files are stored in a single directory.
277
- this.#directoryHandle = await Opfs.getDirectoryHandleByPath(this.#directoryPath, { create: true }).pipe(
278
- Runtime.runPromise(this.#runtime),
279
- )
280
-
281
- await this.#acquireAccessHandles()
282
- if (this.getCapacity() === 0) {
283
- await this.addCapacity(DEFAULT_CAPACITY)
323
+ return Effect.gen(this, function* () {
324
+ if (!this.#directoryHandle) {
325
+ // All files are stored in a single directory.
326
+ this.#directoryHandle = yield* Opfs.getDirectoryHandleByPath(this.#directoryPath, { create: true })
327
+
328
+ yield* this.#acquireAccessHandles()
329
+ if (this.getCapacity() === 0) {
330
+ yield* this.addCapacity(DEFAULT_CAPACITY)
331
+ }
284
332
  }
285
- }
286
- return true
333
+ return true
334
+ }).pipe(Runtime.runPromise(this.#runtime))
287
335
  }
288
336
 
289
337
  /**
@@ -313,155 +361,172 @@ export class AccessHandlePoolVFS extends FacadeVFS {
313
361
  /**
314
362
  * Increase the capacity of the file system by n.
315
363
  */
316
- async addCapacity(n: number): Promise<number> {
317
- for (let i = 0; i < n; ++i) {
318
- const name = Math.random().toString(36).replace('0.', '')
319
- const accessHandle = await Opfs.Opfs.getFileHandle(this.#directoryHandle!, name, { create: true }).pipe(
320
- Effect.andThen((handle) => Opfs.Opfs.createSyncAccessHandle(handle)),
321
- Effect.mapError((error) => new OpfsError({ cause: error, path: name })),
322
- Effect.retry(Schedule.exponentialBackoff10Sec),
323
- Runtime.runPromise(this.#runtime),
324
- )
325
- this.#mapAccessHandleToName.set(accessHandle, name)
364
+ addCapacity: (n: number) => Effect.Effect<void, WebError.WebError, Opfs.Opfs | Scope.Scope> = Effect.fn((n: number) =>
365
+ Effect.repeatN(
366
+ Effect.gen(this, function* () {
367
+ const name = Math.random().toString(36).replace('0.', '')
368
+ const accessHandle = yield* Opfs.Opfs.getFileHandle(this.#directoryHandle!, name, { create: true }).pipe(
369
+ Effect.andThen((handle) => Opfs.Opfs.createSyncAccessHandle(handle)),
370
+ Effect.retry(Schedule.exponentialBackoff10Sec),
371
+ )
372
+ this.#mapAccessHandleToName.set(accessHandle, name)
326
373
 
327
- this.#setAssociatedPath(accessHandle, '', 0)
328
- }
329
- return n
330
- }
374
+ yield* this.#setAssociatedPath(accessHandle, '', 0)
375
+ }),
376
+ n,
377
+ ),
378
+ )
331
379
 
332
380
  /**
333
381
  * Decrease the capacity of the file system by n. The capacity cannot be
334
382
  * decreased to fewer than the current number of SQLite files in the
335
383
  * file system.
336
384
  */
337
- async removeCapacity(n: number): Promise<number> {
338
- let nRemoved = 0
339
- for (const accessHandle of Array.from(this.#availableAccessHandles)) {
340
- if (nRemoved === n || this.getSize() === this.getCapacity()) return nRemoved
341
-
342
- const name = this.#mapAccessHandleToName.get(accessHandle)!
343
- accessHandle.close()
344
- Opfs.Opfs.removeEntry(this.#directoryHandle!, name).pipe(Runtime.runPromise(this.#runtime))
345
- this.#mapAccessHandleToName.delete(accessHandle)
346
- this.#availableAccessHandles.delete(accessHandle)
347
- ++nRemoved
348
- }
349
- return nRemoved
350
- }
351
-
352
- async #acquireAccessHandles() {
353
- const handlesStream = Opfs.Opfs.values(this.#directoryHandle!).pipe(Runtime.runSync(this.#runtime))
354
-
355
- // Enumerate all the files in the directory.
356
- const files = await handlesStream.pipe(
357
- Stream.filter((handle): handle is FileSystemFileHandle => handle.kind === 'file'),
358
- Stream.map((fileHandle) => [fileHandle.name, fileHandle] as const),
359
- Stream.runCollectReadonlyArray,
360
- Runtime.runPromise(this.#runtime),
361
- )
362
-
363
- // Open access handles in parallel, separating associated and unassociated.
364
- await Promise.all(
365
- files.map(async ([name, handle]) => {
366
- const accessHandle = await Opfs.Opfs.createSyncAccessHandle(handle).pipe(
367
- Effect.mapError((error) => new OpfsError({ cause: error, path: name })),
368
- Effect.retry(Schedule.exponentialBackoff10Sec),
369
- Runtime.runPromise(this.#runtime),
370
- )
371
- this.#mapAccessHandleToName.set(accessHandle, name)
372
- const path = this.#getAssociatedPath(accessHandle)
373
- if (path) {
374
- this.#mapPathToAccessHandle.set(path, accessHandle)
375
- } else {
376
- this.#availableAccessHandles.add(accessHandle)
377
- }
378
- }),
379
- )
380
- }
381
-
382
- #releaseAccessHandles() {
383
- for (const accessHandle of this.#mapAccessHandleToName.keys()) {
384
- accessHandle.close()
385
- }
386
- this.#mapAccessHandleToName.clear()
387
- this.#mapPathToAccessHandle.clear()
388
- this.#availableAccessHandles.clear()
389
- }
385
+ removeCapacity = Effect.fn((n: number) =>
386
+ Effect.gen(this, function* () {
387
+ let nRemoved = 0
388
+ yield* Effect.forEach(
389
+ this.#availableAccessHandles,
390
+ (accessHandle) =>
391
+ Effect.gen(this, function* () {
392
+ if (nRemoved === n || this.getSize() === this.getCapacity()) return nRemoved
393
+
394
+ const name = this.#mapAccessHandleToName.get(accessHandle)!
395
+ accessHandle.close()
396
+ yield* Opfs.Opfs.removeEntry(this.#directoryHandle!, name)
397
+ this.#mapAccessHandleToName.delete(accessHandle)
398
+ this.#availableAccessHandles.delete(accessHandle)
399
+ ++nRemoved
400
+ }),
401
+ { concurrency: 'unbounded', discard: true },
402
+ )
403
+ return nRemoved
404
+ }),
405
+ )
406
+
407
+ #acquireAccessHandles = Effect.fn(() =>
408
+ Effect.gen(this, function* () {
409
+ const handlesStream = yield* Opfs.Opfs.values(this.#directoryHandle!)
410
+
411
+ yield* handlesStream.pipe(
412
+ Stream.filter((handle): handle is FileSystemFileHandle => handle.kind === 'file'),
413
+ Stream.mapEffect(
414
+ (fileHandle) =>
415
+ Effect.gen(function* () {
416
+ return {
417
+ opfsFileName: fileHandle.name,
418
+ accessHandle: yield* Opfs.Opfs.createSyncAccessHandle(fileHandle),
419
+ } as const
420
+ }),
421
+ { concurrency: 'unbounded' },
422
+ ),
423
+ Stream.runForEach(({ opfsFileName, accessHandle }) =>
424
+ Effect.gen(this, function* () {
425
+ this.#mapAccessHandleToName.set(accessHandle, opfsFileName)
426
+ const path = yield* this.#getAssociatedPath(accessHandle)
427
+
428
+ if (path) {
429
+ this.#mapPathToAccessHandle.set(path, accessHandle)
430
+ } else {
431
+ this.#availableAccessHandles.add(accessHandle)
432
+ }
433
+ }),
434
+ ),
435
+ )
436
+ }),
437
+ )
438
+
439
+ #releaseAccessHandles = Effect.fn(() =>
440
+ Effect.gen(this, function* () {
441
+ yield* Effect.forEach(
442
+ this.#mapAccessHandleToName.keys(),
443
+ (accessHandle) => Effect.sync(() => accessHandle.close()),
444
+ { concurrency: 'unbounded', discard: true },
445
+ )
446
+ this.#mapAccessHandleToName.clear()
447
+ this.#mapPathToAccessHandle.clear()
448
+ this.#availableAccessHandles.clear()
449
+ }),
450
+ )
390
451
 
391
452
  /**
392
453
  * Read and return the associated path from an OPFS file header.
393
454
  * Empty string is returned for an unassociated OPFS file.
394
455
  * @returns {string} path or empty string
395
456
  */
396
- #getAssociatedPath(accessHandle: FileSystemSyncAccessHandle): string {
397
- // Read the path and digest of the path from the file.
398
- const corpus = new Uint8Array(HEADER_CORPUS_SIZE)
399
- Opfs.Opfs.syncRead(accessHandle, corpus, { at: 0 }).pipe(Runtime.runSync(this.#runtime))
400
-
401
- // Delete files not expected to be present.
402
- const dataView = new DataView(corpus.buffer, corpus.byteOffset)
403
- const flags = dataView.getUint32(HEADER_OFFSET_FLAGS)
404
- if (corpus[0] && (flags & VFS.SQLITE_OPEN_DELETEONCLOSE || (flags & PERSISTENT_FILE_TYPES) === 0)) {
405
- console.warn(`Remove file with unexpected flags ${flags.toString(16)}`)
406
- this.#setAssociatedPath(accessHandle, '', 0)
407
- return ''
408
- }
457
+ #getAssociatedPath = Effect.fn((accessHandle: FileSystemSyncAccessHandle) =>
458
+ Effect.gen(this, function* () {
459
+ // Read the path and digest of the path from the file.
460
+ const corpus = new Uint8Array(HEADER_CORPUS_SIZE)
461
+ yield* Opfs.Opfs.syncRead(accessHandle, corpus, { at: 0 })
462
+
463
+ // Delete files not expected to be present.
464
+ const dataView = new DataView(corpus.buffer, corpus.byteOffset)
465
+ const flags = dataView.getUint32(HEADER_OFFSET_FLAGS)
466
+ if (corpus[0] && (flags & VFS.SQLITE_OPEN_DELETEONCLOSE || (flags & PERSISTENT_FILE_TYPES) === 0)) {
467
+ yield* Effect.logWarning(`Remove file with unexpected flags ${flags.toString(16)}`)
468
+ yield* this.#setAssociatedPath(accessHandle, '', 0)
469
+ return ''
470
+ }
409
471
 
410
- const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4)
411
- Opfs.Opfs.syncRead(accessHandle, fileDigest, { at: HEADER_OFFSET_DIGEST }).pipe(Runtime.runSync(this.#runtime))
412
-
413
- // Verify the digest.
414
- const computedDigest = this.#computeDigest(corpus)
415
- if (fileDigest.every((value, i) => value === computedDigest[i])) {
416
- // Good digest. Decode the null-terminated path string.
417
- const pathBytes = corpus.indexOf(0)
418
- if (pathBytes === 0) {
419
- // Ensure that unassociated files are empty. Unassociated files are
420
- // truncated in #setAssociatedPath after the header is written. If
421
- // an interruption occurs right before the truncation then garbage
422
- // may remain in the file.
423
- Opfs.Opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA).pipe(Runtime.runSync(this.#runtime))
472
+ const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4)
473
+ yield* Opfs.Opfs.syncRead(accessHandle, fileDigest, { at: HEADER_OFFSET_DIGEST })
474
+
475
+ // Verify the digest.
476
+ const computedDigest = this.#computeDigest(corpus)
477
+ if (fileDigest.every((value, i) => value === computedDigest[i])) {
478
+ // Good digest. Decode the null-terminated path string.
479
+ const pathBytes = corpus.indexOf(0)
480
+ if (pathBytes === 0) {
481
+ // Ensure that unassociated files are empty. Unassociated files are
482
+ // truncated in #setAssociatedPath after the header is written. If
483
+ // an interruption occurs right before the truncation then garbage
484
+ // may remain in the file.
485
+ yield* Opfs.Opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA)
486
+ }
487
+ return new TextDecoder().decode(corpus.subarray(0, pathBytes))
488
+ } else {
489
+ // Bad digest. Repair this header.
490
+ yield* Effect.logWarning('Disassociating file with bad digest.')
491
+ yield* this.#setAssociatedPath(accessHandle, '', 0)
492
+ return ''
424
493
  }
425
- return new TextDecoder().decode(corpus.subarray(0, pathBytes))
426
- } else {
427
- // Bad digest. Repair this header.
428
- console.warn('Disassociating file with bad digest.')
429
- this.#setAssociatedPath(accessHandle, '', 0)
430
- return ''
431
- }
432
- }
494
+ }),
495
+ )
433
496
 
434
497
  /**
435
498
  * Set the path on an OPFS file header.
436
499
  */
437
- #setAssociatedPath(accessHandle: FileSystemSyncAccessHandle, path: string, flags: number) {
438
- // Convert the path string to UTF-8.
439
- const corpus = new Uint8Array(HEADER_CORPUS_SIZE)
440
- const encodedResult = new TextEncoder().encodeInto(path, corpus)
441
- if (encodedResult.written >= HEADER_MAX_PATH_SIZE) {
442
- throw new Error('path too long')
443
- }
500
+ #setAssociatedPath = Effect.fn((accessHandle: FileSystemSyncAccessHandle, path: string, flags: number) =>
501
+ Effect.gen(this, function* () {
502
+ // Convert the path string to UTF-8.
503
+ const corpus = new Uint8Array(HEADER_CORPUS_SIZE)
504
+ const encodedResult = new TextEncoder().encodeInto(path, corpus)
505
+ if (encodedResult.written >= HEADER_MAX_PATH_SIZE) {
506
+ return yield* Effect.dieMessage('path too long')
507
+ }
444
508
 
445
- // Add the creation flags.
446
- const dataView = new DataView(corpus.buffer, corpus.byteOffset)
447
- dataView.setUint32(HEADER_OFFSET_FLAGS, flags)
448
-
449
- // Write the OPFS file header, including the digest.
450
- const digest = this.#computeDigest(corpus)
451
- Opfs.Opfs.syncWrite(accessHandle, corpus, { at: 0 }).pipe(Runtime.runSync(this.#runtime))
452
- Opfs.Opfs.syncWrite(accessHandle, digest, { at: HEADER_OFFSET_DIGEST }).pipe(Runtime.runSync(this.#runtime))
453
- Opfs.Opfs.syncFlush(accessHandle).pipe(Runtime.runSync(this.#runtime))
454
-
455
- if (path) {
456
- this.#mapPathToAccessHandle.set(path, accessHandle)
457
- this.#availableAccessHandles.delete(accessHandle)
458
- } else {
459
- // This OPFS file doesn't represent any SQLite file so it doesn't
460
- // need to keep any data.
461
- Opfs.Opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA).pipe(Runtime.runSync(this.#runtime))
462
- this.#availableAccessHandles.add(accessHandle)
463
- }
464
- }
509
+ // Add the creation flags.
510
+ const dataView = new DataView(corpus.buffer, corpus.byteOffset)
511
+ dataView.setUint32(HEADER_OFFSET_FLAGS, flags)
512
+
513
+ // Write the OPFS file header, including the digest.
514
+ const digest = this.#computeDigest(corpus)
515
+ yield* Opfs.Opfs.syncWrite(accessHandle, corpus, { at: 0 })
516
+ yield* Opfs.Opfs.syncWrite(accessHandle, digest, { at: HEADER_OFFSET_DIGEST })
517
+ yield* Opfs.Opfs.syncFlush(accessHandle)
518
+
519
+ if (path) {
520
+ this.#mapPathToAccessHandle.set(path, accessHandle)
521
+ this.#availableAccessHandles.delete(accessHandle)
522
+ } else {
523
+ // This OPFS file doesn't represent any SQLite file so it doesn't
524
+ // need to keep any data.
525
+ yield* Opfs.Opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA)
526
+ this.#availableAccessHandles.add(accessHandle)
527
+ }
528
+ }),
529
+ )
465
530
 
466
531
  /**
467
532
  * We need a synchronous digest function so can't use WebCrypto.
@@ -500,17 +565,14 @@ export class AccessHandlePoolVFS extends FacadeVFS {
500
565
  * Remove the association between a path and an OPFS file.
501
566
  * @param {string} path
502
567
  */
503
- #deletePath(path: string) {
504
- const accessHandle = this.#mapPathToAccessHandle.get(path)
505
- if (accessHandle) {
506
- // Un-associate the SQLite path from the OPFS file.
507
- this.#mapPathToAccessHandle.delete(path)
508
- this.#setAssociatedPath(accessHandle, '', 0)
509
- }
510
- }
568
+ #deletePath = Effect.fn((path: string) =>
569
+ Effect.gen(this, function* () {
570
+ const accessHandle = this.#mapPathToAccessHandle.get(path)
571
+ if (accessHandle) {
572
+ // Un-associate the SQLite path from the OPFS file.
573
+ this.#mapPathToAccessHandle.delete(path)
574
+ yield* this.#setAssociatedPath(accessHandle, '', 0)
575
+ }
576
+ }),
577
+ )
511
578
  }
512
-
513
- export class OpfsError extends Schema.TaggedError<OpfsError>()('OpfsError', {
514
- cause: Schema.Defect,
515
- path: Schema.String,
516
- }) {}