@livestore/sqlite-wasm 0.4.0-dev.9 → 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 +24 -18
  12. package/dist/browser/opfs/AccessHandlePoolVFS.d.ts.map +1 -1
  13. package/dist/browser/opfs/AccessHandlePoolVFS.js +165 -166
  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 +3 -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 +17 -8
  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 +360 -268
  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 +4 -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 +25 -8
  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
@@ -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,22 +83,45 @@ 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
126
  /**
96
127
  * Reads the SQLite payload (without the OPFS header) for the given file.
@@ -101,67 +132,63 @@ export class AccessHandlePoolVFS extends FacadeVFS {
101
132
  * acquires an exclusive lock — we don't need to handle short reads as
102
133
  * the file cannot be modified by other threads.
103
134
  */
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
- }
135
+ readFilePayload = Effect.fn((zName: string) =>
136
+ Effect.gen(this, function* () {
137
+ const path = this.#getPath(zName)
138
+ const accessHandle = this.#mapPathToAccessHandle.get(path)
114
139
 
115
- const fileSize = accessHandle.getSize()
116
- if (fileSize <= HEADER_OFFSET_DATA) {
117
- throw new OpfsError({
118
- path,
119
- cause: new Error(
120
- `OPFS file too small to contain header and payload: size ${fileSize} < HEADER_OFFSET_DATA ${HEADER_OFFSET_DATA}`,
121
- ),
122
- })
123
- }
140
+ if (accessHandle === undefined) {
141
+ return shouldNeverHappen('Cannot read payload for untracked OPFS path')
142
+ }
124
143
 
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
- }
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
+ }
136
150
 
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
- }
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
+ }
144
157
 
145
- jOpen(zName: string, fileId: number, flags: number, pOutFlags: DataView): number {
146
- try {
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* () {
147
174
  // First try to open a path that already exists in the file system.
148
- 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)
149
177
  let accessHandle = this.#mapPathToAccessHandle.get(path)
150
- if (!accessHandle && flags & VFS.SQLITE_OPEN_CREATE) {
178
+ if (accessHandle == null && (flags & VFS.SQLITE_OPEN_CREATE) !== 0) {
151
179
  // File not found so try to create it.
152
180
  if (this.getSize() < this.getCapacity()) {
153
181
  // Choose an unassociated OPFS file from the pool.
154
182
  ;[accessHandle] = this.#availableAccessHandles.keys()
155
- this.#setAssociatedPath(accessHandle!, path, flags)
183
+ yield* this.#setAssociatedPath(accessHandle!, path, flags)
156
184
  } else {
157
185
  // Out of unassociated files. This can be fixed by calling
158
186
  // addCapacity() from the application.
159
- throw new Error('cannot create file')
187
+ return yield* Effect.dieMessage('cannot create file')
160
188
  }
161
189
  }
162
- if (!accessHandle) {
163
- throw new Error('file not found')
164
- }
190
+ if (accessHandle == null) return yield* Effect.dieMessage('file not found')
191
+
165
192
  // Subsequent methods are only passed the fileId, so make sure we have
166
193
  // a way to get the file resources.
167
194
  const file = { path, flags, accessHandle }
@@ -169,104 +196,153 @@ export class AccessHandlePoolVFS extends FacadeVFS {
169
196
 
170
197
  pOutFlags.setInt32(0, flags, true)
171
198
  return VFS.SQLITE_OK
172
- } catch (e: any) {
173
- console.error(e.message)
174
- return VFS.SQLITE_CANTOPEN
175
- }
199
+ }).pipe(
200
+ Effect.tapCauseLogPretty,
201
+ Effect.catchAllCause(() => Effect.succeed(VFS.SQLITE_CANTOPEN)),
202
+ Runtime.runSync(this.#runtime),
203
+ )
176
204
  }
177
205
 
178
- 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)
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
+ }
185
215
  }
