@livestore/sqlite-wasm
Version:
191 lines (162 loc) • 5.64 kB
text/typescript
/// <reference types="node" />
/* eslint-disable prefer-arrow/prefer-arrow-functions */
import * as fs from 'node:fs'
import path from 'node:path'
import type * as WaSqlite from '@livestore/wa-sqlite'
import * as VFS from '@livestore/wa-sqlite/src/VFS.js'
import { FacadeVFS } from '../FacadeVFS.js'
interface NodeFsFile {
pathname: string
flags: number
fileHandle: number | null
}
export class NodeFS extends FacadeVFS {
private mapIdToFile = new Map<number, NodeFsFile>()
private lastError: Error | null = null
private readonly directory: string
constructor(name: string, sqlite3: WaSqlite.SQLiteAPI, directory: string) {
super(name, sqlite3)
this.directory = directory
}
getFilename(fileId: number): string {
const pathname = this.mapIdToFile.get(fileId)?.pathname
return `NodeFS:${pathname}`
}
jOpen(zName: string | null, fileId: number, flags: number, pOutFlags: DataView): number {
try {
const pathname = zName ? path.resolve(this.directory, zName) : Math.random().toString(36).slice(2)
const file: NodeFsFile = { pathname, flags, fileHandle: null }
this.mapIdToFile.set(fileId, file)
const create = !!(flags & VFS.SQLITE_OPEN_CREATE)
const readwrite = !!(flags & VFS.SQLITE_OPEN_READWRITE)
// Convert SQLite flags to Node.js flags
let fsFlags = 'r'
if (create && readwrite) {
// Check if file exists first
const exists = fs.existsSync(pathname)
fsFlags = exists ? 'r+' : 'w+' // Use r+ for existing files, w+ only for new files
} else if (readwrite) {
fsFlags = 'r+' // Open file for reading and writing
}
try {
file.fileHandle = fs.openSync(pathname, fsFlags)
pOutFlags.setInt32(0, flags, true)
return VFS.SQLITE_OK
} catch (err: any) {
if (err.code === 'ENOENT' && !create) {
return VFS.SQLITE_CANTOPEN
}
throw err
}
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_CANTOPEN
}
}
jRead(fileId: number, pData: Uint8Array, iOffset: number): number {
try {
const file = this.mapIdToFile.get(fileId)
if (!file?.fileHandle) return VFS.SQLITE_IOERR_READ
// const view = new DataView(pData.buffer, pData.byteOffset, pData.length)
// const bytesRead = fs.readSync(file.fileHandle, view, 0, pData.length, iOffset)
const bytesRead = fs.readSync(file.fileHandle, pData.subarray(), { position: iOffset })
if (bytesRead < pData.length) {
pData.fill(0, bytesRead)
return VFS.SQLITE_IOERR_SHORT_READ
}
return VFS.SQLITE_OK
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_IOERR_READ
}
}
jWrite(fileId: number, pData: Uint8Array, iOffset: number): number {
try {
const file = this.mapIdToFile.get(fileId)
if (!file?.fileHandle) return VFS.SQLITE_IOERR_WRITE
// const view = new DataView(pData.buffer, pData.byteOffset, pData.length)
// fs.writeSync(file.fileHandle, view, 0, pData.length, iOffset)
fs.writeSync(file.fileHandle, Buffer.from(pData.subarray()), 0, pData.length, iOffset)
return VFS.SQLITE_OK
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_IOERR_WRITE
}
}
jClose(fileId: number): number {
try {
const file = this.mapIdToFile.get(fileId)
if (!file) return VFS.SQLITE_OK
this.mapIdToFile.delete(fileId)
if (file.fileHandle !== null) {
fs.closeSync(file.fileHandle)
}
if (file.flags & VFS.SQLITE_OPEN_DELETEONCLOSE) {
fs.unlinkSync(file.pathname)
}
return VFS.SQLITE_OK
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_IOERR_CLOSE
}
}
jFileSize(fileId: number, pSize64: DataView): number {
try {
const file = this.mapIdToFile.get(fileId)
if (!file?.fileHandle) return VFS.SQLITE_IOERR_FSTAT
const stats = fs.fstatSync(file.fileHandle)
pSize64.setBigInt64(0, BigInt(stats.size), true)
return VFS.SQLITE_OK
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_IOERR_FSTAT
}
}
jTruncate(fileId: number, iSize: number): number {
try {
const file = this.mapIdToFile.get(fileId)
if (!file?.fileHandle) return VFS.SQLITE_IOERR_TRUNCATE
fs.ftruncateSync(file.fileHandle, iSize)
return VFS.SQLITE_OK
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_IOERR_TRUNCATE
}
}
jSync(fileId: number, _flags: number): number {
try {
const file = this.mapIdToFile.get(fileId)
if (!file?.fileHandle) return VFS.SQLITE_OK
// TODO do this out of band (for now we disable it to speed up the node vfs)
// fs.fsyncSync(file.fileHandle)
return VFS.SQLITE_OK
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_IOERR_FSYNC
}
}
jDelete(zName: string, _syncDir: number): number {
try {
const pathname = path.resolve(this.directory, zName)
fs.unlinkSync(pathname)
return VFS.SQLITE_OK
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_IOERR_DELETE
}
}
jAccess(zName: string, _flags: number, pResOut: DataView): number {
try {
const pathname = path.resolve(this.directory, zName)
const exists = fs.existsSync(pathname)
pResOut.setInt32(0, exists ? 1 : 0, true)
return VFS.SQLITE_OK
} catch (e: any) {
this.lastError = e
return VFS.SQLITE_IOERR_ACCESS
}
}
deleteDb(fileName: string) {
fs.unlinkSync(path.join(this.directory, fileName))
}
}