@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.
- package/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/FacadeVFS.d.ts +0 -1
- package/dist/FacadeVFS.d.ts.map +1 -1
- package/dist/FacadeVFS.js +13 -14
- package/dist/FacadeVFS.js.map +1 -1
- package/dist/browser/mod.d.ts +13 -6
- 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 +33 -17
- package/dist/browser/opfs/AccessHandlePoolVFS.d.ts.map +1 -1
- package/dist/browser/opfs/AccessHandlePoolVFS.js +187 -152
- 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 +4 -4
- package/dist/browser/opfs/opfs-sah-pool.js.map +1 -1
- package/dist/cf/CloudflareDurableObjectVFS.d.ts +104 -0
- package/dist/cf/CloudflareDurableObjectVFS.d.ts.map +1 -0
- package/dist/cf/CloudflareDurableObjectVFS.js +281 -0
- package/dist/cf/CloudflareDurableObjectVFS.js.map +1 -0
- package/dist/cf/CloudflareWorkerVFS.d.ts.map +1 -1
- package/dist/cf/CloudflareWorkerVFS.js +29 -28
- package/dist/cf/CloudflareWorkerVFS.js.map +1 -1
- package/dist/cf/mod.d.ts +3 -4
- package/dist/cf/mod.d.ts.map +1 -1
- package/dist/cf/mod.js +6 -14
- package/dist/cf/mod.js.map +1 -1
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js +5 -4
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js.map +1 -1
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js +3 -3
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js.map +1 -1
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js +21 -3
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js.map +1 -1
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js +3 -3
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js.map +1 -1
- package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js +194 -179
- package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js.map +1 -1
- package/dist/in-memory-vfs.d.ts.map +1 -1
- package/dist/in-memory-vfs.js +0 -1
- package/dist/in-memory-vfs.js.map +1 -1
- package/dist/load-wasm/mod.node.js +1 -1
- package/dist/load-wasm/mod.node.js.map +1 -1
- package/dist/load-wasm/mod.workerd.d.ts.map +1 -1
- package/dist/load-wasm/mod.workerd.js +2 -0
- package/dist/load-wasm/mod.workerd.js.map +1 -1
- package/dist/make-sqlite-db.d.ts.map +1 -1
- package/dist/make-sqlite-db.js +45 -12
- package/dist/make-sqlite-db.js.map +1 -1
- package/dist/node/NodeFS.d.ts.map +1 -1
- package/dist/node/NodeFS.js +13 -13
- package/dist/node/NodeFS.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 +56 -17
- package/src/FacadeVFS.ts +14 -14
- package/src/browser/mod.ts +38 -13
- package/src/browser/opfs/AccessHandlePoolVFS.ts +374 -240
- package/src/browser/opfs/index.ts +4 -3
- package/src/browser/opfs/opfs-sah-pool.ts +4 -4
- package/src/cf/CloudflareDurableObjectVFS.ts +325 -0
- package/src/cf/CloudflareWorkerVFS.ts +41 -39
- package/src/cf/README.md +3 -3
- package/src/cf/mod.ts +12 -17
- package/src/cf/test/README.md +55 -26
- package/src/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.ts +6 -4
- package/src/cf/test/async-storage/cloudflare-worker-vfs-core.test.ts +4 -3
- package/src/cf/test/async-storage/cloudflare-worker-vfs-integration.test.ts +22 -3
- package/src/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.ts +4 -3
- package/src/cf/test/sql/cloudflare-sql-vfs-core.test.ts +228 -197
- package/src/in-memory-vfs.ts +0 -1
- package/src/load-wasm/mod.node.ts +1 -1
- package/src/load-wasm/mod.workerd.ts +2 -1
- package/src/make-sqlite-db.ts +62 -12
- package/src/node/NodeFS.ts +24 -23
- package/src/node/mod.ts +2 -2
- package/dist/cf/BlockManager.d.ts +0 -61
- package/dist/cf/BlockManager.d.ts.map +0 -1
- package/dist/cf/BlockManager.js +0 -157
- package/dist/cf/BlockManager.js.map +0 -1
- package/dist/cf/CloudflareSqlVFS.d.ts +0 -51
- package/dist/cf/CloudflareSqlVFS.d.ts.map +0 -1
- package/dist/cf/CloudflareSqlVFS.js +0 -351
- package/dist/cf/CloudflareSqlVFS.js.map +0 -1
- package/src/ambient.d.ts +0 -18
- package/src/cf/BlockManager.ts +0 -225
- 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
|
-
// -
|
|
43
|
-
// - Safety buffer for future SQLite versions and unpredictable usage =
|
|
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
|
|
79
|
-
const
|
|
80
|
-
|
|
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(
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
187
|
+
return yield* Effect.dieMessage('cannot create file')
|
|
118
188
|
}
|
|
119
189
|
}
|
|
120
|
-
if (
|
|
121
|
-
|
|
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
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
file
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
this.#releaseAccessHandles()
|
|
329
|
+
close() {
|
|
330
|
+
this.#releaseAccessHandles().pipe(Runtime.runPromise(this.#runtime))
|
|
209
331
|
}
|
|
210
332
|
|
|
211
333
|
async isReady() {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
create: true
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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 (
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
}) {}
|