186
- }
187
- 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
+ )
188
222
  }
189
223
 
190
- 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
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
+ )
200
240
  }
201
241
 
202
- 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
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
+ )
208
257
  }
209
258
 
210
- 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
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
+ )
214
269
  }
215
270
 
216
- jSync(fileId: number, _flags: number): number {
217
- const file = this.#mapIdToFile.get(fileId)!
218
- file.accessHandle.flush()
219
- 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
+ )
220
281
  }
221
282
 
222
- 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
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
+ )
227
295
  }
228
296
 
229
- jSectorSize(_fileId: number): number {
297
+ override jSectorSize(_fileId: number): number {
230
298
  return SECTOR_SIZE
231
299
  }
232
300
 
233
- jDeviceCharacteristics(_fileId: number): number {
301
+ override jDeviceCharacteristics(_fileId: number): number {
234
302
  return VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
235
303
  }
236
304
 
237
- 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
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
+ )
241
315
  }
242
316
 
243
- jDelete(zName: string, _syncDir: number): number {
244
- const path = this.#getPath(zName)
245
- this.#deletePath(path)
246
- 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
+ )
247
327
  }
248
328
 
249
- async close() {
250
- this.#releaseAccessHandles()
329
+ close() {
330
+ this.#releaseAccessHandles().pipe(Runtime.runPromise(this.#runtime))
251
331
  }
252
332
 
253
333
  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 })
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)
260
342
  }
261
343
  }
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
344
+ return true
345
+ }).pipe(Runtime.runPromise(this.#runtime))
270
346
  }
271
347
 
272
348
  /**
@@ -296,154 +372,173 @@ export class AccessHandlePoolVFS extends FacadeVFS {
296
372
  /**
297
373
  * Increase the capacity of the file system by n.
298
374
  */
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)
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)
311
384
 
312
- this.#setAssociatedPath(accessHandle, '', 0)
313
- }
314
- return n
315
- }
385
+ yield* this.#setAssociatedPath(accessHandle, '', 0)
386
+ }),
387
+ n,
388
+ ),
389
+ )
316
390
 
317
391
  /**
318
392
  * Decrease the capacity of the file system by n. The capacity cannot be
319
393
  * decreased to fewer than the current number of SQLite files in the
320
394
  * file system.
321
395
  */
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
- }
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
+ )
372
463
 
373
464
  /**
374
465
  * Read and return the associated path from an OPFS file header.
375
466
  * Empty string is returned for an unassociated OPFS file.
376
467
  * @returns {string} path or empty string
377
468
  */
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
- }
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
+ }
391
483
 
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)
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 ''
406
505
  }
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
- }
506
+ }),
507
+ )
415
508
 
416
509
  /**
417
510
  * Set the path on an OPFS file header.
418
511
  */
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
- }
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
+ }
426
520
 
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
- }
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
+ )
447
542
 
448
543
  /**
449
544
  * We need a synchronous digest function so can't use WebCrypto.
@@ -451,7 +546,7 @@ export class AccessHandlePoolVFS extends FacadeVFS {
451
546
  * @returns {ArrayBuffer} 64-bit digest
452
547
  */
453
548
  #computeDigest(corpus: Uint8Array): Uint32Array {
454
- if (!corpus[0]) {
549
+ if (corpus[0] === 0) {
455
550
  // Optimization for deleted file.
456
551
  return new Uint32Array([0xfe_cc_5f_80, 0xac_ce_c0_37])
457
552
  }
@@ -482,17 +577,14 @@ export class AccessHandlePoolVFS extends FacadeVFS {
482
577
  * Remove the association between a path and an OPFS file.
483
578
  * @param {string} path
484
579
  */
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
- }
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
+ )
493
590
  }
494
-
495
- export class OpfsError extends Schema.TaggedError<OpfsError>()('OpfsError', {
496
- cause: Schema.Defect,
497
- path: Schema.String,
498
- }) {}