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