@livestore/sqlite-wasm 0.0.0-snapshot-1d99fea7d2ce2c7a5d9ed0a3752f8a7bda6bc3db

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.
Files changed (69) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/FacadeVFS.d.ts +243 -0
  3. package/dist/FacadeVFS.d.ts.map +1 -0
  4. package/dist/FacadeVFS.js +474 -0
  5. package/dist/FacadeVFS.js.map +1 -0
  6. package/dist/browser/mod.d.ts +44 -0
  7. package/dist/browser/mod.d.ts.map +1 -0
  8. package/dist/browser/mod.js +51 -0
  9. package/dist/browser/mod.js.map +1 -0
  10. package/dist/browser/opfs/AccessHandlePoolVFS.d.ts +47 -0
  11. package/dist/browser/opfs/AccessHandlePoolVFS.d.ts.map +1 -0
  12. package/dist/browser/opfs/AccessHandlePoolVFS.js +355 -0
  13. package/dist/browser/opfs/AccessHandlePoolVFS.js.map +1 -0
  14. package/dist/browser/opfs/index.d.ts +12 -0
  15. package/dist/browser/opfs/index.d.ts.map +1 -0
  16. package/dist/browser/opfs/index.js +19 -0
  17. package/dist/browser/opfs/index.js.map +1 -0
  18. package/dist/browser/opfs/opfs-sah-pool.d.ts +3 -0
  19. package/dist/browser/opfs/opfs-sah-pool.d.ts.map +1 -0
  20. package/dist/browser/opfs/opfs-sah-pool.js +55 -0
  21. package/dist/browser/opfs/opfs-sah-pool.js.map +1 -0
  22. package/dist/in-memory-vfs.d.ts +7 -0
  23. package/dist/in-memory-vfs.d.ts.map +1 -0
  24. package/dist/in-memory-vfs.js +15 -0
  25. package/dist/in-memory-vfs.js.map +1 -0
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +2 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/index_.d.ts +3 -0
  31. package/dist/index_.d.ts.map +1 -0
  32. package/dist/index_.js +3 -0
  33. package/dist/index_.js.map +1 -0
  34. package/dist/load-wasm/mod.browser.d.ts +2 -0
  35. package/dist/load-wasm/mod.browser.d.ts.map +1 -0
  36. package/dist/load-wasm/mod.browser.js +12 -0
  37. package/dist/load-wasm/mod.browser.js.map +1 -0
  38. package/dist/load-wasm/mod.node.d.ts +2 -0
  39. package/dist/load-wasm/mod.node.d.ts.map +1 -0
  40. package/dist/load-wasm/mod.node.js +13 -0
  41. package/dist/load-wasm/mod.node.js.map +1 -0
  42. package/dist/make-sqlite-db.d.ts +11 -0
  43. package/dist/make-sqlite-db.d.ts.map +1 -0
  44. package/dist/make-sqlite-db.js +181 -0
  45. package/dist/make-sqlite-db.js.map +1 -0
  46. package/dist/node/NodeFS.d.ts +20 -0
  47. package/dist/node/NodeFS.d.ts.map +1 -0
  48. package/dist/node/NodeFS.js +174 -0
  49. package/dist/node/NodeFS.js.map +1 -0
  50. package/dist/node/mod.d.ts +41 -0
  51. package/dist/node/mod.d.ts.map +1 -0
  52. package/dist/node/mod.js +61 -0
  53. package/dist/node/mod.js.map +1 -0
  54. package/package.json +38 -0
  55. package/src/FacadeVFS.ts +510 -0
  56. package/src/ambient.d.ts +18 -0
  57. package/src/browser/mod.ts +109 -0
  58. package/src/browser/opfs/AccessHandlePoolVFS.ts +404 -0
  59. package/src/browser/opfs/index.ts +35 -0
  60. package/src/browser/opfs/opfs-sah-pool.ts +68 -0
  61. package/src/in-memory-vfs.ts +20 -0
  62. package/src/index.ts +1 -0
  63. package/src/index_.ts +2 -0
  64. package/src/load-wasm/mod.browser.ts +12 -0
  65. package/src/load-wasm/mod.node.ts +13 -0
  66. package/src/make-sqlite-db.ts +220 -0
  67. package/src/node/NodeFS.ts +190 -0
  68. package/src/node/mod.ts +132 -0
  69. package/tsconfig.json +10 -0
