@livestore/sqlite-wasm 0.4.0-dev.18 → 0.4.0-dev.19

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