@livestore/sqlite-wasm 0.4.0-dev.3 → 0.4.0-dev.6

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 (79) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/browser/mod.d.ts +1 -0
  3. package/dist/browser/mod.d.ts.map +1 -1
  4. package/dist/browser/mod.js.map +1 -1
  5. package/dist/browser/opfs/AccessHandlePoolVFS.d.ts +7 -0
  6. package/dist/browser/opfs/AccessHandlePoolVFS.d.ts.map +1 -1
  7. package/dist/browser/opfs/AccessHandlePoolVFS.js +36 -1
  8. package/dist/browser/opfs/AccessHandlePoolVFS.js.map +1 -1
  9. package/dist/cf/BlockManager.d.ts +61 -0
  10. package/dist/cf/BlockManager.d.ts.map +1 -0
  11. package/dist/cf/BlockManager.js +157 -0
  12. package/dist/cf/BlockManager.js.map +1 -0
  13. package/dist/cf/CloudflareSqlVFS.d.ts +51 -0
  14. package/dist/cf/CloudflareSqlVFS.d.ts.map +1 -0
  15. package/dist/cf/CloudflareSqlVFS.js +351 -0
  16. package/dist/cf/CloudflareSqlVFS.js.map +1 -0
  17. package/dist/cf/CloudflareWorkerVFS.d.ts +72 -0
  18. package/dist/cf/CloudflareWorkerVFS.d.ts.map +1 -0
  19. package/dist/cf/CloudflareWorkerVFS.js +552 -0
  20. package/dist/cf/CloudflareWorkerVFS.js.map +1 -0
  21. package/dist/cf/mod.d.ts +43 -0
  22. package/dist/cf/mod.d.ts.map +1 -0
  23. package/dist/cf/mod.js +74 -0
  24. package/dist/cf/mod.js.map +1 -0
  25. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.d.ts +2 -0
  26. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.d.ts.map +1 -0
  27. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js +314 -0
  28. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js.map +1 -0
  29. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.d.ts +2 -0
  30. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.d.ts.map +1 -0
  31. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js +266 -0
  32. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js.map +1 -0
  33. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.d.ts +2 -0
  34. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.d.ts.map +1 -0
  35. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js +444 -0
  36. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js.map +1 -0
  37. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.d.ts +2 -0
  38. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.d.ts.map +1 -0
  39. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js +334 -0
  40. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js.map +1 -0
  41. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.d.ts +2 -0
  42. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.d.ts.map +1 -0
  43. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js +354 -0
  44. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js.map +1 -0
  45. package/dist/load-wasm/mod.node.d.ts.map +1 -1
  46. package/dist/load-wasm/mod.node.js +1 -2
  47. package/dist/load-wasm/mod.node.js.map +1 -1
  48. package/dist/load-wasm/mod.workerd.d.ts +2 -0
  49. package/dist/load-wasm/mod.workerd.d.ts.map +1 -0
  50. package/dist/load-wasm/mod.workerd.js +26 -0
  51. package/dist/load-wasm/mod.workerd.js.map +1 -0
  52. package/dist/make-sqlite-db.d.ts +1 -0
  53. package/dist/make-sqlite-db.d.ts.map +1 -1
  54. package/dist/make-sqlite-db.js.map +1 -1
  55. package/dist/node/NodeFS.d.ts +1 -2
  56. package/dist/node/NodeFS.d.ts.map +1 -1
  57. package/dist/node/NodeFS.js +1 -6
  58. package/dist/node/NodeFS.js.map +1 -1
  59. package/dist/node/mod.js +3 -8
  60. package/dist/node/mod.js.map +1 -1
  61. package/package.json +20 -7
  62. package/src/browser/mod.ts +1 -0
  63. package/src/browser/opfs/AccessHandlePoolVFS.ts +37 -1
  64. package/src/cf/BlockManager.ts +225 -0
  65. package/src/cf/CloudflareSqlVFS.ts +450 -0
  66. package/src/cf/CloudflareWorkerVFS.ts +664 -0
  67. package/src/cf/README.md +60 -0
  68. package/src/cf/mod.ts +143 -0
  69. package/src/cf/test/README.md +224 -0
  70. package/src/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.ts +389 -0
  71. package/src/cf/test/async-storage/cloudflare-worker-vfs-core.test.ts +322 -0
  72. package/src/cf/test/async-storage/cloudflare-worker-vfs-integration.test.ts +567 -0
  73. package/src/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.ts +403 -0
  74. package/src/cf/test/sql/cloudflare-sql-vfs-core.test.ts +433 -0
  75. package/src/load-wasm/mod.node.ts +1 -2
  76. package/src/load-wasm/mod.workerd.ts +26 -0
  77. package/src/make-sqlite-db.ts +1 -0
  78. package/src/node/NodeFS.ts +1 -9
  79. package/src/node/mod.ts +3 -10
