@livestore/sqlite-wasm 0.4.0-dev.22 → 0.4.0-dev.24

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/README.md +1 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/FacadeVFS.d.ts +0 -1
  4. package/dist/FacadeVFS.d.ts.map +1 -1
  5. package/dist/FacadeVFS.js +9 -14
  6. package/dist/FacadeVFS.js.map +1 -1
  7. package/dist/browser/mod.d.ts +2 -2
  8. package/dist/browser/mod.d.ts.map +1 -1
  9. package/dist/browser/mod.js +2 -2
  10. package/dist/browser/mod.js.map +1 -1
  11. package/dist/browser/opfs/AccessHandlePoolVFS.d.ts.map +1 -1
  12. package/dist/browser/opfs/AccessHandlePoolVFS.js +15 -13
  13. package/dist/browser/opfs/AccessHandlePoolVFS.js.map +1 -1
  14. package/dist/browser/opfs/opfs-sah-pool.js +3 -3
  15. package/dist/browser/opfs/opfs-sah-pool.js.map +1 -1
  16. package/dist/cf/CloudflareDurableObjectVFS.d.ts +104 -0
  17. package/dist/cf/CloudflareDurableObjectVFS.d.ts.map +1 -0
  18. package/dist/cf/CloudflareDurableObjectVFS.js +281 -0
  19. package/dist/cf/CloudflareDurableObjectVFS.js.map +1 -0
  20. package/dist/cf/CloudflareWorkerVFS.d.ts.map +1 -1
  21. package/dist/cf/CloudflareWorkerVFS.js +29 -28
  22. package/dist/cf/CloudflareWorkerVFS.js.map +1 -1
  23. package/dist/cf/mod.d.ts +3 -4
  24. package/dist/cf/mod.d.ts.map +1 -1
  25. package/dist/cf/mod.js +4 -12
  26. package/dist/cf/mod.js.map +1 -1
  27. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js +5 -4
  28. package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js.map +1 -1
  29. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js +3 -3
  30. package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js.map +1 -1
  31. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js +3 -3
  32. package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js.map +1 -1
  33. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js +3 -3
  34. package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js.map +1 -1
  35. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js +194 -179
  36. package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js.map +1 -1
  37. package/dist/in-memory-vfs.d.ts.map +1 -1
  38. package/dist/in-memory-vfs.js +0 -1
  39. package/dist/in-memory-vfs.js.map +1 -1
  40. package/dist/load-wasm/mod.node.js +1 -1
  41. package/dist/load-wasm/mod.node.js.map +1 -1
  42. package/dist/load-wasm/mod.workerd.d.ts.map +1 -1
  43. package/dist/load-wasm/mod.workerd.js.map +1 -1
  44. package/dist/make-sqlite-db.d.ts.map +1 -1
  45. package/dist/make-sqlite-db.js +16 -4
  46. package/dist/make-sqlite-db.js.map +1 -1
  47. package/dist/node/NodeFS.d.ts.map +1 -1
  48. package/dist/node/NodeFS.js +13 -13
  49. package/dist/node/NodeFS.js.map +1 -1
  50. package/package.json +54 -15
  51. package/src/FacadeVFS.ts +9 -14
  52. package/src/browser/mod.ts +1 -1
  53. package/src/browser/opfs/AccessHandlePoolVFS.ts +33 -25
  54. package/src/browser/opfs/opfs-sah-pool.ts +3 -3
  55. package/src/cf/CloudflareDurableObjectVFS.ts +325 -0
  56. package/src/cf/CloudflareWorkerVFS.ts +41 -39
  57. package/src/cf/README.md +3 -3
  58. package/src/cf/mod.ts +10 -15
  59. package/src/cf/test/README.md +55 -26
  60. package/src/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.ts +6 -4
  61. package/src/cf/test/async-storage/cloudflare-worker-vfs-core.test.ts +4 -3
  62. package/src/cf/test/async-storage/cloudflare-worker-vfs-integration.test.ts +4 -3
  63. package/src/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.ts +4 -3
  64. package/src/cf/test/sql/cloudflare-sql-vfs-core.test.ts +228 -197
  65. package/src/in-memory-vfs.ts +0 -1
  66. package/src/load-wasm/mod.node.ts +1 -1
  67. package/src/load-wasm/mod.workerd.ts +0 -1
  68. package/src/make-sqlite-db.ts +24 -4
  69. package/src/node/NodeFS.ts +24 -23
  70. package/dist/cf/BlockManager.d.ts +0 -61
  71. package/dist/cf/BlockManager.d.ts.map +0 -1
  72. package/dist/cf/BlockManager.js +0 -157
  73. package/dist/cf/BlockManager.js.map +0 -1
  74. package/dist/cf/CloudflareSqlVFS.d.ts +0 -51
  75. package/dist/cf/CloudflareSqlVFS.d.ts.map +0 -1
  76. package/dist/cf/CloudflareSqlVFS.js +0 -351
  77. package/dist/cf/CloudflareSqlVFS.js.map +0 -1
  78. package/src/cf/BlockManager.ts +0 -225
  79. package/src/cf/CloudflareSqlVFS.ts +0 -450
