@livestore/sqlite-wasm 0.4.0-dev.9 → 0.5.0-dev.0

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