@livestore/sqlite-wasm 0.4.0-dev.3 → 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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser/mod.d.ts +1 -0
- package/dist/browser/mod.d.ts.map +1 -1
- package/dist/browser/mod.js.map +1 -1
- package/dist/cf/BlockManager.d.ts +61 -0
- package/dist/cf/BlockManager.d.ts.map +1 -0
- package/dist/cf/BlockManager.js +157 -0
- package/dist/cf/BlockManager.js.map +1 -0
- package/dist/cf/CloudflareSqlVFS.d.ts +51 -0
- package/dist/cf/CloudflareSqlVFS.d.ts.map +1 -0
- package/dist/cf/CloudflareSqlVFS.js +351 -0
- package/dist/cf/CloudflareSqlVFS.js.map +1 -0
- package/dist/cf/CloudflareWorkerVFS.d.ts +72 -0
- package/dist/cf/CloudflareWorkerVFS.d.ts.map +1 -0
- package/dist/cf/CloudflareWorkerVFS.js +552 -0
- package/dist/cf/CloudflareWorkerVFS.js.map +1 -0
- package/dist/cf/mod.d.ts +43 -0
- package/dist/cf/mod.d.ts.map +1 -0
- package/dist/cf/mod.js +74 -0
- package/dist/cf/mod.js.map +1 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.d.ts +2 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.d.ts.map +1 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js +314 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.js.map +1 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.d.ts +2 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.d.ts.map +1 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js +266 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-core.test.js.map +1 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.d.ts +2 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.d.ts.map +1 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js +444 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-integration.test.js.map +1 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.d.ts +2 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.d.ts.map +1 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js +334 -0
- package/dist/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.js.map +1 -0
- package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.d.ts +2 -0
- package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.d.ts.map +1 -0
- package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js +354 -0
- package/dist/cf/test/sql/cloudflare-sql-vfs-core.test.js.map +1 -0
- package/dist/load-wasm/mod.node.d.ts.map +1 -1
- package/dist/load-wasm/mod.node.js +1 -2
- package/dist/load-wasm/mod.node.js.map +1 -1
- package/dist/load-wasm/mod.workerd.d.ts +2 -0
- package/dist/load-wasm/mod.workerd.d.ts.map +1 -0
- package/dist/load-wasm/mod.workerd.js +26 -0
- package/dist/load-wasm/mod.workerd.js.map +1 -0
- package/dist/make-sqlite-db.d.ts +1 -0
- package/dist/make-sqlite-db.d.ts.map +1 -1
- package/dist/make-sqlite-db.js.map +1 -1
- package/dist/node/NodeFS.d.ts +1 -2
- package/dist/node/NodeFS.d.ts.map +1 -1
- package/dist/node/NodeFS.js +1 -6
- package/dist/node/NodeFS.js.map +1 -1
- package/dist/node/mod.js +3 -8
- package/dist/node/mod.js.map +1 -1
- package/package.json +20 -7
- package/src/browser/mod.ts +1 -0
- package/src/cf/BlockManager.ts +225 -0
- package/src/cf/CloudflareSqlVFS.ts +450 -0
- package/src/cf/CloudflareWorkerVFS.ts +664 -0
- package/src/cf/README.md +60 -0
- package/src/cf/mod.ts +143 -0
- package/src/cf/test/README.md +224 -0
- package/src/cf/test/async-storage/cloudflare-worker-vfs-advanced.test.ts +389 -0
- package/src/cf/test/async-storage/cloudflare-worker-vfs-core.test.ts +322 -0
- package/src/cf/test/async-storage/cloudflare-worker-vfs-integration.test.ts +567 -0
- package/src/cf/test/async-storage/cloudflare-worker-vfs-reliability.test.ts +403 -0
- package/src/cf/test/sql/cloudflare-sql-vfs-core.test.ts +433 -0
- package/src/load-wasm/mod.node.ts +1 -2
- package/src/load-wasm/mod.workerd.ts +26 -0
- package/src/make-sqlite-db.ts +1 -0
- package/src/node/NodeFS.ts +1 -9
- 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
|
+
}
|