@livestore/sqlite-wasm 0.4.0-dev.9 → 0.5.0-dev.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 -4
- package/dist/FacadeVFS.d.ts.map +1 -1
- package/dist/FacadeVFS.js +13 -17
- 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 +48 -42
- 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 +178 -166
- 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 +3 -3
- 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 +283 -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 +6 -5
- 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 +4 -4
- 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 +4 -4
- 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 +4 -4
- 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 +195 -180
- 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 +18 -9
- package/dist/make-sqlite-db.js.map +1 -1
- package/dist/node/NodeFS.d.ts.map +1 -1
- package/dist/node/NodeFS.js +14 -14
- package/dist/node/NodeFS.js.map +1 -1
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +3 -3
- package/dist/node/mod.js.map +1 -1
- package/package.json +48 -18
- package/src/FacadeVFS.ts +13 -17
- package/src/browser/mod.ts +38 -13
- package/src/browser/opfs/AccessHandlePoolVFS.ts +374 -268
- package/src/browser/opfs/index.ts +5 -4
- package/src/browser/opfs/opfs-sah-pool.ts +4 -4
- package/src/cf/CloudflareDurableObjectVFS.ts +324 -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 +63 -37
- package/src/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.ts +8 -5
- package/src/cf/test/async-storage/cloudflare-worker-vfs-core.test.ts +6 -4
- package/src/cf/test/async-storage/cloudflare-worker-vfs-integration.test.ts +6 -4
- package/src/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.ts +6 -4
- package/src/cf/test/sql/cloudflare-sql-vfs-core.test.ts +232 -198
- 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 +21 -9
- package/src/node/NodeFS.ts +25 -24
- package/src/node/mod.ts +3 -3
- 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 { type Context, Effect, 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
|
+
// Context for executing Effect operations.
|
|
71
|
+
readonly #services: Context.Context<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
|
|
79
|
-
const
|
|
80
|
-
|
|
86
|
+
static create = Effect.fn(function* (name: string, directoryPath: string, module: any) {
|
|
87
|
+
const services = yield* Effect.context<Opfs.Opfs | Scope.Scope>()
|
|
88
|
+
const vfs = new AccessHandlePoolVFS({ name, directoryPath, module, services })
|
|
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
|
+
services,
|
|
98
|
+
}: {
|
|
99
|
+
name: string
|
|
100
|
+
directoryPath: string
|
|
101
|
+
module: any
|
|
102
|
+
services: Context.Context<Opfs.Opfs | Scope.Scope>
|
|
103
|
+
}) {
|
|
85
104
|
super(name, module)
|
|
86
105
|
this.#directoryPath = directoryPath
|
|
106
|
+
this.#services = services
|
|
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({ self: 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,65 @@ 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)
|
|
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
|
-
}
|
|
135
|
+
readFilePayload = Effect.fn((zName: string) =>
|
|
136
|
+
Effect.gen({ self: this }, function* () {
|
|
137
|
+
const path = this.#getPath(zName)
|
|
138
|
+
const accessHandle = this.#mapPathToAccessHandle.get(path)
|
|
114
139
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
return payload.buffer
|
|
135
|
-
}
|
|
144
|
+
const opfs = yield* Opfs.Opfs
|
|
145
|
+
const fileSize = yield* opfs.syncGetSize(accessHandle)
|
|
146
|
+
if (fileSize <= HEADER_OFFSET_DATA) {
|
|
147
|
+
return shouldNeverHappen(
|
|
148
|
+
`OPFS file too small to contain header and payload: size ${fileSize} < HEADER_OFFSET_DATA ${HEADER_OFFSET_DATA}`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
136
151
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
152
|
+
const payloadSize = fileSize - HEADER_OFFSET_DATA
|
|
153
|
+
const payload = new Uint8Array(payloadSize)
|
|
154
|
+
const bytesRead = yield* opfs.syncRead(accessHandle, payload, { at: HEADER_OFFSET_DATA })
|
|
155
|
+
if (bytesRead !== payloadSize) {
|
|
156
|
+
return shouldNeverHappen(`Failed to read full payload from OPFS file: read ${bytesRead}/${payloadSize}`)
|
|
157
|
+
}
|
|
144
158
|
|
|
145
|
-
|
|
146
|
-
|
|
159
|
+
return payload.buffer
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
resetAccessHandle = Effect.fn((zName: string) =>
|
|
164
|
+
Effect.gen({ self: this }, function* () {
|
|
165
|
+
const path = this.#getPath(zName)
|
|
166
|
+
const accessHandle = this.#mapPathToAccessHandle.get(path)!
|
|
167
|
+
const opfs = yield* Opfs.Opfs
|
|
168
|
+
yield* opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA)
|
|
169
|
+
// accessHandle.write(new Uint8Array(), { at: HEADER_OFFSET_DATA })
|
|
170
|
+
// accessHandle.flush()
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
override jOpen(zName: string, fileId: number, flags: number, pOutFlags: DataView): number {
|
|
175
|
+
return Effect.gen({ self: this }, function* () {
|
|
147
176
|
// First try to open a path that already exists in the file system.
|
|
148
|
-
const
|
|
177
|
+
const name = zName as unknown
|
|
178
|
+
const path = typeof name === 'string' && name !== '' ? this.#getPath(name) : Math.random().toString(36)
|
|
149
179
|
let accessHandle = this.#mapPathToAccessHandle.get(path)
|
|
150
|
-
if (
|
|
180
|
+
if (accessHandle == null && (flags & VFS.SQLITE_OPEN_CREATE) !== 0) {
|
|
151
181
|
// File not found so try to create it.
|
|
152
182
|
if (this.getSize() < this.getCapacity()) {
|
|
153
183
|
// Choose an unassociated OPFS file from the pool.
|
|
154
184
|
;[accessHandle] = this.#availableAccessHandles.keys()
|
|
155
|
-
this.#setAssociatedPath(accessHandle!, path, flags)
|
|
185
|
+
yield* this.#setAssociatedPath(accessHandle!, path, flags)
|
|
156
186
|
} else {
|
|
157
187
|
// Out of unassociated files. This can be fixed by calling
|
|
158
188
|
// addCapacity() from the application.
|
|
159
|
-
|
|
189
|
+
return yield* Effect.die(new Error('cannot create file'))
|
|
160
190
|
}
|
|
161
191
|
}
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
}
|
|
192
|
+
if (accessHandle == null) return yield* Effect.die(new Error('file not found'))
|
|
193
|
+
|
|
165
194
|
// Subsequent methods are only passed the fileId, so make sure we have
|
|
166
195
|
// a way to get the file resources.
|
|
167
196
|
const file = { path, flags, accessHandle }
|
|
@@ -169,104 +198,159 @@ export class AccessHandlePoolVFS extends FacadeVFS {
|
|
|
169
198
|
|
|
170
199
|
pOutFlags.setInt32(0, flags, true)
|
|
171
200
|
return VFS.SQLITE_OK
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
201
|
+
}).pipe(
|
|
202
|
+
Effect.tapCauseLogPretty,
|
|
203
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_CANTOPEN)),
|
|
204
|
+
Effect.runSyncWith(this.#services),
|
|
205
|
+
)
|
|
176
206
|
}
|
|
177
207
|
|
|
178
|
-
jClose(fileId: number): number {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
file
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
this.#
|
|
208
|
+
override jClose(fileId: number): number {
|
|
209
|
+
return Effect.gen({ self: this }, function* () {
|
|
210
|
+
const file = this.#mapIdToFile.get(fileId)
|
|
211
|
+
if (file !== undefined) {
|
|
212
|
+
const opfs = yield* Opfs.Opfs
|
|
213
|
+
yield* opfs.syncFlush(file.accessHandle)
|
|
214
|
+
this.#mapIdToFile.delete(fileId)
|
|
215
|
+
if ((file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) !== 0) {
|
|
216
|
+
yield* this.#deletePath(file.path)
|
|
217
|
+
}
|
|
185
218
|
}
|
|
186
|
-
|
|
187
|
-
|
|
219
|
+
return VFS.SQLITE_OK
|
|
220
|
+
}).pipe(
|
|
221
|
+
Effect.tapCauseLogPretty,
|
|
222
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_IOERR_CLOSE)),
|
|
223
|
+
Effect.runSyncWith(this.#services),
|
|
224
|
+
)
|
|
188
225
|
}
|
|
189
226
|
|
|
190
|
-
jRead(fileId: number, pData: Uint8Array<ArrayBuffer>, iOffset: number): number {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
override jRead(fileId: number, pData: Uint8Array<ArrayBuffer>, iOffset: number): number {
|
|
228
|
+
return Effect.gen({ self: this }, function* () {
|
|
229
|
+
const file = this.#mapIdToFile.get(fileId)!
|
|
230
|
+
const opfs = yield* Opfs.Opfs
|
|
231
|
+
const nBytes = yield* opfs.syncRead(file.accessHandle, pData.subarray(), {
|
|
232
|
+
at: HEADER_OFFSET_DATA + iOffset,
|
|
233
|
+
})
|
|
234
|
+
if (nBytes < pData.byteLength) {
|
|
235
|
+
pData.fill(0, nBytes, pData.byteLength)
|
|
236
|
+
return VFS.SQLITE_IOERR_SHORT_READ
|
|
237
|
+
}
|
|
238
|
+
return VFS.SQLITE_OK
|
|
239
|
+
}).pipe(
|
|
240
|
+
Effect.tapCauseLogPretty,
|
|
241
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_IOERR_READ)),
|
|
242
|
+
Effect.runSyncWith(this.#services),
|
|
243
|
+
)
|
|
200
244
|
}
|
|
201
245
|
|
|
202
|
-
jWrite(fileId: number, pData: Uint8Array<ArrayBuffer>, iOffset: number): number {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
246
|
+
override jWrite(fileId: number, pData: Uint8Array<ArrayBuffer>, iOffset: number): number {
|
|
247
|
+
return Effect.gen({ self: this }, function* () {
|
|
248
|
+
const file = this.#mapIdToFile.get(fileId)!
|
|
249
|
+
const opfs = yield* Opfs.Opfs
|
|
250
|
+
const nBytes = yield* opfs.syncWrite(file.accessHandle, pData.subarray(), {
|
|
251
|
+
at: HEADER_OFFSET_DATA + iOffset,
|
|
252
|
+
})
|
|
253
|
+
if (nBytes !== pData.byteLength) {
|
|
254
|
+
return yield* Effect.die(new Error(`Wrote ${nBytes} bytes, expected ${pData.byteLength}`))
|
|
255
|
+
}
|
|
256
|
+
return VFS.SQLITE_OK
|
|
257
|
+
}).pipe(
|
|
258
|
+
Effect.tapCauseLogPretty,
|
|
259
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_IOERR_WRITE)),
|
|
260
|
+
Effect.runSyncWith(this.#services),
|
|
261
|
+
)
|
|
208
262
|
}
|
|
209
263
|
|
|
210
|
-
jTruncate(fileId: number, iSize: number): number {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
264
|
+
override jTruncate(fileId: number, iSize: number): number {
|
|
265
|
+
return Effect.gen({ self: this }, function* () {
|
|
266
|
+
const file = this.#mapIdToFile.get(fileId)!
|
|
267
|
+
const opfs = yield* Opfs.Opfs
|
|
268
|
+
yield* opfs.syncTruncate(file.accessHandle, HEADER_OFFSET_DATA + iSize)
|
|
269
|
+
return VFS.SQLITE_OK
|
|
270
|
+
}).pipe(
|
|
271
|
+
Effect.tapCauseLogPretty,
|
|
272
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_IOERR_TRUNCATE)),
|
|
273
|
+
Effect.runSyncWith(this.#services),
|
|
274
|
+
)
|
|
214
275
|
}
|
|
215
276
|
|
|
216
|
-
jSync(fileId: number, _flags: number): number {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
277
|
+
override jSync(fileId: number, _flags: number): number {
|
|
278
|
+
return Effect.gen({ self: this }, function* () {
|
|
279
|
+
const file = this.#mapIdToFile.get(fileId)!
|
|
280
|
+
const opfs = yield* Opfs.Opfs
|
|
281
|
+
yield* opfs.syncFlush(file.accessHandle)
|
|
282
|
+
return VFS.SQLITE_OK
|
|
283
|
+
}).pipe(
|
|
284
|
+
Effect.tapCauseLogPretty,
|
|
285
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_IOERR_FSYNC)),
|
|
286
|
+
Effect.runSyncWith(this.#services),
|
|
287
|
+
)
|
|
220
288
|
}
|
|
221
289
|
|
|
222
|
-
jFileSize(fileId: number, pSize64: DataView): number {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
290
|
+
override jFileSize(fileId: number, pSize64: DataView): number {
|
|
291
|
+
return Effect.gen({ self: this }, function* () {
|
|
292
|
+
const file = this.#mapIdToFile.get(fileId)!
|
|
293
|
+
const opfs = yield* Opfs.Opfs
|
|
294
|
+
const opfsFileSize = yield* opfs.syncGetSize(file.accessHandle)
|
|
295
|
+
const size = opfsFileSize - HEADER_OFFSET_DATA
|
|
296
|
+
pSize64.setBigInt64(0, BigInt(size), true)
|
|
297
|
+
return VFS.SQLITE_OK
|
|
298
|
+
}).pipe(
|
|
299
|
+
Effect.tapCauseLogPretty,
|
|
300
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_IOERR_FSTAT)),
|
|
301
|
+
Effect.runSyncWith(this.#services),
|
|
302
|
+
)
|
|
227
303
|
}
|
|
228
304
|
|
|
229
|
-
jSectorSize(_fileId: number): number {
|
|
305
|
+
override jSectorSize(_fileId: number): number {
|
|
230
306
|
return SECTOR_SIZE
|
|
231
307
|
}
|
|
232
308
|
|
|
233
|
-
jDeviceCharacteristics(_fileId: number): number {
|
|
309
|
+
override jDeviceCharacteristics(_fileId: number): number {
|
|
234
310
|
return VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
|
|
235
311
|
}
|
|
236
312
|
|
|
237
|
-
jAccess(zName: string, _flags: number, pResOut: DataView): number {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
313
|
+
override jAccess(zName: string, _flags: number, pResOut: DataView): number {
|
|
314
|
+
return Effect.gen({ self: this }, function* () {
|
|
315
|
+
const path = this.#getPath(zName)
|
|
316
|
+
pResOut.setInt32(0, this.#mapPathToAccessHandle.has(path) === true ? 1 : 0, true)
|
|
317
|
+
return VFS.SQLITE_OK
|
|
318
|
+
}).pipe(
|
|
319
|
+
Effect.tapCauseLogPretty,
|
|
320
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_IOERR_ACCESS)),
|
|
321
|
+
Effect.runSyncWith(this.#services),
|
|
322
|
+
)
|
|
241
323
|
}
|
|
242
324
|
|
|
243
|
-
jDelete(zName: string, _syncDir: number): number {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
325
|
+
override jDelete(zName: string, _syncDir: number): number {
|
|
326
|
+
return Effect.gen({ self: this }, function* () {
|
|
327
|
+
const path = this.#getPath(zName)
|
|
328
|
+
yield* this.#deletePath(path)
|
|
329
|
+
return VFS.SQLITE_OK
|
|
330
|
+
}).pipe(
|
|
331
|
+
Effect.tapCauseLogPretty,
|
|
332
|
+
Effect.catchCause(() => Effect.succeed(VFS.SQLITE_IOERR_DELETE)),
|
|
333
|
+
Effect.runSyncWith(this.#services),
|
|
334
|
+
)
|
|
247
335
|
}
|
|
248
336
|
|
|
249
|
-
|
|
250
|
-
this.#releaseAccessHandles()
|
|
337
|
+
close() {
|
|
338
|
+
this.#releaseAccessHandles().pipe(Effect.runPromiseWith(this.#services))
|
|
251
339
|
}
|
|
252
340
|
|
|
253
341
|
async isReady() {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
342
|
+
return Effect.gen({ self: this }, function* () {
|
|
343
|
+
if (this.#directoryHandle == null) {
|
|
344
|
+
// All files are stored in a single directory.
|
|
345
|
+
this.#directoryHandle = yield* Opfs.getDirectoryHandleByPath(this.#directoryPath, { create: true })
|
|
346
|
+
|
|
347
|
+
yield* this.#acquireAccessHandles()
|
|
348
|
+
if (this.getCapacity() === 0) {
|
|
349
|
+
yield* this.addCapacity(DEFAULT_CAPACITY)
|
|
260
350
|
}
|
|
261
351
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
await this.#acquireAccessHandles()
|
|
265
|
-
if (this.getCapacity() === 0) {
|
|
266
|
-
await this.addCapacity(DEFAULT_CAPACITY)
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return true
|
|
352
|
+
return true
|
|
353
|
+
}).pipe(Effect.runPromiseWith(this.#services))
|
|
270
354
|
}
|
|
271
355
|
|
|
272
356
|
/**
|
|
@@ -296,154 +380,179 @@ export class AccessHandlePoolVFS extends FacadeVFS {
|
|
|
296
380
|
/**
|
|
297
381
|
* Increase the capacity of the file system by n.
|
|
298
382
|
*/
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}).pipe(Effect.retry(Schedule.exponentialBackoff10Sec), Effect.runPromise)
|
|
310
|
-
this.#mapAccessHandleToName.set(accessHandle, name)
|
|
383
|
+
addCapacity: (n: number) => Effect.Effect<void, WebError.WebError, Opfs.Opfs | Scope.Scope> = Effect.fn((n: number) =>
|
|
384
|
+
Effect.replicateEffect(
|
|
385
|
+
Effect.gen({ self: this }, function* () {
|
|
386
|
+
const name = Math.random().toString(36).replace('0.', '')
|
|
387
|
+
const opfs = yield* Opfs.Opfs
|
|
388
|
+
const accessHandle = yield* opfs.getFileHandle(this.#directoryHandle!, name, { create: true }).pipe(
|
|
389
|
+
Effect.andThen((handle) => opfs.createSyncAccessHandle(handle)),
|
|
390
|
+
Effect.retry(Schedule.exponentialBackoff10Sec),
|
|
391
|
+
)
|
|
392
|
+
this.#mapAccessHandleToName.set(accessHandle, name)
|
|
311
393
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
394
|
+
yield* this.#setAssociatedPath(accessHandle, '', 0)
|
|
395
|
+
}),
|
|
396
|
+
n,
|
|
397
|
+
{ discard: true },
|
|
398
|
+
),
|
|
399
|
+
)
|
|
316
400
|
|
|
317
401
|
/**
|
|
318
402
|
* Decrease the capacity of the file system by n. The capacity cannot be
|
|
319
403
|
* decreased to fewer than the current number of SQLite files in the
|
|
320
404
|
* file system.
|
|
321
405
|
*/
|
|
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
|
-
|
|
406
|
+
removeCapacity = Effect.fn((n: number) =>
|
|
407
|
+
Effect.gen({ self: this }, function* () {
|
|
408
|
+
let nRemoved = 0
|
|
409
|
+
yield* Effect.forEach(
|
|
410
|
+
this.#availableAccessHandles,
|
|
411
|
+
(accessHandle) =>
|
|
412
|
+
Effect.gen({ self: this }, function* () {
|
|
413
|
+
if (nRemoved === n || this.getSize() === this.getCapacity()) return nRemoved
|
|
414
|
+
|
|
415
|
+
const name = this.#mapAccessHandleToName.get(accessHandle)!
|
|
416
|
+
accessHandle.close()
|
|
417
|
+
const opfs = yield* Opfs.Opfs
|
|
418
|
+
yield* opfs.removeEntry(this.#directoryHandle!, name)
|
|
419
|
+
this.#mapAccessHandleToName.delete(accessHandle)
|
|
420
|
+
this.#availableAccessHandles.delete(accessHandle)
|
|
421
|
+
++nRemoved
|
|
422
|
+
return nRemoved
|
|
423
|
+
}),
|
|
424
|
+
{ discard: true },
|
|
425
|
+
)
|
|
426
|
+
return nRemoved
|
|
427
|
+
}),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
#acquireAccessHandles = Effect.fn(() =>
|
|
431
|
+
Effect.gen({ self: this }, function* () {
|
|
432
|
+
const opfs = yield* Opfs.Opfs
|
|
433
|
+
const handlesStream = opfs.values(this.#directoryHandle!)
|
|
434
|
+
|
|
435
|
+
yield* handlesStream.pipe(
|
|
436
|
+
Stream.filter((handle): handle is FileSystemFileHandle => handle.kind === 'file'),
|
|
437
|
+
Stream.mapEffect(
|
|
438
|
+
(fileHandle) =>
|
|
439
|
+
Effect.gen({ self: this }, function* () {
|
|
440
|
+
const accessHandle = yield* opfs.createSyncAccessHandle(fileHandle)
|
|
441
|
+
return {
|
|
442
|
+
accessHandle,
|
|
443
|
+
opfsFileName: fileHandle.name,
|
|
444
|
+
path: yield* this.#getAssociatedPath(accessHandle),
|
|
445
|
+
} as const
|
|
446
|
+
}),
|
|
447
|
+
{ concurrency: 'unbounded' },
|
|
448
|
+
),
|
|
449
|
+
Stream.runForEach(({ opfsFileName, accessHandle, path }) =>
|
|
450
|
+
Effect.gen({ self: this }, function* () {
|
|
451
|
+
this.#mapAccessHandleToName.set(accessHandle, opfsFileName)
|
|
452
|
+
if (path !== '') {
|
|
453
|
+
this.#mapPathToAccessHandle.set(path, accessHandle)
|
|
454
|
+
} else {
|
|
455
|
+
this.#availableAccessHandles.add(accessHandle)
|
|
456
|
+
}
|
|
457
|
+
}),
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
}),
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
#releaseAccessHandles = Effect.fn(() =>
|
|
464
|
+
Effect.gen({ self: this }, function* () {
|
|
465
|
+
yield* Effect.forEach(
|
|
466
|
+
this.#mapAccessHandleToName.keys(),
|
|
467
|
+
(accessHandle) => Effect.sync(() => accessHandle.close()),
|
|
468
|
+
{ concurrency: 'unbounded', discard: true },
|
|
469
|
+
)
|
|
470
|
+
this.#mapAccessHandleToName.clear()
|
|
471
|
+
this.#mapPathToAccessHandle.clear()
|
|
472
|
+
this.#availableAccessHandles.clear()
|
|
473
|
+
}),
|
|
474
|
+
)
|
|
372
475
|
|
|
373
476
|
/**
|
|
374
477
|
* Read and return the associated path from an OPFS file header.
|
|
375
478
|
* Empty string is returned for an unassociated OPFS file.
|
|
376
479
|
* @returns {string} path or empty string
|
|
377
480
|
*/
|
|
378
|
-
#getAssociatedPath(accessHandle: FileSystemSyncAccessHandle)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
481
|
+
#getAssociatedPath = Effect.fn((accessHandle: FileSystemSyncAccessHandle) =>
|
|
482
|
+
Effect.gen({ self: this }, function* () {
|
|
483
|
+
// Read the path and digest of the path from the file.
|
|
484
|
+
const corpus = new Uint8Array(HEADER_CORPUS_SIZE)
|
|
485
|
+
const opfs = yield* Opfs.Opfs
|
|
486
|
+
yield* opfs.syncRead(accessHandle, corpus, { at: 0 })
|
|
487
|
+
|
|
488
|
+
// Delete files not expected to be present.
|
|
489
|
+
const dataView = new DataView(corpus.buffer, corpus.byteOffset)
|
|
490
|
+
const flags = dataView.getUint32(HEADER_OFFSET_FLAGS)
|
|
491
|
+
if (corpus[0] !== 0 && ((flags & VFS.SQLITE_OPEN_DELETEONCLOSE) !== 0 || (flags & PERSISTENT_FILE_TYPES) === 0)) {
|
|
492
|
+
yield* Effect.logWarning(`Remove file with unexpected flags ${flags.toString(16)}`)
|
|
493
|
+
yield* this.#setAssociatedPath(accessHandle, '', 0)
|
|
494
|
+
return ''
|
|
495
|
+
}
|
|
391
496
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
497
|
+
const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4)
|
|
498
|
+
yield* opfs.syncRead(accessHandle, fileDigest, { at: HEADER_OFFSET_DIGEST })
|
|
499
|
+
|
|
500
|
+
// Verify the digest.
|
|
501
|
+
const computedDigest = this.#computeDigest(corpus)
|
|
502
|
+
if (fileDigest.every((value, i) => value === computedDigest[i]) === true) {
|
|
503
|
+
// Good digest. Decode the null-terminated path string.
|
|
504
|
+
const pathBytes = corpus.indexOf(0)
|
|
505
|
+
if (pathBytes === 0) {
|
|
506
|
+
// Ensure that unassociated files are empty. Unassociated files are
|
|
507
|
+
// truncated in #setAssociatedPath after the header is written. If
|
|
508
|
+
// an interruption occurs right before the truncation then garbage
|
|
509
|
+
// may remain in the file.
|
|
510
|
+
yield* opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA)
|
|
511
|
+
}
|
|
512
|
+
return new TextDecoder().decode(corpus.subarray(0, pathBytes))
|
|
513
|
+
} else {
|
|
514
|
+
// Bad digest. Repair this header.
|
|
515
|
+
yield* Effect.logWarning('Disassociating file with bad digest.')
|
|
516
|
+
yield* this.#setAssociatedPath(accessHandle, '', 0)
|
|
517
|
+
return ''
|
|
406
518
|
}
|
|
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
|
-
}
|
|
519
|
+
}),
|
|
520
|
+
)
|
|
415
521
|
|
|
416
522
|
/**
|
|
417
523
|
* Set the path on an OPFS file header.
|
|
418
524
|
*/
|
|
419
|
-
#setAssociatedPath(accessHandle: FileSystemSyncAccessHandle, path: string, flags: number)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
525
|
+
#setAssociatedPath = Effect.fn((accessHandle: FileSystemSyncAccessHandle, path: string, flags: number) =>
|
|
526
|
+
Effect.gen({ self: this }, function* () {
|
|
527
|
+
// Convert the path string to UTF-8.
|
|
528
|
+
const corpus = new Uint8Array(HEADER_CORPUS_SIZE)
|
|
529
|
+
const encodedResult = new TextEncoder().encodeInto(path, corpus)
|
|
530
|
+
if (encodedResult.written >= HEADER_MAX_PATH_SIZE) {
|
|
531
|
+
return yield* Effect.die(new Error('path too long'))
|
|
532
|
+
}
|
|
426
533
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
534
|
+
// Add the creation flags.
|
|
535
|
+
const dataView = new DataView(corpus.buffer, corpus.byteOffset)
|
|
536
|
+
dataView.setUint32(HEADER_OFFSET_FLAGS, flags)
|
|
537
|
+
|
|
538
|
+
// Write the OPFS file header, including the digest.
|
|
539
|
+
const digest = this.#computeDigest(corpus)
|
|
540
|
+
const opfs = yield* Opfs.Opfs
|
|
541
|
+
yield* opfs.syncWrite(accessHandle, corpus, { at: 0 })
|
|
542
|
+
yield* opfs.syncWrite(accessHandle, digest, { at: HEADER_OFFSET_DIGEST })
|
|
543
|
+
yield* opfs.syncFlush(accessHandle)
|
|
544
|
+
|
|
545
|
+
if (path !== '') {
|
|
546
|
+
this.#mapPathToAccessHandle.set(path, accessHandle)
|
|
547
|
+
this.#availableAccessHandles.delete(accessHandle)
|
|
548
|
+
} else {
|
|
549
|
+
// This OPFS file doesn't represent any SQLite file so it doesn't
|
|
550
|
+
// need to keep any data.
|
|
551
|
+
yield* opfs.syncTruncate(accessHandle, HEADER_OFFSET_DATA)
|
|
552
|
+
this.#availableAccessHandles.add(accessHandle)
|
|
553
|
+
}
|
|
554
|
+
}),
|
|
555
|
+
)
|
|
447
556
|
|
|
448
557
|
/**
|
|
449
558
|
* We need a synchronous digest function so can't use WebCrypto.
|
|
@@ -451,7 +560,7 @@ export class AccessHandlePoolVFS extends FacadeVFS {
|
|
|
451
560
|
* @returns {ArrayBuffer} 64-bit digest
|
|
452
561
|
*/
|
|
453
562
|
#computeDigest(corpus: Uint8Array): Uint32Array {
|
|
454
|
-
if (
|
|
563
|
+
if (corpus[0] === 0) {
|
|
455
564
|
// Optimization for deleted file.
|
|
456
565
|
return new Uint32Array([0xfe_cc_5f_80, 0xac_ce_c0_37])
|
|
457
566
|
}
|
|
@@ -482,17 +591,14 @@ export class AccessHandlePoolVFS extends FacadeVFS {
|
|
|
482
591
|
* Remove the association between a path and an OPFS file.
|
|
483
592
|
* @param {string} path
|
|
484
593
|
*/
|
|
485
|
-
#deletePath(path: string)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
594
|
+
#deletePath = Effect.fn((path: string) =>
|
|
595
|
+
Effect.gen({ self: this }, function* () {
|
|
596
|
+
const accessHandle = this.#mapPathToAccessHandle.get(path)
|
|
597
|
+
if (accessHandle !== undefined) {
|
|
598
|
+
// Un-associate the SQLite path from the OPFS file.
|
|
599
|
+
this.#mapPathToAccessHandle.delete(path)
|
|
600
|
+
yield* this.#setAssociatedPath(accessHandle, '', 0)
|
|
601
|
+
}
|
|
602
|
+
}),
|
|
603
|
+
)
|
|
493
604
|
}
|
|
494
|
-
|
|
495
|
-
export class OpfsError extends Schema.TaggedError<OpfsError>()('OpfsError', {
|
|
496
|
-
cause: Schema.Defect,
|
|
497
|
-
path: Schema.String,
|
|
498
|
-
}) {}
|