@@ -0,0 +1,450 @@
1
+ import type { CfTypes } from '@livestore/common-cf'
2
+ import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
3
+ import { FacadeVFS } from '../FacadeVFS.ts'
4
+ import { BlockManager } from './BlockManager.ts'
5
+
6
+ const SECTOR_SIZE = 4096
7
+
8
+ // Block size for SQL-based storage (same as CloudflareWorkerVFS for consistency)
9
+ const BLOCK_SIZE = 64 * 1024 // 64 KiB
10
+
11
+ // Maximum number of open files
12
+ const DEFAULT_MAX_FILES = 100
13
+
14
+ // These file types are expected to persist in the file system
15
+ const PERSISTENT_FILE_TYPES =
16
+ VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_MAIN_JOURNAL | VFS.SQLITE_OPEN_SUPER_JOURNAL | VFS.SQLITE_OPEN_WAL
17
+
18
+ interface FileMetadata {
19
+ path: string
20
+ size: number
21
+ flags: number
22
+ created: number
23
+ modified: number
24
+ }
25
+
26
+ interface FileHandle {
27
+ path: string
28
+ flags: number
29
+ metadata: FileMetadata
30
+ }
31
+
32
+ export interface SqlVfsOptions {
33
+ maxFiles?: number
34
+ blockSize?: number
35
+ }
36
+
37
+ /**
38
+ * VFS implementation using Cloudflare Durable Object SQL storage as the backend.
39
+ * This provides a synchronous VFS interface by leveraging SQL's synchronous API.
40
+ *
41
+ * Storage Strategy:
42
+ * - Files are stored as blocks in SQL tables for efficient I/O
43
+ * - File metadata stored in vfs_files table
44
+ * - File data stored as fixed-size blocks in vfs_blocks table
45
+ * - Synchronous operations via SQL's synchronous API
46
+ *
47
+ * Key advantages over async VFS:
48
+ * - No async/await complexity
49
+ * - Native SQL ACID properties
50
+ * - Efficient range queries for file operations
51
+ * - Built-in consistency and durability
52
+ */
53
+ export class CloudflareSqlVFS extends FacadeVFS {
54
+ log = null
55
+
56
+ #sql: CfTypes.SqlStorage
57
+ #initialized = false
58
+ #blockManager: BlockManager
59
+
60
+ // File management
61
+ #openFiles = new Map<number, FileHandle>()
62
+ #maxFiles: number
63
+
64
+ static async create(name: string, sql: CfTypes.SqlStorage, module: any, options: SqlVfsOptions = {}) {
65
+ const vfs = new CloudflareSqlVFS(name, sql, module, options)
66
+ await vfs.isReady()
67
+ return vfs
68
+ }
69
+
70
+ constructor(name: string, sql: CfTypes.SqlStorage, module: any, options: SqlVfsOptions = {}) {
71
+ super(name, module)
72
+ this.#sql = sql
73
+ this.#maxFiles = options.maxFiles || DEFAULT_MAX_FILES
74
+ this.#blockManager = new BlockManager(options.blockSize || BLOCK_SIZE)
75
+ }
76
+
77
+ /**
78
+ * Initialize the VFS by setting up SQL schema
79
+ */
80
+ async isReady(): Promise<boolean> {
81
+ if (this.#initialized) {
82
+ return true
83
+ }
84
+
85
+ try {
86
+ // Initialize SQL schema
87
+ this.#initializeSchema()
88
+
89
+ // Clean up non-persistent files from previous sessions
90
+ this.#cleanupNonPersistentFiles()
91
+
92
+ this.#initialized = true
93
+ return true
94
+ } catch (error) {
95
+ console.error('CloudflareSqlVFS initialization failed:', error)
96
+ return false
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Initialize the SQL schema for the VFS
102
+ */
103
+ #initializeSchema(): void {
104
+ // Execute each statement individually to avoid parsing issues
105
+ const statements = [
106
+ `CREATE TABLE IF NOT EXISTS vfs_files (
107
+ file_path TEXT PRIMARY KEY,
108
+ file_size INTEGER NOT NULL DEFAULT 0,
109
+ flags INTEGER NOT NULL DEFAULT 0,
110
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
111
+ modified_at INTEGER NOT NULL DEFAULT (unixepoch())
112
+ )`,
113
+
114
+ `CREATE TABLE IF NOT EXISTS vfs_blocks (
115
+ file_path TEXT NOT NULL,
116
+ block_id INTEGER NOT NULL,
117
+ block_data BLOB NOT NULL,
118
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
119
+ PRIMARY KEY (file_path, block_id),
120
+ FOREIGN KEY (file_path) REFERENCES vfs_files(file_path) ON DELETE CASCADE
121
+ )`,
122
+
123
+ `CREATE INDEX IF NOT EXISTS idx_vfs_blocks_range ON vfs_blocks(file_path, block_id)`,
124
+
125
+ `CREATE INDEX IF NOT EXISTS idx_vfs_files_modified ON vfs_files(modified_at)`,
126
+
127
+ `CREATE TRIGGER IF NOT EXISTS trg_vfs_files_update_modified
128
+ AFTER UPDATE OF file_size ON vfs_files
129
+ BEGIN
130
+ UPDATE vfs_files SET modified_at = unixepoch() WHERE file_path = NEW.file_path;
131
+ END`,
132
+ ]
133
+
134
+ for (const statement of statements) {
135
+ try {
136
+ this.#sql.exec(statement)
137
+ } catch (error) {
138
+ console.error('Failed to execute schema statement:', statement)
139
+ throw error
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Clean up non-persistent files from previous sessions
146
+ */
147
+ #cleanupNonPersistentFiles(): void {
148
+ try {
149
+ const cursor = this.#sql.exec<{ file_path: string; flags: number }>('SELECT file_path, flags FROM vfs_files')
150
+
151
+ const filesToDelete: string[] = []
152
+
153
+ for (const row of cursor) {
154
+ // Check if file should be persistent
155
+ if (!(row.flags & PERSISTENT_FILE_TYPES)) {
156
+ filesToDelete.push(row.file_path)
157
+ }
158
+ }
159
+
160
+ // Delete non-persistent files
161
+ for (const filePath of filesToDelete) {
162
+ this.#sql.exec('DELETE FROM vfs_files WHERE file_path = ?', filePath)
163
+ }
164
+ } catch (error) {
165
+ console.warn('Error during cleanup:', error)
166
+ }
167
+ }
168
+
169
+ // VFS Interface Implementation
170
+
171
+ jOpen(path: string, fileId: number, flags: number, pOutFlags: DataView): number {
172
+ try {
173
+ if (this.#openFiles.size >= this.#maxFiles) {
174
+ return VFS.SQLITE_CANTOPEN
175
+ }
176
+
177
+ // Check if file exists
178
+ const existingFile = this.#getFileMetadata(path)
179
+
180
+ if (!existingFile && !(flags & VFS.SQLITE_OPEN_CREATE)) {
181
+ return VFS.SQLITE_CANTOPEN
182
+ }
183
+
184
+ let metadata: FileMetadata
185
+
186
+ if (existingFile) {
187
+ metadata = existingFile
188
+ } else {
189
+ // Create new file
190
+ const now = Math.floor(Date.now() / 1000)
191
+ metadata = {
192
+ path,
193
+ size: 0,
194
+ flags,
195
+ created: now,
196
+ modified: now,
197
+ }
198
+
199
+ this.#sql.exec(
200
+ 'INSERT INTO vfs_files (file_path, file_size, flags, created_at, modified_at) VALUES (?, ?, ?, ?, ?)',
201
+ path,
202
+ 0,
203
+ flags,
204
+ now,
205
+ now,
206
+ )
207
+ }
208
+
209
+ // Store file handle
210
+ this.#openFiles.set(fileId, {
211
+ path,
212
+ flags,
213
+ metadata,
214
+ })
215
+
216
+ pOutFlags.setInt32(0, flags, true)
217
+ return VFS.SQLITE_OK
218
+ } catch (error) {
219
+ console.error('jOpen error:', error)
220
+ return VFS.SQLITE_CANTOPEN
221
+ }
222
+ }
223
+
224
+ jClose(fileId: number): number {
225
+ this.#openFiles.delete(fileId)
226
+ return VFS.SQLITE_OK
227
+ }
228
+
229
+ jRead(fileId: number, buffer: Uint8Array, offset: number): number {
230
+ try {
231
+ const handle = this.#openFiles.get(fileId)
232
+ if (!handle) {
233
+ return VFS.SQLITE_IOERR
234
+ }
235
+
236
+ const range = this.#blockManager.calculateBlockRange(offset, buffer.length)
237
+ const blockIds = []
238
+ for (let i = range.startBlock; i <= range.endBlock; i++) {
239
+ blockIds.push(i)
240
+ }
241
+
242
+ const blocks = this.#blockManager.readBlocks(this.#sql, handle.path, blockIds)
243
+ const data = this.#blockManager.assembleBlocks(blocks, range, buffer.length)
244
+
245
+ buffer.set(data)
246
+ return VFS.SQLITE_OK
247
+ } catch (error) {
248
+ console.error('jRead error:', error)
249
+ return VFS.SQLITE_IOERR
250
+ }
251
+ }
252
+
253
+ jWrite(fileId: number, data: Uint8Array, offset: number): number {
254
+ try {
255
+ const handle = this.#openFiles.get(fileId)
256
+ if (!handle) {
257
+ return VFS.SQLITE_IOERR
258
+ }
259
+
260
+ // Split write data into blocks
261
+ const writeBlocks = this.#blockManager.splitIntoBlocks(data, offset)
262
+ const finalBlocks = new Map<number, Uint8Array>()
263
+
264
+ for (const [blockId, blockInfo] of writeBlocks) {
265
+ let blockData: Uint8Array
266
+
267
+ if (blockInfo.blockOffset === 0 && blockInfo.data.length === this.#blockManager.getBlockSize()) {
268
+ // Full block write
269
+ blockData = blockInfo.data
270
+ } else {
271
+ // Partial block write - merge with existing data
272
+ blockData = this.#blockManager.mergePartialBlock(
273
+ this.#sql,
274
+ handle.path,
275
+ blockInfo.blockId,
276
+ blockInfo.blockOffset,
277
+ blockInfo.data,
278
+ )
279
+ }
280
+
281
+ finalBlocks.set(blockId, blockData)
282
+ }
283
+
284
+ // Write blocks to SQL storage
285
+ this.#blockManager.writeBlocks(this.#sql, handle.path, finalBlocks)
286
+
287
+ // Update file size if necessary
288
+ const newSize = Math.max(handle.metadata.size, offset + data.length)
289
+ if (newSize !== handle.metadata.size) {
290
+ this.#sql.exec('UPDATE vfs_files SET file_size = ? WHERE file_path = ?', newSize, handle.path)
291
+ handle.metadata.size = newSize
292
+ }
293
+
294
+ return VFS.SQLITE_OK
295
+ } catch (error) {
296
+ console.error('jWrite error:', error)
297
+ return VFS.SQLITE_IOERR
298
+ }
299
+ }
300
+
301
+ jTruncate(fileId: number, size: number): number {
302
+ try {
303
+ const handle = this.#openFiles.get(fileId)
304
+ if (!handle) {
305
+ return VFS.SQLITE_IOERR
306
+ }
307
+
308
+ // Calculate which block contains the new end of file
309
+ const lastBlockId = Math.floor(size / this.#blockManager.getBlockSize())
310
+
311
+ // Delete blocks beyond the truncation point
312
+ this.#blockManager.deleteBlocksAfter(this.#sql, handle.path, lastBlockId + 1)
313
+
314
+ // If truncating within a block, we need to truncate that block's data
315
+ if (size % this.#blockManager.getBlockSize() !== 0) {
316
+ const existingBlocks = this.#blockManager.readBlocks(this.#sql, handle.path, [lastBlockId])
317
+ const blockData = existingBlocks.get(lastBlockId)
318
+
319
+ if (blockData) {
320
+ const truncatedBlock = blockData.slice(0, size % this.#blockManager.getBlockSize())
321
+ const paddedBlock = new Uint8Array(this.#blockManager.getBlockSize())
322
+ paddedBlock.set(truncatedBlock)
323
+
324
+ const blocksToWrite = new Map([[lastBlockId, paddedBlock]])
325
+ this.#blockManager.writeBlocks(this.#sql, handle.path, blocksToWrite)
326
+ }
327
+ }
328
+
329
+ // Update file metadata
330
+ this.#sql.exec('UPDATE vfs_files SET file_size = ? WHERE file_path = ?', size, handle.path)
331
+ handle.metadata.size = size
332
+
333
+ return VFS.SQLITE_OK
334
+ } catch (error) {
335
+ console.error('jTruncate error:', error)
336
+ return VFS.SQLITE_IOERR
337
+ }
338
+ }
339
+
340
+ jSync(fileId: number, _flags: number): number {
341
+ // SQL storage provides immediate durability, so sync is effectively a no-op
342
+ const handle = this.#openFiles.get(fileId)
343
+ if (!handle) {
344
+ return VFS.SQLITE_IOERR
345
+ }
346
+ return VFS.SQLITE_OK
347
+ }
348
+
349
+ jFileSize(fileId: number, pSize64: DataView): number {
350
+ try {
351
+ const handle = this.#openFiles.get(fileId)
352
+ if (!handle) {
353
+ return VFS.SQLITE_IOERR
354
+ }
355
+
356
+ pSize64.setBigInt64(0, BigInt(handle.metadata.size), true)
357
+ return VFS.SQLITE_OK
358
+ } catch (error) {
359
+ console.error('jFileSize error:', error)
360
+ return VFS.SQLITE_IOERR
361
+ }
362
+ }
363
+
364
+ jDelete(path: string, _syncDir: number): number {
365
+ try {
366
+ this.#sql.exec('DELETE FROM vfs_files WHERE file_path = ?', path)
367
+ return VFS.SQLITE_OK
368
+ } catch (error) {
369
+ console.error('jDelete error:', error)
370
+ return VFS.SQLITE_IOERR
371
+ }
372
+ }
373
+
374
+ jAccess(path: string, _flags: number, pResOut: DataView): number {
375
+ try {
376
+ const metadata = this.#getFileMetadata(path)
377
+ pResOut.setInt32(0, metadata ? 1 : 0, true)
378
+ return VFS.SQLITE_OK
379
+ } catch (error) {
380
+ console.error('jAccess error:', error)
381
+ return VFS.SQLITE_IOERR
382
+ }
383
+ }
384
+
385
+ jSectorSize(_fileId: number): number {
386
+ return SECTOR_SIZE
387
+ }
388
+
389
+ jDeviceCharacteristics(_fileId: number): number {
390
+ return VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN
391
+ }
392
+
393
+ // Helper methods
394
+
395
+ #getFileMetadata(path: string): FileMetadata | undefined {
396
+ try {
397
+ const cursor = this.#sql.exec<{
398
+ file_path: string
399
+ file_size: number
400
+ flags: number
401
+ created_at: number
402
+ modified_at: number
403
+ }>('SELECT file_path, file_size, flags, created_at, modified_at FROM vfs_files WHERE file_path = ?', path)
404
+
405
+ const row = cursor.one()
406
+ return {
407
+ path: row.file_path,
408
+ size: row.file_size,
409
+ flags: row.flags,
410
+ created: row.created_at,
411
+ modified: row.modified_at,
412
+ }
413
+ } catch {
414
+ return undefined
415
+ }
416
+ }
417
+
418
+ // Statistics and debugging
419
+
420
+ getStats(): {
421
+ activeFiles: number
422
+ openFiles: number
423
+ maxFiles: number
424
+ blockSize: number
425
+ totalStoredBytes: number
426
+ } {
427
+ try {
428
+ const cursor = this.#sql.exec<{ total_files: number; total_bytes: number }>(
429
+ 'SELECT COUNT(*) as total_files, COALESCE(SUM(LENGTH(block_data)), 0) as total_bytes FROM vfs_files LEFT JOIN vfs_blocks USING (file_path)',
430
+ )
431
+ const stats = cursor.one()
432
+
433
+ return {
434
+ activeFiles: stats.total_files,
435
+ openFiles: this.#openFiles.size,
436
+ maxFiles: this.#maxFiles,
437
+ blockSize: this.#blockManager.getBlockSize(),
438
+ totalStoredBytes: stats.total_bytes,
439
+ }
440
+ } catch {
441
+ return {
442
+ activeFiles: 0,
443
+ openFiles: this.#openFiles.size,
444
+ maxFiles: this.#maxFiles,
445
+ blockSize: this.#blockManager.getBlockSize(),
446
+ totalStoredBytes: 0,
447
+ }
448
+ }
449
+ }
450
+ }