@livestore/sqlite-wasm 0.4.0-dev.2 → 0.4.0-dev.5

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 (74) 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/cf/BlockManager.d.ts +61 -0
  6. package/dist/cf/BlockManager.d.ts.map +1 -0
  7. package/dist/cf/BlockManager.js +157 -0
  8. package/dist/cf/BlockManager.js.map +1 -0
  9. package/dist/cf/CloudflareSqlVFS.d.ts +51 -0
  10. package/dist/cf/CloudflareSqlVFS.d.ts.map +1 -0
  11. package/dist/cf/CloudflareSqlVFS.js +351 -0
  12. package/dist/cf/CloudflareSqlVFS.js.map +1 -0
  13. package/dist/cf/CloudflareWorkerVFS.d.ts +72 -0
  14. package/dist/cf/CloudflareWorkerVFS.d.ts.map +1 -0
  15. package/dist/cf/CloudflareWorkerVFS.js +552 -0
  16. package/dist/cf/CloudflareWorkerVFS.js.map +1 -0
  17. package/dist/cf/mod.d.ts +43 -0
  18. package/dist/cf/mod.d.ts.map +1 -0
  19. package/dist/cf/mod.js +74 -0
  20. package/dist/cf/mod.js.map +1 -0
  21. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.d.ts +2 -0
  22. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.d.ts.map +1 -0
  23. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js +314 -0
  24. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js.map +1 -0
  25. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.d.ts +2 -0
  26. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.d.ts.map +1 -0
  27. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js +266 -0
  28. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js.map +1 -0
  29. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.d.ts +2 -0
  30. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.d.ts.map +1 -0
  31. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js +444 -0
  32. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js.map +1 -0
  33. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.d.ts +2 -0
  34. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.d.ts.map +1 -0
  35. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js +334 -0
  36. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js.map +1 -0
  37. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.d.ts +2 -0
  38. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.d.ts.map +1 -0
  39. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js +354 -0
  40. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js.map +1 -0
  41. package/dist/load-wasm/mod.node.d.ts.map +1 -1
  42. package/dist/load-wasm/mod.node.js +1 -2
  43. package/dist/load-wasm/mod.node.js.map +1 -1
  44. package/dist/load-wasm/mod.workerd.d.ts +2 -0
  45. package/dist/load-wasm/mod.workerd.d.ts.map +1 -0
  46. package/dist/load-wasm/mod.workerd.js +26 -0
  47. package/dist/load-wasm/mod.workerd.js.map +1 -0
  48. package/dist/make-sqlite-db.d.ts +1 -0
  49. package/dist/make-sqlite-db.d.ts.map +1 -1
  50. package/dist/make-sqlite-db.js.map +1 -1
  51. package/dist/node/NodeFS.d.ts +1 -2
  52. package/dist/node/NodeFS.d.ts.map +1 -1
  53. package/dist/node/NodeFS.js +1 -6
  54. package/dist/node/NodeFS.js.map +1 -1
  55. package/dist/node/mod.js +3 -8
  56. package/dist/node/mod.js.map +1 -1
  57. package/package.json +20 -7
  58. package/src/browser/mod.ts +1 -0
  59. package/src/cf/BlockManager.ts +225 -0
  60. package/src/cf/CloudflareSqlVFS.ts +450 -0
  61. package/src/cf/CloudflareWorkerVFS.ts +664 -0
  62. package/src/cf/README.md +60 -0
  63. package/src/cf/mod.ts +143 -0
  64. package/src/cf/test/README.md +224 -0
  65. package/src/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.ts +389 -0
  66. package/src/cf/test/async-storage/cloudflare-worker-vfs-core.test.ts +322 -0
  67. package/src/cf/test/async-storage/cloudflare-worker-vfs-integration.test.ts +567 -0
  68. package/src/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.ts +403 -0
  69. package/src/cf/test/sql/cloudflare-sql-vfs-core.test.ts +433 -0
  70. package/src/load-wasm/mod.node.ts +1 -2
  71. package/src/load-wasm/mod.workerd.ts +26 -0
  72. package/src/make-sqlite-db.ts +1 -0
  73. package/src/node/NodeFS.ts +1 -9
  74. package/src/node/mod.ts +3 -10
