UNPKG

@livestore/sqlite-wasm

Version:

138 lines (117 loc) 4.17 kB
import path from 'node:path' import { type MakeSqliteDb, type PersistenceInfo, type SqliteDb, UnexpectedError } from '@livestore/common' import { Effect, FileSystem } from '@livestore/utils/effect' import type * as WaSqlite from '@livestore/wa-sqlite' import type { MemoryVFS } from '@livestore/wa-sqlite/src/examples/MemoryVFS.js' import { makeInMemoryDb } from '../in-memory-vfs.js' import { makeSqliteDb } from '../make-sqlite-db.js' import { NodeFS } from './NodeFS.js' export type NodeDatabaseMetadataInMemory = { _tag: 'in-memory' vfs: MemoryVFS dbPointer: number persistenceInfo: PersistenceInfo deleteDb: () => void configureDb: (db: SqliteDb) => void } export type NodeDatabaseMetadataFs = { _tag: 'fs' vfs: NodeFS dbPointer: number persistenceInfo: PersistenceInfo<{ directory: string }> deleteDb: () => void configureDb: (db: SqliteDb) => void } export type NodeDatabaseMetadata = NodeDatabaseMetadataInMemory | NodeDatabaseMetadataFs export type NodeDatabaseInputInMemory = { _tag: 'in-memory' configureDb?: (db: SqliteDb) => void } export type NodeDatabaseInputFs = { _tag: 'fs' directory: string fileName: string configureDb?: (db: SqliteDb) => void } export type NodeDatabaseInput = NodeDatabaseInputInMemory | NodeDatabaseInputFs export type MakeNodeSqliteDb = MakeSqliteDb< { dbPointer: number; persistenceInfo: PersistenceInfo }, NodeDatabaseInput, NodeDatabaseMetadata > export const sqliteDbFactory = ({ sqlite3, }: { sqlite3: SQLiteAPI }): Effect.Effect<MakeNodeSqliteDb, never, FileSystem.FileSystem> => Effect.andThen( FileSystem.FileSystem, (fs) => (input) => Effect.gen(function* () { if (input._tag === 'in-memory') { const { dbPointer, vfs } = makeInMemoryDb(sqlite3) return makeSqliteDb<NodeDatabaseMetadataInMemory>({ sqlite3, metadata: { _tag: 'in-memory', vfs, dbPointer, persistenceInfo: { fileName: ':memory:' }, deleteDb: () => {}, configureDb: input.configureDb ?? (() => {}), }, }) as any } const { dbPointer, vfs } = yield* makeNodeFsDb({ sqlite3, fileName: input.fileName, directory: input.directory, fs, }) const filePath = path.join(input.directory, input.fileName) return makeSqliteDb<NodeDatabaseMetadataFs>({ sqlite3, metadata: { _tag: 'fs', vfs, dbPointer, persistenceInfo: { fileName: input.fileName, directory: input.directory }, deleteDb: () => vfs.deleteDb(filePath), configureDb: input.configureDb ?? (() => {}), }, }) }), ) const nodeFsVfsMap = new Map<string, NodeFS>() const makeNodeFsDb = ({ sqlite3, fileName, directory, fs, }: { sqlite3: WaSqlite.SQLiteAPI fileName: string directory: string fs: FileSystem.FileSystem }) => Effect.gen(function* () { // NOTE to keep the filePath short, we use the directory name in the vfs name // If this is becoming a problem, we can use a hashed version of the directory name const vfsName = `node-fs-${directory}` if (nodeFsVfsMap.has(vfsName) === false) { // TODO refactor with Effect FileSystem instead of using `node:fs` directly inside of NodeFS const nodeFsVfs = new NodeFS(vfsName, (sqlite3 as any).module, directory) // @ts-expect-error TODO fix types sqlite3.vfs_register(nodeFsVfs, false) nodeFsVfsMap.set(vfsName, nodeFsVfs) } yield* fs.makeDirectory(directory, { recursive: true }) const FILE_NAME_MAX_LENGTH = 56 if (fileName.length > FILE_NAME_MAX_LENGTH) { throw new Error(`File name ${fileName} is too long. Maximum length is ${FILE_NAME_MAX_LENGTH} characters.`) } // NOTE SQLite will return a "disk I/O error" if the file path is too long. const dbPointer = sqlite3.open_v2Sync(fileName, undefined, vfsName) const vfs = nodeFsVfsMap.get(vfsName)! return { dbPointer, vfs } }).pipe(UnexpectedError.mapToUnexpectedError)