@@ -0,0 +1,404 @@
1
+ /* eslint-disable prefer-arrow/prefer-arrow-functions */
2
+
3
+ // Based on https://github.com/rhashimoto/wa-sqlite/blob/master/src/examples/AccessHandlePoolVFS.js
4
+ import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
5
+
6
+ import { FacadeVFS } from '../../FacadeVFS.js'
7
+
8
+ const SECTOR_SIZE = 4096
9
+
10
+ // Each OPFS file begins with a fixed-size header with metadata. The
11
+ // contents of the file follow immediately after the header.
12
+ const HEADER_MAX_PATH_SIZE = 512
13
+ const HEADER_FLAGS_SIZE = 4
14
+ const HEADER_DIGEST_SIZE = 8
15
+ const HEADER_CORPUS_SIZE = HEADER_MAX_PATH_SIZE + HEADER_FLAGS_SIZE
16
+ const HEADER_OFFSET_FLAGS = HEADER_MAX_PATH_SIZE
17
+ const HEADER_OFFSET_DIGEST = HEADER_CORPUS_SIZE
18
+ const HEADER_OFFSET_DATA = SECTOR_SIZE
19
+
20
+ // These file types are expected to persist in the file system outside
21
+ // a session. Other files will be removed on VFS start.
22
+ const PERSISTENT_FILE_TYPES =
23
+ VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_MAIN_JOURNAL | VFS.SQLITE_OPEN_SUPER_JOURNAL | VFS.SQLITE_OPEN_WAL
24
+
25
+ const DEFAULT_CAPACITY = 6
26
+
27
+ /**
28
+ * This VFS uses the updated Access Handle API with all synchronous methods
29
+ * on FileSystemSyncAccessHandle (instead of just read and write). It will
30
+ * work with the regular SQLite WebAssembly build, i.e. the one without
31
+ * Asyncify.
32
+ */
33
+ export class AccessHandlePoolVFS extends FacadeVFS {
34
+ log = null //function(...args) { console.log(`[${contextName}]`, ...args) };
35
+
36
+ // All the OPFS files the VFS uses are contained in one flat directory
37
+ // specified in the constructor. No other files should be written here.
38
+ #directoryPath
39
+ #directoryHandle: FileSystemDirectoryHandle | undefined
40
+
41
+ // The OPFS files all have randomly-generated names that do not match
42
+ // the SQLite files whose data they contain. This map links those names
43
+ // with their respective OPFS access handles.
44
+ #mapAccessHandleToName = new Map<FileSystemSyncAccessHandle, string>()
45
+
46
+ // When a SQLite file is associated with an OPFS file, that association
47
+ // is kept in #mapPathToAccessHandle. Each access handle is in exactly
48
+ // one of #mapPathToAccessHandle or #availableAccessHandles.
49
+ #mapPathToAccessHandle = new Map<string, FileSystemSyncAccessHandle>()
50
+ #availableAccessHandles = new Set<FileSystemSyncAccessHandle>()
51
+
52
+ #mapIdToFile = new Map<number, { path: string; flags: number; accessHandle: FileSystemSyncAccessHandle }>()
53
+
54
+ static async create(name: string, directoryPath: string, module: any) {
55
+ const vfs = new AccessHandlePoolVFS(name, directoryPath, module)
56
+ await vfs.isReady()
57
+ return vfs
58
+ }
59
+
60
+ constructor(name: string, directoryPath: string, module: any) {
61
+ super(name, module)
62
+ this.#directoryPath = directoryPath
63
+ }
64
+
65
+ getOpfsFileName(zName: string) {
66
+ const path = this.#getPath(zName)
67
+ const accessHandle = this.#mapPathToAccessHandle.get(path)!
68
+ return this.#mapAccessHandleToName.get(accessHandle)!
69
+ }
70
+
71
+ resetAccessHandle(zName: string) {
72
+ const path = this.#getPath(zName)
73
+ const accessHandle = this.#mapPathToAccessHandle.get(path)!
74
+ accessHandle.truncate(HEADER_OFFSET_DATA)
75
+ // accessHandle.write(new Uint8Array(), { at: HEADER_OFFSET_DATA })
76
+ // accessHandle.flush()
77
+ }
78
+
79
+ jOpen(zName: string, fileId: number, flags: number, pOutFlags: DataView): number {
80
+ try {
81
+ // First try to open a path that already exists in the file system.
82
+ const path = zName ? this.#getPath(zName) : Math.random().toString(36)
83
+ let accessHandle = this.#mapPathToAccessHandle.get(path)
84
+ if (!accessHandle && flags & VFS.SQLITE_OPEN_CREATE) {
85
+ // File not found so try to create it.
86
+ if (this.getSize() < this.getCapacity()) {
87
+ // Choose an unassociated OPFS file from the pool.
88
+ ;[accessHandle] = this.#availableAccessHandles.keys()
89
+ this.#setAssociatedPath(accessHandle!, path, flags)
90
+ } else {
91
+ // Out of unassociated files. This can be fixed by calling
92
+ // addCapacity() from the application.
93
+ throw new Error('cannot create file')
94
+ }
95
+ }
96
+ if (!accessHandle) {
97
+ throw new Error('file not found')
98
+ }
99
+ // Subsequent methods are only passed the fileId, so make sure we have
100
+ // a way to get the file resources.
101
+ const file = { path, flags, accessHandle }
102
+ this.#mapIdToFile.set(fileId, file)
103
+
104
+ pOutFlags.setInt32(0, flags, true)
105
+ return VFS.SQLITE_OK
106
+ } catch (e: any) {
107
+ console.error(e.message)
108
+ return VFS.SQLITE_CANTOPEN
109
+ }
110
+ }
111
+
112
+ jClose(fileId: number): number {
113
+ const file = this.#mapIdToFile.get(fileId)
114
+ if (file) {
115
+ file.accessHandle.flush()
116
+ this.#mapIdToFile.delete(fileId)
117
+ if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
118
+ this.#deletePath(file.path)
119
+ }
120
+ }
121
+ return VFS.SQLITE_OK
122
+ }
123
+
124
+ jRead(fileId: number, pData: Uint8Array, iOffset: number): number {
125
+ const file = this.#mapIdToFile.get(fileId)!
126
+ const nBytes = file.accessHandle.read(pData.subarray(), { at: HEADER_OFFSET_DATA + iOffset })
127
+ if (nBytes < pData.byteLength) {
128
+ pData.fill(0, nBytes, pData.byteLength)
129
+ return VFS.SQLITE_IOERR_SHORT_READ
130
+ }
131
+ return VFS.SQLITE_OK
132
+ }
133
+
134
+ jWrite(fileId: number, pData: Uint8Array, iOffset: number): number {
135
+ const file = this.#mapIdToFile.get(fileId)!
136
+ const nBytes = file.accessHandle.write(pData.subarray(), { at: HEADER_OFFSET_DATA + iOffset })
137
+ return nBytes === pData.byteLength ? VFS.SQLITE_OK : VFS.SQLITE_IOERR
138
+ }
139
+
140
+ jTruncate(fileId: number, iSize: number): number {
141
+ const file = this.#mapIdToFile.get(fileId)!
142
+ file.accessHandle.truncate(HEADER_OFFSET_DATA + iSize)
143
+ return VFS.SQLITE_OK
144
+ }
145
+
146
+ jSync(fileId: number, _flags: number): number {
147
+ const file = this.#mapIdToFile.get(fileId)!
148
+ file.accessHandle.flush()
149
+ return VFS.SQLITE_OK
150
+ }
151
+
152
+ jFileSize(fileId: number, pSize64: DataView): number {
153
+ const file = this.#mapIdToFile.get(fileId)!
154
+ const size = file.accessHandle.getSize() - HEADER_OFFSET_DATA
155
+ pSize64.setBigInt64(0, BigInt(size), true)
156
+ return VFS.SQLITE_OK
157
+ }
158
+
159
+ jSectorSize(_fileId: number): number {
160
+ return SECTOR_SIZE
161
+ }
162
+
163
+ jDeviceCharacteristics(_fileId: number): number {
164
+ return VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
165
+ }
166
+
167
+ jAccess(zName: string, flags: number, pResOut: DataView): number {
168
+ const path = this.#getPath(zName)
169
+ pResOut.setInt32(0, this.#mapPathToAccessHandle.has(path) ? 1 : 0, true)
170
+ return VFS.SQLITE_OK
171
+ }
172
+
173
+ jDelete(zName: string, _syncDir: number): number {
174
+ const path = this.#getPath(zName)
175
+ this.#deletePath(path)
176
+ return VFS.SQLITE_OK
177
+ }
178
+
179
+ async close() {
180
+ this.#releaseAccessHandles()
181
+ }
182
+
183
+ async isReady() {
184
+ if (!this.#directoryHandle) {
185
+ // All files are stored in a single directory.
186
+ let handle = await navigator.storage.getDirectory()
187
+ for (const d of this.#directoryPath.split('/')) {
188
+ if (d) {
189
+ handle = await handle.getDirectoryHandle(d, { create: true })
190
+ }
191
+ }
192
+ this.#directoryHandle = handle
193
+
194
+ await this.#acquireAccessHandles()
195
+ if (this.getCapacity() === 0) {
196
+ await this.addCapacity(DEFAULT_CAPACITY)
197
+ }
198
+ }
199
+ return true
200
+ }
201
+
202
+ /**
203
+ * Returns the number of SQLite files in the file system.
204
+ */
205
+ getSize(): number {
206
+ return this.#mapPathToAccessHandle.size
207
+ }
208
+
209
+ /**
210
+ * Returns the maximum number of SQLite files the file system can hold.
211
+ */
212
+ getCapacity(): number {
213
+ return this.#mapAccessHandleToName.size
214
+ }
215
+
216
+ /**
217
+ * Increase the capacity of the file system by n.
218
+ */
219
+ async addCapacity(n: number): Promise<number> {
220
+ for (let i = 0; i < n; ++i) {
221
+ const name = Math.random().toString(36).replace('0.', '')
222
+ const handle = await this.#directoryHandle!.getFileHandle(name, { create: true })
223
+ const accessHandle = await handle.createSyncAccessHandle()
224
+ this.#mapAccessHandleToName.set(accessHandle, name)
225
+
226
+ this.#setAssociatedPath(accessHandle, '', 0)
227
+ }
228
+ return n
229
+ }
230
+
231
+ /**
232
+ * Decrease the capacity of the file system by n. The capacity cannot be
233
+ * decreased to fewer than the current number of SQLite files in the
234
+ * file system.
235
+ */
236
+ async removeCapacity(n: number): Promise<number> {
237
+ let nRemoved = 0
238
+ for (const accessHandle of Array.from(this.#availableAccessHandles)) {
239
+ if (nRemoved == n || this.getSize() === this.getCapacity()) return nRemoved
240
+
241
+ const name = this.#mapAccessHandleToName.get(accessHandle)!
242
+ accessHandle.close()
243
+ await this.#directoryHandle!.removeEntry(name)
244
+ this.#mapAccessHandleToName.delete(accessHandle)
245
+ this.#availableAccessHandles.delete(accessHandle)
246
+ ++nRemoved
247
+ }
248
+ return nRemoved
249
+ }
250
+
251
+ async #acquireAccessHandles() {
252
+ // Enumerate all the files in the directory.
253
+ const files = [] as [string, FileSystemFileHandle][]
254
+ for await (const [name, handle] of this.#directoryHandle!) {
255
+ if (handle.kind === 'file') {
256
+ files.push([name, handle])
257
+ }
258
+ }
259
+
260
+ // Open access handles in parallel, separating associated and unassociated.
261
+ await Promise.all(
262
+ files.map(async ([name, handle]) => {
263
+ const accessHandle = await handle.createSyncAccessHandle()
264
+ this.#mapAccessHandleToName.set(accessHandle, name)
265
+ const path = this.#getAssociatedPath(accessHandle)
266
+ if (path) {
267
+ this.#mapPathToAccessHandle.set(path, accessHandle)
268
+ } else {
269
+ this.#availableAccessHandles.add(accessHandle)
270
+ }
271
+ }),
272
+ )
273
+ }
274
+
275
+ #releaseAccessHandles() {
276
+ for (const accessHandle of this.#mapAccessHandleToName.keys()) {
277
+ accessHandle.close()
278
+ }
279
+ this.#mapAccessHandleToName.clear()
280
+ this.#mapPathToAccessHandle.clear()
281
+ this.#availableAccessHandles.clear()
282
+ }
283
+
284
+ /**
285
+ * Read and return the associated path from an OPFS file header.
286
+ * Empty string is returned for an unassociated OPFS file.
287
+ * @returns {string} path or empty string
288
+ */
289
+ #getAssociatedPath(accessHandle: FileSystemSyncAccessHandle): string {
290
+ // Read the path and digest of the path from the file.
291
+ const corpus = new Uint8Array(HEADER_CORPUS_SIZE)
292
+ accessHandle.read(corpus, { at: 0 })
293
+
294
+ // Delete files not expected to be present.
295
+ const dataView = new DataView(corpus.buffer, corpus.byteOffset)
296
+ const flags = dataView.getUint32(HEADER_OFFSET_FLAGS)
297
+ if (corpus[0] && (flags & VFS.SQLITE_OPEN_DELETEONCLOSE || (flags & PERSISTENT_FILE_TYPES) === 0)) {
298
+ console.warn(`Remove file with unexpected flags ${flags.toString(16)}`)
299
+ this.#setAssociatedPath(accessHandle, '', 0)
300
+ return ''
301
+ }
302
+
303
+ const fileDigest = new Uint32Array(HEADER_DIGEST_SIZE / 4)
304
+ accessHandle.read(fileDigest, { at: HEADER_OFFSET_DIGEST })
305
+
306
+ // Verify the digest.
307
+ const computedDigest = this.#computeDigest(corpus)
308
+ if (fileDigest.every((value, i) => value === computedDigest[i])) {
309
+ // Good digest. Decode the null-terminated path string.
310
+ const pathBytes = corpus.indexOf(0)
311
+ if (pathBytes === 0) {
312
+ // Ensure that unassociated files are empty. Unassociated files are
313
+ // truncated in #setAssociatedPath after the header is written. If
314
+ // an interruption occurs right before the truncation then garbage
315
+ // may remain in the file.
316
+ accessHandle.truncate(HEADER_OFFSET_DATA)
317
+ }
318
+ return new TextDecoder().decode(corpus.subarray(0, pathBytes))
319
+ } else {
320
+ // Bad digest. Repair this header.
321
+ console.warn('Disassociating file with bad digest.')
322
+ this.#setAssociatedPath(accessHandle, '', 0)
323
+ return ''
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Set the path on an OPFS file header.
329
+ */
330
+ #setAssociatedPath(accessHandle: FileSystemSyncAccessHandle, path: string, flags: number) {
331
+ // Convert the path string to UTF-8.
332
+ const corpus = new Uint8Array(HEADER_CORPUS_SIZE)
333
+ const encodedResult = new TextEncoder().encodeInto(path, corpus)
334
+ if (encodedResult.written >= HEADER_MAX_PATH_SIZE) {
335
+ throw new Error('path too long')
336
+ }
337
+
338
+ // Add the creation flags.
339
+ const dataView = new DataView(corpus.buffer, corpus.byteOffset)
340
+ dataView.setUint32(HEADER_OFFSET_FLAGS, flags)
341
+
342
+ // Write the OPFS file header, including the digest.
343
+ const digest = this.#computeDigest(corpus)
344
+ accessHandle.write(corpus, { at: 0 })
345
+ accessHandle.write(digest, { at: HEADER_OFFSET_DIGEST })
346
+ accessHandle.flush()
347
+
348
+ if (path) {
349
+ this.#mapPathToAccessHandle.set(path, accessHandle)
350
+ this.#availableAccessHandles.delete(accessHandle)
351
+ } else {
352
+ // This OPFS file doesn't represent any SQLite file so it doesn't
353
+ // need to keep any data.
354
+ accessHandle.truncate(HEADER_OFFSET_DATA)
355
+ this.#availableAccessHandles.add(accessHandle)
356
+ }
357
+ }
358
+
359
+ /**
360
+ * We need a synchronous digest function so can't use WebCrypto.
361
+ * Adapted from https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
362
+ * @returns {ArrayBuffer} 64-bit digest
363
+ */
364
+ #computeDigest(corpus: Uint8Array): Uint32Array {
365
+ if (!corpus[0]) {
366
+ // Optimization for deleted file.
367
+ return new Uint32Array([0xfe_cc_5f_80, 0xac_ce_c0_37])
368
+ }
369
+
370
+ let h1 = 0xde_ad_be_ef
371
+ let h2 = 0x41_c6_ce_57
372
+
373
+ for (const value of corpus) {
374
+ h1 = Math.imul(h1 ^ value, 2_654_435_761)
375
+ h2 = Math.imul(h2 ^ value, 1_597_334_677)
376
+ }
377
+
378
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2_246_822_507) ^ Math.imul(h2 ^ (h2 >>> 13), 3_266_489_909)
379
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2_246_822_507) ^ Math.imul(h1 ^ (h1 >>> 13), 3_266_489_909)
380
+
381
+ return new Uint32Array([h1 >>> 0, h2 >>> 0])
382
+ }
383
+
384
+ /**
385
+ * Convert a bare filename, path, or URL to a UNIX-style path.
386
+ */
387
+ #getPath(nameOrURL: string | URL): string {
388
+ const url = typeof nameOrURL === 'string' ? new URL(nameOrURL, 'file://localhost/') : nameOrURL
389
+ return url.pathname
390
+ }
391
+
392
+ /**
393
+ * Remove the association between a path and an OPFS file.
394
+ * @param {string} path
395
+ */
396
+ #deletePath(path: string) {
397
+ const accessHandle = this.#mapPathToAccessHandle.get(path)
398
+ if (accessHandle) {
399
+ // Un-associate the SQLite path from the OPFS file.
400
+ this.#mapPathToAccessHandle.delete(path)
401
+ this.#setAssociatedPath(accessHandle, '', 0)
402
+ }
403
+ }
404
+ }
@@ -0,0 +1,35 @@
1
+ import { Effect } from '@livestore/utils/effect'
2
+ import type * as WaSqlite from '@livestore/wa-sqlite'
3
+
4
+ import { AccessHandlePoolVFS } from './AccessHandlePoolVFS.js'
5
+
6
+ const semaphore = Effect.makeSemaphore(1).pipe(Effect.runSync)
7
+ const opfsVfsMap = new Map<string, AccessHandlePoolVFS>()
8
+
9
+ export const makeOpfsDb = ({
10
+ sqlite3,
11
+ directory,
12
+ fileName,
13
+ }: {
14
+ sqlite3: WaSqlite.SQLiteAPI
15
+ directory: string
16
+ fileName: string
17
+ }) =>
18
+ Effect.gen(function* () {
19
+ // Replace all special characters with underscores
20
+ const safePath = directory.replaceAll(/["*/:<>?\\|]/g, '_')
21
+ const pathSegment = safePath.length === 0 ? '' : `-${safePath}`
22
+ const vfsName = `opfs${pathSegment}`
23
+
24
+ if (sqlite3.vfs_registered.has(vfsName) === false) {
25
+ const vfs = yield* Effect.promise(() => AccessHandlePoolVFS.create(vfsName, directory, (sqlite3 as any).module))
26
+
27
+ sqlite3.vfs_register(vfs, false)
28
+ opfsVfsMap.set(vfsName, vfs)
29
+ }
30
+
31
+ const dbPointer = sqlite3.open_v2Sync(fileName, undefined, vfsName)
32
+ const vfs = opfsVfsMap.get(vfsName)!
33
+
34
+ return { dbPointer, vfs }
35
+ }).pipe(semaphore.withPermits(1))
@@ -0,0 +1,68 @@
1
+ import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
2
+
3
+ const SECTOR_SIZE = 4096
4
+ const HEADER_MAX_PATH_SIZE = 512
5
+ const HEADER_FLAGS_SIZE = 4
6
+ const HEADER_DIGEST_SIZE = 8
7
+ const HEADER_CORPUS_SIZE = HEADER_MAX_PATH_SIZE + HEADER_FLAGS_SIZE
8
+ const HEADER_OFFSET_FLAGS = HEADER_MAX_PATH_SIZE
9
+ const HEADER_OFFSET_DIGEST = HEADER_CORPUS_SIZE
10
+ export const HEADER_OFFSET_DATA = SECTOR_SIZE
11
+
12
+ const PERSISTENT_FILE_TYPES =
13
+ VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_MAIN_JOURNAL | VFS.SQLITE_OPEN_SUPER_JOURNAL | VFS.SQLITE_OPEN_WAL
14
+
15
+ const textDecoder = new TextDecoder()
16
+
17
+ export const decodeSAHPoolFilename = async (file: File): Promise<string> => {
18
+ // Read the path and digest of the path from the file.
19
+ const corpus = new Uint8Array(await file.slice(0, HEADER_CORPUS_SIZE).arrayBuffer())
20
+
21
+ // Delete files not expected to be present.
22
+ const dataView = new DataView(corpus.buffer, corpus.byteOffset)
23
+ const flags = dataView.getUint32(HEADER_OFFSET_FLAGS)
24
+ if (corpus[0] && (flags & VFS.SQLITE_OPEN_DELETEONCLOSE || (flags & PERSISTENT_FILE_TYPES) === 0)) {
25
+ console.warn(`Remove file with unexpected flags ${flags.toString(16)}`)
26
+ return ''
27
+ }
28
+
29
+ const fileDigest = new Uint32Array(
30
+ await file.slice(HEADER_OFFSET_DIGEST, HEADER_OFFSET_DIGEST + HEADER_DIGEST_SIZE).arrayBuffer(),
31
+ )
32
+
33
+ // Verify the digest.
34
+ const computedDigest = computeDigest(corpus)
35
+ if (fileDigest.every((value, i) => value === computedDigest[i])) {
36
+ // Good digest. Decode the null-terminated path string.
37
+ const pathBytes = corpus.indexOf(0)
38
+ if (pathBytes === 0) {
39
+ // Note: We can't truncate the file here as File objects are read-only
40
+ // console.warn('Unassociated file detected')
41
+ }
42
+ return textDecoder.decode(corpus.subarray(0, pathBytes))
43
+ } else {
44
+ // Bad digest. Repair this header.
45
+ console.warn('Disassociating file with bad digest.')
46
+ return ''
47
+ }
48
+ }
49
+
50
+ const computeDigest = (corpus: Uint8Array): Uint32Array => {
51
+ if (!corpus[0]) {
52
+ // Optimization for deleted file.
53
+ return new Uint32Array([0xfe_cc_5f_80, 0xac_ce_c0_37])
54
+ }
55
+
56
+ let h1 = 0xde_ad_be_ef
57
+ let h2 = 0x41_c6_ce_57
58
+
59
+ for (const value of corpus) {
60
+ h1 = Math.imul(h1 ^ value, 2_654_435_761)
61
+ h2 = Math.imul(h2 ^ value, 1_597_334_677)
62
+ }
63
+
64
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2_246_822_507) ^ Math.imul(h2 ^ (h2 >>> 13), 3_266_489_909)
65
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2_246_822_507) ^ Math.imul(h1 ^ (h1 >>> 13), 3_266_489_909)
66
+
67
+ return new Uint32Array([h1 >>> 0, h2 >>> 0])
68
+ }
@@ -0,0 +1,20 @@
1
+ import type * as WaSqlite from '@livestore/wa-sqlite'
2
+ import { MemoryVFS } from '@livestore/wa-sqlite/src/examples/MemoryVFS.js'
3
+
4
+ let cachedMemoryVfs: MemoryVFS | undefined
5
+
6
+ export const makeInMemoryDb = (sqlite3: WaSqlite.SQLiteAPI) => {
7
+ if (sqlite3.vfs_registered.has('memory-vfs') === false) {
8
+ // @ts-expect-error TODO fix types
9
+ const vfs = new MemoryVFS('memory-vfs', (sqlite3 as any).module)
10
+
11
+ // @ts-expect-error TODO fix types
12
+ sqlite3.vfs_register(vfs, false)
13
+ cachedMemoryVfs = vfs
14
+ }
15
+
16
+ const dbPointer = sqlite3.open_v2Sync(':memory:', undefined, 'memory-vfs')
17
+ const vfs = cachedMemoryVfs!
18
+
19
+ return { dbPointer, vfs }
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * as WaSqlite from './index_.js'
package/src/index_.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from '@livestore/wa-sqlite'
2
+ export * from './make-sqlite-db.js'
@@ -0,0 +1,12 @@
1
+ import * as WaSqlite from '@livestore/wa-sqlite'
2
+ import WaSqliteFactory from '@livestore/wa-sqlite/dist/wa-sqlite.mjs'
3
+
4
+ export const loadSqlite3Wasm = async () => {
5
+ const module = await WaSqliteFactory()
6
+ // https://github.com/rhashimoto/wa-sqlite/issues/143#issuecomment-1899060056
7
+ // module._free(module._malloc(10_000 * 4096 + 65_536))
8
+ const sqlite3 = WaSqlite.Factory(module)
9
+ // @ts-expect-error TODO fix types
10
+ sqlite3.module = module
11
+ return sqlite3
12
+ }
@@ -0,0 +1,13 @@
1
+ import * as WaSqlite from '@livestore/wa-sqlite'
2
+ // @ts-expect-error TODO fix types in wa-sqlite
3
+ import WaSqliteFactory from '@livestore/wa-sqlite/dist/wa-sqlite.node.mjs'
4
+
5
+ export const loadSqlite3Wasm = async () => {
6
+ const module = await WaSqliteFactory()
7
+ // https://github.com/rhashimoto/wa-sqlite/issues/143#issuecomment-1899060056
8
+ // module._free(module._malloc(10_000 * 4096 + 65_536))
9
+ const sqlite3 = WaSqlite.Factory(module)
10
+ // @ts-expect-error TODO fix types
11
+ sqlite3.module = module
12
+ return sqlite3
13
+ }