@@ -0,0 +1,567 @@
1
+ /// <reference types="vitest/globals" />
2
+
3
+ import type { CfTypes } from '@livestore/common-cf'
4
+ import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
5
+ import { beforeEach, describe, expect, it } from 'vitest'
6
+ import { CloudflareWorkerVFS } from '../../mod.ts'
7
+
8
+ describe('CloudflareWorkerVFS - Integration Tests', () => {
9
+ let vfs: CloudflareWorkerVFS
10
+ let mockStorage: CfTypes.DurableObjectStorage
11
+ let storageData: Map<string, any>
12
+ let storageOperations: string[]
13
+
14
+ beforeEach(async () => {
15
+ storageData = new Map<string, any>()
16
+ storageOperations = []
17
+
18
+ mockStorage = {
19
+ get: (async (_key: string | string[]) => {
20
+ if (Array.isArray(_key)) {
21
+ storageOperations.push(`get-batch: ${_key.length} keys`)
22
+ const result = new Map()
23
+ for (const k of _key) {
24
+ const value = storageData.get(k)
25
+ if (value !== undefined) {
26
+ result.set(k, value)
27
+ }
28
+ }
29
+ return result
30
+ } else {
31
+ storageOperations.push(`get: ${_key}`)
32
+ return storageData.get(_key)
33
+ }
34
+ }) as CfTypes.DurableObjectStorage['get'],
35
+
36
+ put: async (_key: string | Record<string, any>, _value?: any) => {
37
+ if (typeof _key === 'string') {
38
+ storageOperations.push(`put: ${_key}`)
39
+ storageData.set(_key, _value)
40
+ } else {
41
+ storageOperations.push(`put-batch: ${Object.keys(_key).length} keys`)
42
+ for (const [k, v] of Object.entries(_key)) {
43
+ storageData.set(k, v)
44
+ }
45
+ }
46
+ },
47
+
48
+ delete: (async (_key: string | string[]) => {
49
+ if (Array.isArray(_key)) {
50
+ storageOperations.push(`delete-batch: ${_key.length} keys`)
51
+ let count = 0
52
+ for (const k of _key) {
53
+ if (storageData.delete(k)) count++
54
+ }
55
+ return count
56
+ } else {
57
+ storageOperations.push(`delete: ${_key}`)
58
+ return storageData.delete(_key)
59
+ }
60
+ }) as CfTypes.DurableObjectStorage['delete'],
61
+
62
+ list: async () => {
63
+ storageOperations.push('list')
64
+ return new Map(storageData)
65
+ },
66
+
67
+ sync: async () => {
68
+ storageOperations.push('sync')
69
+ },
70
+
71
+ transactionSync: (fn: () => any) => {
72
+ storageOperations.push('transactionSync')
73
+ return fn()
74
+ },
75
+
76
+ deleteAll: async () => {
77
+ storageOperations.push('deleteAll')
78
+ storageData.clear()
79
+ },
80
+
81
+ transaction: async (fn: (txn: any) => Promise<any>) => {
82
+ storageOperations.push('transaction')
83
+ return await fn({} as any)
84
+ },
85
+
86
+ getCurrentBookmark: async () => {
87
+ storageOperations.push('getCurrentBookmark')
88
+ return ''
89
+ },
90
+
91
+ getBookmarkForTime: async (_time: number | Date) => {
92
+ storageOperations.push('getBookmarkForTime')
93
+ return ''
94
+ },
95
+
96
+ onNextSessionRestoreBookmark: async (_bookmark: string) => {
97
+ storageOperations.push('onNextSessionRestoreBookmark')
98
+ return ''
99
+ },
100
+
101
+ getAlarm: async () => null,
102
+ setAlarm: async (_timestamp: number | Date) => {},
103
+ deleteAlarm: async () => {},
104
+ sql: {} as any,
105
+ }
106
+
107
+ vfs = new CloudflareWorkerVFS('test-integration-vfs', mockStorage, {})
108
+ await vfs.isReady()
109
+ })
110
+
111
+ describe('Storage Integration', () => {
112
+ it('should integrate properly with DurableObjectStorage API', async () => {
113
+ const path = '/test/storage-integration.db'
114
+ const fileId = 1
115
+ const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
116
+ const pOutFlags = new DataView(new ArrayBuffer(4))
117
+
118
+ storageOperations.length = 0
119
+
120
+ vfs.jOpen(path, fileId, flags, pOutFlags)
121
+
122
+ // Write data that will trigger storage operations
123
+ const testData = new TextEncoder().encode('Storage integration test')
124
+ expect(vfs.jWrite(fileId, testData, 0)).toBe(VFS.SQLITE_OK)
125
+
126
+ // Sync to ensure storage operations occur
127
+ expect(vfs.jSync(fileId, VFS.SQLITE_SYNC_NORMAL)).toBe(VFS.SQLITE_OK)
128
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
129
+
130
+ // Wait for async operations
131
+ await new Promise((resolve) => setTimeout(resolve, 20))
132
+
133
+ // Verify storage operations occurred
134
+ expect(storageOperations.length).toBeGreaterThan(0)
135
+
136
+ // Verify metadata and chunk keys exist in storage
137
+ const metadataKey = `file:${path}:meta`
138
+ const chunkKey = `file:${path}:0`
139
+
140
+ expect(storageData.has(metadataKey)).toBe(true)
141
+ expect(storageData.has(chunkKey)).toBe(true)
142
+
143
+ // Verify stored data integrity
144
+ const metadata = storageData.get(metadataKey)
145
+ expect(metadata.size).toBe(testData.length)
146
+ expect(metadata.flags).toBe(flags)
147
+
148
+ const chunk = storageData.get(chunkKey)
149
+ expect(chunk.slice(0, testData.length)).toEqual(testData)
150
+ })
151
+
152
+ it('should handle storage key collisions gracefully', async () => {
153
+ const paths = ['/test/path:with:colons.db', '/test/path_with_colons.db', '/test/pathwithcolons.db']
154
+
155
+ for (let i = 0; i < paths.length; i++) {
156
+ const fileId = i + 1
157
+ const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
158
+ const pOutFlags = new DataView(new ArrayBuffer(4))
159
+
160
+ expect(vfs.jOpen(paths[i] ?? '', fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
161
+
162
+ const testData = new TextEncoder().encode(`Data for file ${i}`)
163
+ expect(vfs.jWrite(fileId, testData, 0)).toBe(VFS.SQLITE_OK)
164
+
165
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
166
+ }
167
+
168
+ // Wait for async operations
169
+ await new Promise((resolve) => setTimeout(resolve, 20))
170
+
171
+ // Verify all files have separate storage entries
172
+ for (const path of paths) {
173
+ const metadataKey = `file:${path}:meta`
174
+ expect(storageData.has(metadataKey)).toBe(true)
175
+ }
176
+
177
+ // Verify no data corruption between files
178
+ for (let i = 0; i < paths.length; i++) {
179
+ const fileId = i + 1
180
+ const flags = VFS.SQLITE_OPEN_READWRITE
181
+ const pOutFlags = new DataView(new ArrayBuffer(4))
182
+
183
+ expect(vfs.jOpen(paths[i] ?? '', fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
184
+
185
+ const expectedData = new TextEncoder().encode(`Data for file ${i}`)
186
+ const readData = new Uint8Array(expectedData.length)
187
+ expect(vfs.jRead(fileId, readData, 0)).toBe(VFS.SQLITE_OK)
188
+ expect(readData).toEqual(expectedData)
189
+
190
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
191
+ }
192
+ })
193
+
194
+ it('should handle storage size limits correctly', async () => {
195
+ const path = '/test/size-limits.db'
196
+ const fileId = 1
197
+ const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
198
+ const pOutFlags = new DataView(new ArrayBuffer(4))
199
+
200
+ vfs.jOpen(path, fileId, flags, pOutFlags)
201
+
202
+ // Write data that approaches DurableObjectStorage limits (128 KiB per value)
203
+ const chunkSize = 64 * 1024 // VFS chunk size
204
+ const testData = new Uint8Array(chunkSize)
205
+ testData.fill(0xaa)
206
+
207
+ expect(vfs.jWrite(fileId, testData, 0)).toBe(VFS.SQLITE_OK)
208
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
209
+
210
+ // Wait for async operations
211
+ await new Promise((resolve) => setTimeout(resolve, 10))
212
+
213
+ // Verify chunk was stored correctly
214
+ const chunkKey = `file:${path}:0`
215
+ expect(storageData.has(chunkKey)).toBe(true)
216
+
217
+ const storedChunk = storageData.get(chunkKey)
218
+ expect(storedChunk.length).toBe(chunkSize)
219
+ expect(storedChunk.length).toBeLessThanOrEqual(128 * 1024) // Within DO Storage limit
220
+ })
221
+
222
+ it('should handle batch operations efficiently', async () => {
223
+ const path = '/test/batch-operations.db'
224
+ const fileId = 1
225
+ const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
226
+ const pOutFlags = new DataView(new ArrayBuffer(4))
227
+
228
+ vfs.jOpen(path, fileId, flags, pOutFlags)
229
+
230
+ // Write data that spans multiple chunks
231
+ const chunkSize = 64 * 1024
232
+ const numChunks = 3
233
+ const totalData = new Uint8Array(chunkSize * numChunks)
234
+
235
+ for (let i = 0; i < totalData.length; i++) {
236
+ totalData[i] = (i * 3) % 256
237
+ }
238
+
239
+ storageOperations.length = 0
240
+ expect(vfs.jWrite(fileId, totalData, 0)).toBe(VFS.SQLITE_OK)
241
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
242
+
243
+ // Wait for async operations
244
+ await new Promise((resolve) => setTimeout(resolve, 20))
245
+
246
+ // Verify storage operations were batched efficiently
247
+ expect(storageOperations.length).toBeGreaterThan(0)
248
+
249
+ // Verify all chunks were stored
250
+ for (let i = 0; i < numChunks; i++) {
251
+ const chunkKey = `file:${path}:${i}`
252
+ expect(storageData.has(chunkKey)).toBe(true)
253
+ }
254
+
255
+ // Verify data integrity
256
+ vfs.jOpen(path, fileId, flags, pOutFlags)
257
+ const readData = new Uint8Array(totalData.length)
258
+ expect(vfs.jRead(fileId, readData, 0)).toBe(VFS.SQLITE_OK)
259
+ expect(readData).toEqual(totalData)
260
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
261
+ })
262
+ })
263
+
264
+ describe('SQLite Integration', () => {
265
+ it('should handle SQLite database file format correctly', async () => {
266
+ const path = '/test/sqlite-format.db'
267
+ const fileId = 1
268
+ const flags = VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
269
+ const pOutFlags = new DataView(new ArrayBuffer(4))
270
+
271
+ expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
272
+
273
+ // Write SQLite header
274
+ const sqliteHeader = new TextEncoder().encode('SQLite format 3\0')
275
+ expect(vfs.jWrite(fileId, sqliteHeader, 0)).toBe(VFS.SQLITE_OK)
276
+
277
+ // Write page size (typical SQLite page size is 4096)
278
+ const pageSize = new DataView(new ArrayBuffer(2))
279
+ pageSize.setUint16(0, 4096, false) // Big-endian as per SQLite format
280
+ const pageSizeBytes = new Uint8Array(pageSize.buffer)
281
+ expect(vfs.jWrite(fileId, pageSizeBytes, 16)).toBe(VFS.SQLITE_OK)
282
+
283
+ // Read back and verify header
284
+ const readHeader = new Uint8Array(sqliteHeader.length)
285
+ expect(vfs.jRead(fileId, readHeader, 0)).toBe(VFS.SQLITE_OK)
286
+ expect(readHeader).toEqual(sqliteHeader)
287
+
288
+ // Read back and verify page size
289
+ const readPageSize = new Uint8Array(2)
290
+ expect(vfs.jRead(fileId, readPageSize, 16)).toBe(VFS.SQLITE_OK)
291
+ expect(readPageSize).toEqual(pageSizeBytes)
292
+
293
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
294
+ })
295
+
296
+ it('should handle SQLite page-based I/O operations', async () => {
297
+ const path = '/test/sqlite-pages.db'
298
+ const fileId = 1
299
+ const flags = VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
300
+ const pOutFlags = new DataView(new ArrayBuffer(4))
301
+
302
+ vfs.jOpen(path, fileId, flags, pOutFlags)
303
+
304
+ const pageSize = 4096
305
+ const numPages = 10
306
+
307
+ // Write multiple SQLite pages
308
+ for (let pageNum = 0; pageNum < numPages; pageNum++) {
309
+ const pageData = new Uint8Array(pageSize)
310
+
311
+ // Fill page with pattern (page number repeated)
312
+ pageData.fill(pageNum % 256)
313
+
314
+ const offset = pageNum * pageSize
315
+ expect(vfs.jWrite(fileId, pageData, offset)).toBe(VFS.SQLITE_OK)
316
+ }
317
+
318
+ // Read back pages in different order
319
+ const readOrder = [3, 1, 7, 0, 9, 2, 5, 8, 4, 6]
320
+ for (const pageNum of readOrder) {
321
+ const readData = new Uint8Array(pageSize)
322
+ const offset = pageNum * pageSize
323
+
324
+ expect(vfs.jRead(fileId, readData, offset)).toBe(VFS.SQLITE_OK)
325
+
326
+ // Verify page content
327
+ const expectedValue = pageNum % 256
328
+ expect(readData.every((byte) => byte === expectedValue)).toBe(true)
329
+ }
330
+
331
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
332
+ })
333
+
334
+ it('should handle WAL mode operations', async () => {
335
+ const mainPath = '/test/wal-mode.db'
336
+ const walPath = '/test/wal-mode.db-wal'
337
+
338
+ const mainFileId = 1
339
+ const walFileId = 2
340
+
341
+ const mainFlags = VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
342
+ const walFlags = VFS.SQLITE_OPEN_WAL | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
343
+ const pOutFlags = new DataView(new ArrayBuffer(4))
344
+
345
+ // Open main database file
346
+ expect(vfs.jOpen(mainPath, mainFileId, mainFlags, pOutFlags)).toBe(VFS.SQLITE_OK)
347
+
348
+ // Write main database content
349
+ const dbContent = new TextEncoder().encode('SQLite format 3\0Main database content')
350
+ expect(vfs.jWrite(mainFileId, dbContent, 0)).toBe(VFS.SQLITE_OK)
351
+
352
+ // Open WAL file
353
+ expect(vfs.jOpen(walPath, walFileId, walFlags, pOutFlags)).toBe(VFS.SQLITE_OK)
354
+
355
+ // Write WAL header and entries
356
+ const walHeader = new Uint8Array(32)
357
+ walHeader.fill(0x37) // WAL magic number pattern
358
+ expect(vfs.jWrite(walFileId, walHeader, 0)).toBe(VFS.SQLITE_OK)
359
+
360
+ // Write WAL frames
361
+ const frameSize = 4096 + 24 // Page size + frame header
362
+ const numFrames = 5
363
+
364
+ for (let frameNum = 0; frameNum < numFrames; frameNum++) {
365
+ const frameData = new Uint8Array(frameSize)
366
+ frameData.fill((frameNum + 1) % 256)
367
+
368
+ const offset = 32 + frameNum * frameSize // After WAL header
369
+ expect(vfs.jWrite(walFileId, frameData, offset)).toBe(VFS.SQLITE_OK)
370
+ }
371
+
372
+ // Read back WAL header
373
+ const readWalHeader = new Uint8Array(32)
374
+ expect(vfs.jRead(walFileId, readWalHeader, 0)).toBe(VFS.SQLITE_OK)
375
+ expect(readWalHeader).toEqual(walHeader)
376
+
377
+ // Read back WAL frames
378
+ for (let frameNum = 0; frameNum < numFrames; frameNum++) {
379
+ const readFrameData = new Uint8Array(frameSize)
380
+ const offset = 32 + frameNum * frameSize
381
+
382
+ expect(vfs.jRead(walFileId, readFrameData, offset)).toBe(VFS.SQLITE_OK)
383
+
384
+ const expectedValue = (frameNum + 1) % 256
385
+ expect(readFrameData.every((byte) => byte === expectedValue)).toBe(true)
386
+ }
387
+
388
+ // Close both files
389
+ expect(vfs.jClose(mainFileId)).toBe(VFS.SQLITE_OK)
390
+ expect(vfs.jClose(walFileId)).toBe(VFS.SQLITE_OK)
391
+ })
392
+
393
+ it('should handle journal mode operations', async () => {
394
+ const mainPath = '/test/journal-mode.db'
395
+ const journalPath = '/test/journal-mode.db-journal'
396
+
397
+ const mainFileId = 1
398
+ const journalFileId = 2
399
+
400
+ const mainFlags = VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
401
+ const journalFlags = VFS.SQLITE_OPEN_MAIN_JOURNAL | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
402
+ const pOutFlags = new DataView(new ArrayBuffer(4))
403
+
404
+ // Open main database
405
+ expect(vfs.jOpen(mainPath, mainFileId, mainFlags, pOutFlags)).toBe(VFS.SQLITE_OK)
406
+
407
+ // Write database pages
408
+ const pageSize = 4096
409
+ const dbPage = new Uint8Array(pageSize)
410
+ dbPage.fill(0xdb) // DB page pattern
411
+ expect(vfs.jWrite(mainFileId, dbPage, 0)).toBe(VFS.SQLITE_OK)
412
+
413
+ // Open journal file
414
+ expect(vfs.jOpen(journalPath, journalFileId, journalFlags, pOutFlags)).toBe(VFS.SQLITE_OK)
415
+
416
+ // Write journal header
417
+ const journalHeader = new TextEncoder().encode('Journal header\0\0\0\0')
418
+ expect(vfs.jWrite(journalFileId, journalHeader, 0)).toBe(VFS.SQLITE_OK)
419
+
420
+ // Write journal page (copy of original page for rollback)
421
+ const journalPage = new Uint8Array(pageSize)
422
+ journalPage.fill(0x4a) // Journal page pattern
423
+ expect(vfs.jWrite(journalFileId, journalPage, 512)).toBe(VFS.SQLITE_OK)
424
+
425
+ // Verify journal operations
426
+ const readJournalHeader = new Uint8Array(journalHeader.length)
427
+ expect(vfs.jRead(journalFileId, readJournalHeader, 0)).toBe(VFS.SQLITE_OK)
428
+ expect(readJournalHeader).toEqual(journalHeader)
429
+
430
+ const readJournalPage = new Uint8Array(pageSize)
431
+ expect(vfs.jRead(journalFileId, readJournalPage, 512)).toBe(VFS.SQLITE_OK)
432
+ expect(readJournalPage).toEqual(journalPage)
433
+
434
+ // Close files
435
+ expect(vfs.jClose(mainFileId)).toBe(VFS.SQLITE_OK)
436
+ expect(vfs.jClose(journalFileId)).toBe(VFS.SQLITE_OK)
437
+ })
438
+
439
+ it('should handle database file locking simulation', async () => {
440
+ const path = '/test/locking-simulation.db'
441
+ const fileId1 = 1
442
+ const fileId2 = 2
443
+
444
+ const flags = VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
445
+ const pOutFlags = new DataView(new ArrayBuffer(4))
446
+
447
+ // Open file with first handle
448
+ expect(vfs.jOpen(path, fileId1, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
449
+
450
+ // Write data with first handle
451
+ const testData1 = new TextEncoder().encode('First connection data')
452
+ expect(vfs.jWrite(fileId1, testData1, 0)).toBe(VFS.SQLITE_OK)
453
+
454
+ // Open same file with second handle (simulates multiple connections)
455
+ expect(vfs.jOpen(path, fileId2, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
456
+
457
+ // Read with second handle should see data from first handle
458
+ const readData = new Uint8Array(testData1.length)
459
+ expect(vfs.jRead(fileId2, readData, 0)).toBe(VFS.SQLITE_OK)
460
+ expect(readData).toEqual(testData1)
461
+
462
+ // Write with second handle
463
+ const testData2 = new TextEncoder().encode('Second connection data')
464
+ expect(vfs.jWrite(fileId2, testData2, 100)).toBe(VFS.SQLITE_OK)
465
+
466
+ // First handle should see data from second handle
467
+ const readData2 = new Uint8Array(testData2.length)
468
+ expect(vfs.jRead(fileId1, readData2, 100)).toBe(VFS.SQLITE_OK)
469
+ expect(readData2).toEqual(testData2)
470
+
471
+ // Close both handles
472
+ expect(vfs.jClose(fileId1)).toBe(VFS.SQLITE_OK)
473
+ expect(vfs.jClose(fileId2)).toBe(VFS.SQLITE_OK)
474
+ })
475
+ })
476
+
477
+ describe('End-to-End Integration', () => {
478
+ it('should handle complete SQLite workflow', async () => {
479
+ const dbPath = '/test/complete-workflow.db'
480
+ const walPath = '/test/complete-workflow.db-wal'
481
+ const journalPath = '/test/complete-workflow.db-journal'
482
+
483
+ const dbFileId = 1
484
+ const walFileId = 2
485
+ const journalFileId = 3
486
+
487
+ const pOutFlags = new DataView(new ArrayBuffer(4))
488
+
489
+ // 1. Create main database
490
+ const dbFlags = VFS.SQLITE_OPEN_MAIN_DB | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
491
+ expect(vfs.jOpen(dbPath, dbFileId, dbFlags, pOutFlags)).toBe(VFS.SQLITE_OK)
492
+
493
+ // Write SQLite header and initial pages
494
+ const sqliteHeader = new TextEncoder().encode('SQLite format 3\0')
495
+ expect(vfs.jWrite(dbFileId, sqliteHeader, 0)).toBe(VFS.SQLITE_OK)
496
+
497
+ const pageSize = 4096
498
+ const dbPage1 = new Uint8Array(pageSize)
499
+ dbPage1.fill(0x01)
500
+ expect(vfs.jWrite(dbFileId, dbPage1, pageSize)).toBe(VFS.SQLITE_OK)
501
+
502
+ // 2. Create journal for transaction
503
+ const journalFlags = VFS.SQLITE_OPEN_MAIN_JOURNAL | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
504
+ expect(vfs.jOpen(journalPath, journalFileId, journalFlags, pOutFlags)).toBe(VFS.SQLITE_OK)
505
+
506
+ // Write journal header and backup page
507
+ const journalHeader = new Uint8Array(512)
508
+ journalHeader.fill(0x30) // Journal header pattern
509
+ expect(vfs.jWrite(journalFileId, journalHeader, 0)).toBe(VFS.SQLITE_OK)
510
+
511
+ // 3. Modify database page (transaction)
512
+ const modifiedPage = new Uint8Array(pageSize)
513
+ modifiedPage.fill(0x02) // Modified page pattern
514
+ expect(vfs.jWrite(dbFileId, modifiedPage, pageSize)).toBe(VFS.SQLITE_OK)
515
+
516
+ // 4. Sync database (commit transaction)
517
+ expect(vfs.jSync(dbFileId, VFS.SQLITE_SYNC_NORMAL)).toBe(VFS.SQLITE_OK)
518
+
519
+ // 5. Delete journal (transaction committed)
520
+ expect(vfs.jClose(journalFileId)).toBe(VFS.SQLITE_OK)
521
+ expect(vfs.jDelete(journalPath, 0)).toBe(VFS.SQLITE_OK)
522
+
523
+ // 6. Switch to WAL mode
524
+ const walFlags = VFS.SQLITE_OPEN_WAL | VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
525
+ expect(vfs.jOpen(walPath, walFileId, walFlags, pOutFlags)).toBe(VFS.SQLITE_OK)
526
+
527
+ // Write WAL header
528
+ const walHeader = new Uint8Array(32)
529
+ walHeader.fill(0x37) // WAL header pattern
530
+ expect(vfs.jWrite(walFileId, walHeader, 0)).toBe(VFS.SQLITE_OK)
531
+
532
+ // Write WAL frame
533
+ const walFrame = new Uint8Array(pageSize + 24) // Page + frame header
534
+ walFrame.fill(0x03) // WAL frame pattern
535
+ expect(vfs.jWrite(walFileId, walFrame, 32)).toBe(VFS.SQLITE_OK)
536
+
537
+ // 7. Verify all files are accessible and contain expected data
538
+ // Database file
539
+ const readHeader = new Uint8Array(sqliteHeader.length)
540
+ expect(vfs.jRead(dbFileId, readHeader, 0)).toBe(VFS.SQLITE_OK)
541
+ expect(readHeader).toEqual(sqliteHeader)
542
+
543
+ const readDbPage = new Uint8Array(pageSize)
544
+ expect(vfs.jRead(dbFileId, readDbPage, pageSize)).toBe(VFS.SQLITE_OK)
545
+ expect(readDbPage).toEqual(modifiedPage)
546
+
547
+ // WAL file
548
+ const readWalHeader = new Uint8Array(32)
549
+ expect(vfs.jRead(walFileId, readWalHeader, 0)).toBe(VFS.SQLITE_OK)
550
+ expect(readWalHeader).toEqual(walHeader)
551
+
552
+ // 8. Close all files
553
+ expect(vfs.jClose(dbFileId)).toBe(VFS.SQLITE_OK)
554
+ expect(vfs.jClose(walFileId)).toBe(VFS.SQLITE_OK)
555
+
556
+ // 9. Verify persistence across VFS sessions
557
+ await new Promise((resolve) => setTimeout(resolve, 20))
558
+
559
+ expect(vfs.jOpen(dbPath, dbFileId, dbFlags, pOutFlags)).toBe(VFS.SQLITE_OK)
560
+ const persistentReadHeader = new Uint8Array(sqliteHeader.length)
561
+ expect(vfs.jRead(dbFileId, persistentReadHeader, 0)).toBe(VFS.SQLITE_OK)
562
+ expect(persistentReadHeader).toEqual(sqliteHeader)
563
+
564
+ expect(vfs.jClose(dbFileId)).toBe(VFS.SQLITE_OK)
565
+ })
566
+ })
567
+ })