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