@livestore/sqlite-wasm 0.4.0-dev.8 → 0.4.0

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