@@ -3,18 +3,42 @@
3
3
  import type { CfTypes } from '@livestore/common-cf'
4
4
  import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
5
5
  import { beforeEach, describe, expect, it } from 'vitest'
6
- import { CloudflareSqlVFS } from '../../mod.ts'
7
6
 
8
- describe('CloudflareSqlVFS - Core Functionality', () => {
9
- let vfs: CloudflareSqlVFS
7
+ import { CF_SQL_VFS_PAGE_SIZE, CloudflareDurableObjectVFS } from '../../mod.ts'
8
+
9
+ const makePage = (fillByte: number, text?: string): Uint8Array => {
10
+ const page = new Uint8Array(CF_SQL_VFS_PAGE_SIZE)
11
+ page.fill(fillByte)
12
+
13
+ if (text !== undefined) {
14
+ page.set(new TextEncoder().encode(text))
15
+ }
16
+
17
+ return page
18
+ }
19
+
20
+ describe('CloudflareDurableObjectVFS - Core Functionality', () => {
21
+ let vfs: CloudflareDurableObjectVFS
10
22
  let mockSql: CfTypes.SqlStorage
11
- let mockDatabase: Map<string, any[]>
23
+ /** In-memory page store keyed by file_path and page_no */
24
+ let mockPages: Map<string, Map<number, Uint8Array>>
12
25
  let queryLog: string[]
13
26
 
14
27
  beforeEach(async () => {
15
- mockDatabase = new Map()
28
+ mockPages = new Map()
16
29
  queryLog = []
17
30
 
31
+ const getOrCreateFilePages = (filePath: string) => {
32
+ let pages = mockPages.get(filePath)
33
+
34
+ if (pages === undefined) {
35
+ pages = new Map()
36
+ mockPages.set(filePath, pages)
37
+ }
38
+
39
+ return pages
40
+ }
41
+
18
42
  // Mock SQL storage that mimics the Cloudflare DurableObject SQL API
19
43
  mockSql = {
20
44
  exec: <T extends Record<string, CfTypes.SqlStorageValue>>(
@@ -23,118 +47,114 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
23
47
  ): CfTypes.SqlStorageCursor<T> => {
24
48
  queryLog.push(`${query} [${bindings.join(', ')}]`)
25
49
 
26
- // Simple SQL parser for testing - handles basic CREATE, INSERT, SELECT, UPDATE, DELETE
27
- const normalizedQuery = query.trim().toUpperCase()
50
+ const normalizedQuery = query.trim().replace(/\s+/g, ' ').toUpperCase()
28
51
 
29
- if (
30
- normalizedQuery.includes('CREATE TABLE') ||
31
- normalizedQuery.includes('CREATE INDEX') ||
32
- normalizedQuery.includes('CREATE TRIGGER')
33
- ) {
34
- // Handle schema creation
52
+ if (normalizedQuery.includes('CREATE TABLE') === true) {
35
53
  return createMockCursor([] as any)
36
54
  }
37
55
 
38
- if (normalizedQuery.startsWith('INSERT OR REPLACE INTO VFS_BLOCKS')) {
39
- const [filePath, blockId, blockData] = bindings
40
- const key = `blocks:${filePath}`
41
- if (!mockDatabase.has(key)) {
42
- mockDatabase.set(key, [])
43
- }
44
- const blocks = mockDatabase.get(key)!
45
- const existingIndex = blocks.findIndex((b: any) => b.block_id === blockId)
46
- const blockEntry = { file_path: filePath, block_id: blockId, block_data: blockData }
47
-
48
- if (existingIndex >= 0) {
49
- blocks[existingIndex] = blockEntry
50
- } else {
51
- blocks.push(blockEntry)
52
- }
56
+ // INSERT OR REPLACE INTO vfs_pages (file_path, page_no, page_data) VALUES (?, ?, ?)
57
+ if (normalizedQuery.startsWith('INSERT OR REPLACE INTO VFS_PAGES') === true) {
58
+ const [filePath, pageNo, pageData] = bindings
59
+ getOrCreateFilePages(filePath as string).set(
60
+ pageNo as number,
61
+ pageData instanceof Uint8Array ? pageData : new Uint8Array(pageData),
62
+ )
53
63
  return createMockCursor([] as any)
54
64
  }
55
65
 
56
- if (normalizedQuery.startsWith('INSERT INTO VFS_FILES')) {
57
- const [filePath, fileSize, flags, createdAt, modifiedAt] = bindings
58
- mockDatabase.set(`file:${filePath}`, {
59
- file_path: filePath as string,
60
- file_size: fileSize as number,
61
- flags: flags as number,
62
- created_at: createdAt as number,
63
- modified_at: modifiedAt as number,
64
- } as any)
65
- return createMockCursor([] as any)
66
+ // SELECT page_data FROM vfs_pages WHERE file_path = ? AND page_no = ?
67
+ if (normalizedQuery.startsWith('SELECT PAGE_DATA FROM VFS_PAGES WHERE FILE_PATH = ? AND PAGE_NO = ?') === true) {
68
+ const [filePath, pageNo] = bindings
69
+ const data = mockPages.get(filePath as string)?.get(pageNo as number)
70
+ return createMockCursor(data !== undefined ? [{ page_data: data }] as any : [] as any)
66
71
  }
67
72
 
68
- if (normalizedQuery.startsWith('SELECT') && normalizedQuery.includes('FROM VFS_FILES')) {
69
- if (normalizedQuery.includes('WHERE FILE_PATH = ?')) {
70
- const [filePath] = bindings
71
- const fileData = mockDatabase.get(`file:${filePath}`)
72
- return createMockCursor(fileData ? [fileData] : ([] as any))
73
+ // SELECT 1 AS x FROM vfs_pages LIMIT 1
74
+ if (normalizedQuery.includes('SELECT 1 AS X FROM VFS_PAGES') === true) {
75
+ if (mockPages.size > 0) {
76
+ return createMockCursor([{ x: 1 }] as any)
73
77
  }
74
78
  return createMockCursor([] as any)
75
79
  }
76
80
 
77
- if (normalizedQuery.startsWith('SELECT') && normalizedQuery.includes('FROM VFS_BLOCKS')) {
78
- if (normalizedQuery.includes('WHERE FILE_PATH = ?')) {
79
- const filePath = bindings[0]
80
- const blocks = mockDatabase.get(`blocks:${filePath}`) || []
81
-
82
- if (normalizedQuery.includes('AND BLOCK_ID IN')) {
83
- const requestedBlockIds = bindings.slice(1)
84
- const matchingBlocks = blocks.filter((b: any) => requestedBlockIds.includes(b.block_id))
85
- return createMockCursor(matchingBlocks as any)
86
- }
81
+ // SELECT MAX(page_no) AS max_page FROM vfs_pages WHERE file_path = ?
82
+ if (normalizedQuery.includes('SELECT MAX(PAGE_NO) AS MAX_PAGE FROM VFS_PAGES WHERE FILE_PATH = ?') === true) {
83
+ const [filePath] = bindings
84
+ const pages = mockPages.get(filePath as string)
87
85
 
88
- return createMockCursor(blocks as any)
86
+ if (pages === undefined || pages.size === 0) {
87
+ return createMockCursor([{ max_page: null }] as any)
89
88
  }
90
- return createMockCursor([] as any)
89
+
90
+ const maxPage = Math.max(...pages.keys())
91
+ return createMockCursor([{ max_page: maxPage }] as any)
91
92
  }
92
93
 
93
- if (normalizedQuery.startsWith('UPDATE VFS_FILES')) {
94
- if (normalizedQuery.includes('SET FILE_SIZE = ?')) {
95
- const [newSize, filePath] = bindings
96
- const fileData = mockDatabase.get(`file:${filePath}`) as any
97
- if (fileData) {
98
- fileData.file_size = newSize as number
99
- fileData.modified_at = Math.floor(Date.now() / 1000)
94
+ // SELECT COUNT(*) AS total_pages, COALESCE(...) FROM vfs_pages
95
+ if (normalizedQuery.includes('FROM VFS_PAGES') === true && normalizedQuery.includes('COUNT(*)') === true) {
96
+ let totalBytes = 0
97
+ let totalPages = 0
98
+
99
+ for (const pages of mockPages.values()) {
100
+ totalPages += pages.size
101
+
102
+ for (const data of pages.values()) {
103
+ totalBytes += data.length
100
104
  }
101
105
  }
102
- return createMockCursor([] as any)
106
+
107
+ return createMockCursor([{ total_pages: totalPages, total_bytes: totalBytes }] as any)
103
108
  }
104
109
 
105
- if (normalizedQuery.startsWith('DELETE FROM VFS_BLOCKS')) {
106
- const [filePath, minBlockId] = bindings
107
- const blocks = mockDatabase.get(`blocks:${filePath}`)
108
- if (blocks) {
109
- const filteredBlocks = blocks.filter((b: any) => b.block_id < minBlockId)
110
- mockDatabase.set(`blocks:${filePath}`, filteredBlocks)
110
+ // DELETE FROM vfs_pages WHERE file_path = ? AND page_no >= ?
111
+ if (normalizedQuery.startsWith('DELETE FROM VFS_PAGES WHERE FILE_PATH = ? AND PAGE_NO >= ?') === true) {
112
+ const [filePath, minPageNo] = bindings
113
+ const pages = mockPages.get(filePath as string)
114
+
115
+ if (pages !== undefined) {
116
+ for (const pageNo of pages.keys()) {
117
+ if (pageNo >= (minPageNo as number)) {
118
+ pages.delete(pageNo)
119
+ }
120
+ }
121
+
122
+ if (pages.size === 0) {
123
+ mockPages.delete(filePath as string)
124
+ }
111
125
  }
126
+
112
127
  return createMockCursor([] as any)
113
128
  }
114
129
 
115
- if (normalizedQuery.startsWith('DELETE FROM VFS_FILES')) {
130
+ // DELETE FROM vfs_pages WHERE file_path = ?
131
+ if (normalizedQuery === 'DELETE FROM VFS_PAGES WHERE FILE_PATH = ?') {
116
132
  const [filePath] = bindings
117
- mockDatabase.delete(`file:${filePath}`)
118
- mockDatabase.delete(`blocks:${filePath}`)
133
+ mockPages.delete(filePath as string)
134
+ return createMockCursor([] as any)
135
+ }
136
+
137
+ // DELETE FROM vfs_pages (no WHERE - full wipe)
138
+ if (normalizedQuery === 'DELETE FROM VFS_PAGES') {
139
+ mockPages.clear()
119
140
  return createMockCursor([] as any)
120
141
  }
121
142
 
122
- // Default empty result for unhandled queries
123
143
  console.warn('Unhandled query:', query, bindings)
124
144
  return createMockCursor([] as any)
125
145
  },
126
146
 
127
147
  get databaseSize(): number {
128
- return 1024 * 1024 // Mock 1MB database
148
+ return 1024 * 1024
129
149
  },
130
150
 
131
151
  Cursor: {} as any,
132
152
  Statement: {} as any,
133
153
  } as CfTypes.SqlStorage
134
154
 
135
- function createMockCursor<T extends Record<string, CfTypes.SqlStorageValue>>(
155
+ const createMockCursor = <T extends Record<string, CfTypes.SqlStorageValue>>(
136
156
  data: T[],
137
- ): CfTypes.SqlStorageCursor<T> {
157
+ ): CfTypes.SqlStorageCursor<T> => {
138
158
  let index = 0
139
159
 
140
160
  return {
@@ -153,7 +173,7 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
153
173
  },
154
174
  raw: function* () {
155
175
  for (const item of data) {
156
- yield Object.values(item) as CfTypes.SqlStorageValue[]
176
+ yield Object.values(item)
157
177
  }
158
178
  },
159
179
  columnNames: Object.keys(data[0] || {}),
@@ -171,12 +191,11 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
171
191
  } as CfTypes.SqlStorageCursor<T>
172
192
  }
173
193
 
174
- vfs = new CloudflareSqlVFS('test-sql-vfs', mockSql, {})
175
- await vfs.isReady()
194
+ vfs = new CloudflareDurableObjectVFS('test-sql-vfs', mockSql, {})
176
195
  })
177
196
 
178
197
  describe('Basic File Operations', () => {
179
- it('should create and open files', async () => {
198
+ it('should create and open files', () => {
180
199
  const path = '/test/basic.db'
181
200
  const fileId = 1
182
201
  const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
@@ -185,34 +204,33 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
185
204
  const result = vfs.jOpen(path, fileId, flags, pOutFlags)
186
205
  expect(result).toBe(VFS.SQLITE_OK)
187
206
  expect(pOutFlags.getUint32(0, true)).toBe(flags)
188
-
189
- expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
190
-
191
- // Verify file was created in mock database
192
- expect(mockDatabase.has(`file:${path}`)).toBe(true)
193
207
  })
194
208
 
195
- it('should handle file access checks', async () => {
196
- const path = '/test/access.db'
197
- const fileId = 1
198
- const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
209
+ it('should delete only the opened file on close when SQLITE_OPEN_DELETEONCLOSE is set', () => {
199
210
  const pOutFlags = new DataView(new ArrayBuffer(4))
200
211
 
201
- // File doesn't exist initially
202
- const pResOut = new DataView(new ArrayBuffer(4))
203
- expect(vfs.jAccess(path, VFS.SQLITE_ACCESS_EXISTS, pResOut)).toBe(VFS.SQLITE_OK)
204
- expect(pResOut.getUint32(0, true)).toBe(0)
205
-
206
- // Create file
207
- vfs.jOpen(path, fileId, flags, pOutFlags)
208
- vfs.jClose(fileId)
209
-
210
- // File should exist now
211
- expect(vfs.jAccess(path, VFS.SQLITE_ACCESS_EXISTS, pResOut)).toBe(VFS.SQLITE_OK)
212
- expect(pResOut.getUint32(0, true)).toBe(1)
212
+ const keepPath = '/test/keep-on-close.db'
213
+ const keepFileId = 1
214
+ const keepFlags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
215
+ vfs.jOpen(keepPath, keepFileId, keepFlags, pOutFlags)
216
+ const keepData = makePage(0x11, 'keep-me')
217
+ expect(vfs.jWrite(keepFileId, keepData, 0)).toBe(VFS.SQLITE_OK)
218
+
219
+ const deletePath = '/test/delete-on-close.db'
220
+ const deleteFileId = 2
221
+ const deleteFlags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE | VFS.SQLITE_OPEN_DELETEONCLOSE
222
+ vfs.jOpen(deletePath, deleteFileId, deleteFlags, pOutFlags)
223
+ expect(vfs.jWrite(deleteFileId, makePage(0x22, 'delete-me'), 0)).toBe(VFS.SQLITE_OK)
224
+
225
+ expect(vfs.jClose(deleteFileId)).toBe(VFS.SQLITE_OK)
226
+
227
+ const readBuffer = new Uint8Array(keepData.length)
228
+ expect(vfs.jRead(keepFileId, readBuffer, 0)).toBe(VFS.SQLITE_OK)
229
+ expect(readBuffer).toEqual(keepData)
230
+ expect(mockPages.has(deletePath)).toBe(false)
213
231
  })
214
232
 
215
- it('should handle basic read/write operations', async () => {
233
+ it('should handle basic read/write operations', () => {
216
234
  const path = '/test/readwrite.db'
217
235
  const fileId = 1
218
236
  const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
@@ -221,18 +239,36 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
221
239
  vfs.jOpen(path, fileId, flags, pOutFlags)
222
240
 
223
241
  // Write data
224
- const testData = new TextEncoder().encode('Hello, SQL VFS!')
242
+ const testData = makePage(0x33, 'Hello, SQL VFS!')
225
243
  expect(vfs.jWrite(fileId, testData, 0)).toBe(VFS.SQLITE_OK)
226
244
 
227
245
  // Read data back
228
246
  const readBuffer = new Uint8Array(testData.length)
229
247
  expect(vfs.jRead(fileId, readBuffer, 0)).toBe(VFS.SQLITE_OK)
230
248
  expect(readBuffer).toEqual(testData)
249
+ })
250
+
251
+ it('should isolate pages by file path', () => {
252
+ const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
253
+ const pOutFlags = new DataView(new ArrayBuffer(4))
254
+
255
+ const oldPath = '/test/state-old.db'
256
+ const oldFileId = 1
257
+ vfs.jOpen(oldPath, oldFileId, flags, pOutFlags)
258
+
259
+ const oldData = makePage(0x44, 'stale-state')
260
+ expect(vfs.jWrite(oldFileId, oldData, 0)).toBe(VFS.SQLITE_OK)
231
261
 
232
- vfs.jClose(fileId)
262
+ const newPath = '/test/state-new.db'
263
+ const newFileId = 2
264
+ vfs.jOpen(newPath, newFileId, flags, pOutFlags)
265
+
266
+ const readBuffer = new Uint8Array(oldData.length)
267
+ expect(vfs.jRead(newFileId, readBuffer, 0)).toBe(VFS.SQLITE_OK)
268
+ expect(readBuffer).toEqual(new Uint8Array(oldData.length))
233
269
  })
234
270
 
235
- it('should handle file size operations', async () => {
271
+ it('should handle file size operations', () => {
236
272
  const path = '/test/size.db'
237
273
  const fileId = 1
238
274
  const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
@@ -245,18 +281,17 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
245
281
  expect(vfs.jFileSize(fileId, pSize64)).toBe(VFS.SQLITE_OK)
246
282
  expect(pSize64.getBigInt64(0, true)).toBe(0n)
247
283
 
248
- // Write data and check size
249
- const testData = new Uint8Array(1000)
284
+ // Write one full page (8 KiB) and check size
285
+ const pageSize = CF_SQL_VFS_PAGE_SIZE
286
+ const testData = new Uint8Array(pageSize)
250
287
  testData.fill(0xaa)
251
288
  vfs.jWrite(fileId, testData, 0)
252
289
 
253
290
  expect(vfs.jFileSize(fileId, pSize64)).toBe(VFS.SQLITE_OK)
254
- expect(pSize64.getBigInt64(0, true)).toBe(1000n)
255
-
256
- vfs.jClose(fileId)
291
+ expect(pSize64.getBigInt64(0, true)).toBe(BigInt(pageSize))
257
292
  })
258
293
 
259
- it('should handle file truncation', async () => {
294
+ it('should handle file truncation', () => {
260
295
  const path = '/test/truncate.db'
261
296
  const fileId = 1
262
297
  const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
@@ -264,42 +299,21 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
264
299
 
265
300
  vfs.jOpen(path, fileId, flags, pOutFlags)
266
301
 
267
- // Write data
268
- const testData = new Uint8Array(2000)
269
- testData.fill(0xbb)
270
- vfs.jWrite(fileId, testData, 0)
302
+ // Write two full pages
303
+ const pageSize = CF_SQL_VFS_PAGE_SIZE
304
+ vfs.jWrite(fileId, makePage(0xbb), 0)
305
+ vfs.jWrite(fileId, makePage(0xcc), pageSize)
271
306
 
272
- // Truncate to smaller size
273
- expect(vfs.jTruncate(fileId, 500)).toBe(VFS.SQLITE_OK)
307
+ // Truncate to one page
308
+ expect(vfs.jTruncate(fileId, pageSize)).toBe(VFS.SQLITE_OK)
274
309
 
275
310
  // Verify size
276
311
  const pSize64 = new DataView(new ArrayBuffer(8))
277
312
  expect(vfs.jFileSize(fileId, pSize64)).toBe(VFS.SQLITE_OK)
278
- expect(pSize64.getBigInt64(0, true)).toBe(500n)
279
-
280
- vfs.jClose(fileId)
313
+ expect(pSize64.getBigInt64(0, true)).toBe(BigInt(pageSize))
281
314
  })
282
315
 
283
- it('should handle sync operations', async () => {
284
- const path = '/test/sync.db'
285
- const fileId = 1
286
- const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
287
- const pOutFlags = new DataView(new ArrayBuffer(4))
288
-
289
- vfs.jOpen(path, fileId, flags, pOutFlags)
290
-
291
- const testData = new TextEncoder().encode('Sync test data')
292
- vfs.jWrite(fileId, testData, 0)
293
-
294
- // Test different sync modes - should all be no-ops for SQL VFS
295
- expect(vfs.jSync(fileId, VFS.SQLITE_SYNC_NORMAL)).toBe(VFS.SQLITE_OK)
296
- expect(vfs.jSync(fileId, VFS.SQLITE_SYNC_FULL)).toBe(VFS.SQLITE_OK)
297
- expect(vfs.jSync(fileId, VFS.SQLITE_SYNC_DATAONLY)).toBe(VFS.SQLITE_OK)
298
-
299
- vfs.jClose(fileId)
300
- })
301
-
302
- it('should handle file deletion', async () => {
316
+ it('should handle file deletion', () => {
303
317
  const path = '/test/delete.db'
304
318
  const fileId = 1
305
319
  const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
@@ -307,83 +321,80 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
307
321
 
308
322
  // Create file
309
323
  vfs.jOpen(path, fileId, flags, pOutFlags)
310
- const testData = new TextEncoder().encode('Delete test')
324
+ const testData = makePage(0x55, 'Delete test')
311
325
  vfs.jWrite(fileId, testData, 0)
312
- vfs.jClose(fileId)
313
326
 
314
327
  // Delete file
315
328
  expect(vfs.jDelete(path, 0)).toBe(VFS.SQLITE_OK)
316
329
 
317
- // Verify file is gone
318
- expect(mockDatabase.has(`file:${path}`)).toBe(false)
319
- expect(mockDatabase.has(`blocks:${path}`)).toBe(false)
320
- })
321
- })
322
-
323
- describe('VFS Management', () => {
324
- it('should provide correct VFS characteristics', () => {
325
- expect(vfs.jSectorSize(1)).toBe(4096)
326
- expect(vfs.jDeviceCharacteristics(1)).toBe(VFS.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN)
330
+ // Verify pages are gone
331
+ expect(mockPages.size).toBe(0)
327
332
  })
328
333
 
329
- it('should handle multiple files', async () => {
330
- const files = [
331
- { path: '/test/file1.db', id: 1 },
332
- { path: '/test/file2.db', id: 2 },
333
- { path: '/test/file3.db', id: 3 },
334
- ]
335
-
334
+ it('should delete only the requested file path', () => {
336
335
  const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
337
336
  const pOutFlags = new DataView(new ArrayBuffer(4))
338
337
 
339
- // Open all files
340
- for (const file of files) {
341
- expect(vfs.jOpen(file.path, file.id, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
342
- }
338
+ const keepPath = '/test/keep.db'
339
+ const keepFileId = 1
340
+ vfs.jOpen(keepPath, keepFileId, flags, pOutFlags)
341
+ const keepData = makePage(0x66, 'keep-me')
342
+ expect(vfs.jWrite(keepFileId, keepData, 0)).toBe(VFS.SQLITE_OK)
343
343
 
344
- // Write different data to each
345
- for (let i = 0; i < files.length; i++) {
346
- const data = new TextEncoder().encode(`File ${i + 1} data`)
347
- expect(vfs.jWrite(files[i]?.id ?? 0, data, 0)).toBe(VFS.SQLITE_OK)
348
- }
344
+ const deletePath = '/test/delete-only.db'
345
+ const deleteFileId = 2
346
+ vfs.jOpen(deletePath, deleteFileId, flags, pOutFlags)
347
+ expect(vfs.jWrite(deleteFileId, makePage(0x77, 'delete-me'), 0)).toBe(VFS.SQLITE_OK)
349
348
 
350
- // Read back and verify
351
- for (let i = 0; i < files.length; i++) {
352
- const expected = new TextEncoder().encode(`File ${i + 1} data`)
353
- const actual = new Uint8Array(expected.length)
354
- expect(vfs.jRead(files[i]?.id ?? 0, actual, 0)).toBe(VFS.SQLITE_OK)
355
- expect(actual).toEqual(expected)
356
- }
349
+ expect(vfs.jDelete(deletePath, 0)).toBe(VFS.SQLITE_OK)
357
350
 
358
- // Close all files
359
- for (const file of files) {
360
- expect(vfs.jClose(file.id)).toBe(VFS.SQLITE_OK)
361
- }
351
+ const readBuffer = new Uint8Array(keepData.length)
352
+ expect(vfs.jRead(keepFileId, readBuffer, 0)).toBe(VFS.SQLITE_OK)
353
+ expect(readBuffer).toEqual(keepData)
354
+ expect(mockPages.has(deletePath)).toBe(false)
362
355
  })
356
+ })
363
357
 
358
+ describe('VFS Management', () => {
364
359
  it('should provide VFS statistics', () => {
365
360
  const stats = vfs.getStats()
366
- expect(stats).toHaveProperty('activeFiles')
367
- expect(stats).toHaveProperty('openFiles')
368
- expect(stats).toHaveProperty('maxFiles')
369
- expect(stats).toHaveProperty('blockSize')
361
+ expect(stats).toHaveProperty('pageSize')
362
+ expect(stats).toHaveProperty('totalPages')
370
363
  expect(stats).toHaveProperty('totalStoredBytes')
371
- expect(stats.blockSize).toBe(64 * 1024)
364
+ expect(stats.pageSize).toBe(CF_SQL_VFS_PAGE_SIZE)
372
365
  })
373
366
  })
374
367
 
375
368
  describe('Error Handling', () => {
376
- it('should handle invalid file IDs', () => {
369
+ it('should return SQLITE_IOERR for handle-based operations on unknown file IDs', () => {
377
370
  const invalidFileId = 999
378
- const buffer = new Uint8Array(100)
371
+ const buffer = new Uint8Array(CF_SQL_VFS_PAGE_SIZE)
372
+ const pSize64 = new DataView(new ArrayBuffer(8))
379
373
 
380
374
  expect(vfs.jRead(invalidFileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
381
375
  expect(vfs.jWrite(invalidFileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
382
- expect(vfs.jTruncate(invalidFileId, 50)).toBe(VFS.SQLITE_IOERR)
383
- expect(vfs.jSync(invalidFileId, 0)).toBe(VFS.SQLITE_IOERR)
376
+ expect(vfs.jTruncate(invalidFileId, 0)).toBe(VFS.SQLITE_IOERR)
377
+ expect(vfs.jFileSize(invalidFileId, pSize64)).toBe(VFS.SQLITE_IOERR)
384
378
  expect(vfs.jClose(invalidFileId)).toBe(VFS.SQLITE_OK)
385
379
  })
386
380
 
381
+ it('should return SQLITE_IOERR for handle-based operations after close', () => {
382
+ const path = '/test/closed.db'
383
+ const fileId = 1
384
+ const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
385
+ const pOutFlags = new DataView(new ArrayBuffer(4))
386
+ const buffer = new Uint8Array(CF_SQL_VFS_PAGE_SIZE)
387
+ const pSize64 = new DataView(new ArrayBuffer(8))
388
+
389
+ expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
390
+ expect(vfs.jClose(fileId)).toBe(VFS.SQLITE_OK)
391
+
392
+ expect(vfs.jRead(fileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
393
+ expect(vfs.jWrite(fileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
394
+ expect(vfs.jTruncate(fileId, 0)).toBe(VFS.SQLITE_IOERR)
395
+ expect(vfs.jFileSize(fileId, pSize64)).toBe(VFS.SQLITE_IOERR)
396
+ })
397
+
387
398
  it('should handle invalid paths', () => {
388
399
  const invalidPath = ''
389
400
  const fileId = 1
@@ -393,20 +404,40 @@ describe('CloudflareSqlVFS - Core Functionality', () => {
393
404
  expect(vfs.jOpen(invalidPath, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
394
405
  })
395
406
 
396
- it('should handle file operations on closed files', () => {
397
- const path = '/test/closed.db'
407
+ it('should reject writes that do not match the configured page size', () => {
408
+ const path = '/test/page-size-mismatch.db'
398
409
  const fileId = 1
399
410
  const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
400
411
  const pOutFlags = new DataView(new ArrayBuffer(4))
401
412
 
402
- // Open and close file
403
- vfs.jOpen(path, fileId, flags, pOutFlags)
404
- vfs.jClose(fileId)
413
+ expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
414
+ expect(vfs.jWrite(fileId, new Uint8Array(CF_SQL_VFS_PAGE_SIZE / 2), 0)).toBe(VFS.SQLITE_IOERR)
415
+ expect(mockPages.size).toBe(0)
416
+ })
405
417
 
406
- // Try to operate on closed file
407
- const buffer = new Uint8Array(10)
408
- expect(vfs.jRead(fileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
409
- expect(vfs.jWrite(fileId, buffer, 0)).toBe(VFS.SQLITE_IOERR)
418
+ it('should reject writes at non-page-aligned offsets', () => {
419
+ const path = '/test/page-offset-mismatch.db'
420
+ const fileId = 1
421
+ const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
422
+ const pOutFlags = new DataView(new ArrayBuffer(4))
423
+
424
+ expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
425
+ expect(vfs.jWrite(fileId, makePage(0x88), CF_SQL_VFS_PAGE_SIZE / 2)).toBe(VFS.SQLITE_IOERR)
426
+ expect(mockPages.size).toBe(0)
427
+ })
428
+
429
+ it('should always report not found from jAccess', () => {
430
+ const path = '/test/access.db'
431
+ const fileId = 1
432
+ const flags = VFS.SQLITE_OPEN_CREATE | VFS.SQLITE_OPEN_READWRITE
433
+ const pOutFlags = new DataView(new ArrayBuffer(4))
434
+ const pResOut = new DataView(new ArrayBuffer(4))
435
+
436
+ expect(vfs.jOpen(path, fileId, flags, pOutFlags)).toBe(VFS.SQLITE_OK)
437
+ expect(vfs.jWrite(fileId, makePage(0x99, 'access-test'), 0)).toBe(VFS.SQLITE_OK)
438
+
439
+ expect(vfs.jAccess(path, VFS.SQLITE_ACCESS_EXISTS, pResOut)).toBe(VFS.SQLITE_OK)
440
+ expect(pResOut.getUint32(0, true)).toBe(0)
410
441
  })
411
442
  })
412
443
 
@@ -5,7 +5,6 @@ let cachedMemoryVfs: MemoryVFS | undefined
5
5
 
6
6
  export const makeInMemoryDb = (sqlite3: WaSqlite.SQLiteAPI) => {
7
7
  if (sqlite3.vfs_registered.has('memory-vfs') === false) {
8
- // @ts-expect-error TODO fix types
9
8
  const vfs = new MemoryVFS('memory-vfs', (sqlite3 as any).module)
10
9
 
11
10
  // @ts-expect-error TODO fix types
@@ -6,7 +6,7 @@ export const loadSqlite3Wasm = async () => {
6
6
  // https://github.com/rhashimoto/wa-sqlite/issues/143#issuecomment-1899060056
7
7
  // module._free(module._malloc(10_000 * 4096 + 65_536))
8
8
  const sqlite3 = WaSqlite.Factory(module)
9
- // @ts-expect-error TODO fix types
9
+ // @ts-expect-error Adding module property for internal use (not in wa-sqlite types)
10
10
  sqlite3.module = module
11
11
  return sqlite3
12
12
  }
@@ -1,6 +1,5 @@
1
1
  import * as WaSqlite from '@livestore/wa-sqlite'
2
2
  import WaSqliteFactory from '@livestore/wa-sqlite/dist/wa-sqlite.mjs'
3
-
4
3
  // @ts-expect-error TODO fix types in wa-sqlite
5
4
  import wasm from '@livestore/wa-sqlite/dist/wa-sqlite.wasm'
6
5