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