UNPKG

expo-sqlite

Version:

Provides access to a database using SQLite (https://www.sqlite.org/). The database is persisted across restarts of your app.

812 lines (713 loc) 25.3 kB
// Copyright 2015-present 650 Industries. All rights reserved. /// <reference types="./wa-sqlite/types" /> import { createSQLAction } from './SQLAction'; import { SQLiteOptions } from './SQLiteOptions'; import { sendWorkerResult } from './WorkerChannel'; import { type Changeset } from '../src/NativeSession'; import { type SQLiteColumnNames, type SQLiteColumnValues } from '../src/NativeStatement'; import { AccessHandlePoolVFS } from './wa-sqlite/AccessHandlePoolVFS'; import { MemoryVFS } from './wa-sqlite/MemoryVFS'; import * as SQLite from './wa-sqlite/sqlite-api'; import { SQLITE_ROW, SQLITE_DONE, SQLITE_OK, SQLITE_OPEN_READWRITE, SQLITE_OPEN_CREATE, } from './wa-sqlite/sqlite-constants'; import WaSQLiteFactory from './wa-sqlite/wa-sqlite'; // @ts-expect-error wasm module is not typed import wasmModule from './wa-sqlite/wa-sqlite.wasm'; import { type SQLiteWorkerMessage, type SQLiteWorkerMessageType, type MessageTypeMap, type ResultType, type ResultTypeMap, type OnDatabaseChangeMessage, } from './web.types'; type DatabasePointer = number; type StatementPointer = number; type SessionPointer = number; interface DatabaseEntity { pointer: DatabasePointer; databasePath: string; openOptions: SQLiteOptions; } interface StatementEntity { pointer: StatementPointer; } interface SessionEntity { pointer: SessionPointer; } const VFS_NAME_PERSISTENT = 'expo-sqlite'; const VFS_NAME_MEMORY = 'expo-sqlite-memfs'; const MAX_INT32 = 0x7fffffff; const MIN_INT32 = -0x80000000; let _sqlite3: SQLiteAPI | null = null; let _vfs: AccessHandlePoolVFS | null = null; let _vfsMemory: MemoryVFS | null = null; const databaseIdMap = new Map<number, DatabaseEntity>(); const statementIdMap = new Map<number, StatementEntity>(); const sessionIdMap = new Map<number, SessionEntity>(); class SQLiteErrorException extends Error {} self.onmessage = async (event: MessageEvent<SQLiteWorkerMessage>) => { let result: ResultType | null = null; let error: Error | null = null; try { const message = event.data as MessageTypeMap[typeof event.data.type]; result = await handleMessageImpl(message); } catch (e) { error = e instanceof Error ? e : new Error(String(e)); } const syncTrait = event.data.isSync ? { lockBuffer: event.data.lockBuffer, resultBuffer: event.data.resultBuffer, } : undefined; sendWorkerResult({ id: event.data.id, result, error, syncTrait, }); }; async function handleMessageImpl<T extends SQLiteWorkerMessageType>({ type, data, }: MessageTypeMap[T]): Promise<ResultType> { let result: ResultType | undefined; switch (type) { case 'backupDatabase': { await backupDatabase( data.destNativeDatabaseId, data.destDatabaseName, data.sourceNativeDatabaseId, data.sourceDatabaseName ); break; } case 'close': { await closeDatabase(data.nativeDatabaseId); break; } case 'deleteDatabase': { await deleteDatabase(data.databasePath); break; } case 'exec': { await exec(data.nativeDatabaseId, data.source); break; } case 'finalize': { await finalize(data.nativeDatabaseId, data.nativeStatementId); break; } case 'getAll': { result = await getAllRows(data.nativeDatabaseId, data.nativeStatementId); break; } case 'getColumnNames': { result = await getColumnNames(data.nativeStatementId); break; } case 'importAssetDatabase': { await importAssetDatabase(data.databasePath, data.assetDatabasePath, data.forceOverwrite); break; } case 'isInTransaction': { result = await isInTransaction(data.nativeDatabaseId); break; } case 'open': { await openDatabase( data.nativeDatabaseId, data.databasePath, new SQLiteOptions(data.options), data.serializedData ); break; } case 'prepare': { result = await prepare(data.nativeDatabaseId, data.nativeStatementId, data.source); break; } case 'reset': { await reset(data.nativeDatabaseId, data.nativeStatementId); break; } case 'run': { result = await run( data.nativeDatabaseId, data.nativeStatementId, data.bindParams, data.bindBlobParams, data.shouldPassAsArray ); break; } case 'serialize': { result = await serializeDatabase(data.nativeDatabaseId, data.schemaName); break; } case 'step': { result = await step(data.nativeDatabaseId, data.nativeStatementId); break; } case 'sessionCreate': { await sessionCreate(data.nativeDatabaseId, data.nativeSessionId, data.dbName); break; } case 'sessionAttach': { await sessionAttach(data.nativeDatabaseId, data.nativeSessionId, data.table); break; } case 'sessionEnable': { await sessionEnable(data.nativeDatabaseId, data.nativeSessionId, data.enabled); break; } case 'sessionClose': { await sessionClose(data.nativeDatabaseId, data.nativeSessionId); break; } case 'sessionCreateChangeset': { result = await sessionCreateChangeset(data.nativeDatabaseId, data.nativeSessionId); break; } case 'sessionCreateInvertedChangeset': { result = await sessionCreateInvertedChangeset(data.nativeDatabaseId, data.nativeSessionId); break; } case 'sessionApplyChangeset': { await sessionApplyChangeset(data.nativeDatabaseId, data.nativeSessionId, data.changeset); break; } case 'sessionInvertChangeset': { result = await sessionInvertChangeset( data.nativeDatabaseId, data.nativeSessionId, data.changeset ); break; } default: { throw new Error(`Unknown message type: ${type}`); } } return result; } //#region Request handlers async function backupDatabase( destNativeDatabaseId: number, destDatabaseName: string, sourceNativeDatabaseId: number, sourceDatabaseName: string ): Promise<void> { const { sqlite3 } = await maybeInitAsync(); const destDb = databaseIdMap.get(destNativeDatabaseId); if (!destDb) throw new Error(`Database not found - nativeDatabaseId[${destNativeDatabaseId}]`); const sourceDb = databaseIdMap.get(sourceNativeDatabaseId); if (!sourceDb) throw new Error(`Database not found - nativeDatabaseId[${sourceNativeDatabaseId}]`); await sqlite3.backup(destDb.pointer, destDatabaseName, sourceDb.pointer, sourceDatabaseName); } async function closeDatabase(nativeDatabaseId: number) { maybeFinalizeAllStatements(nativeDatabaseId); const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (dbEntity) { databaseIdMap.delete(nativeDatabaseId); await sqlite3.close(dbEntity.pointer); } } async function deleteDatabase(databasePath: string): Promise<void> { const { vfs } = await maybeInitAsync(); if (databasePath !== ':memory:') { vfs.jDelete(databasePath, 0 /* unused arg for AccessHandlePoolVFS */); } } async function deserializeDatabase( sqlite3: SQLiteAPI, serializedData: Uint8Array ): Promise<DatabasePointer> { const pointer = await sqlite3.open_v2( ':memory:', SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, VFS_NAME_MEMORY ); await sqlite3.deserialize(pointer, 'main', serializedData); return pointer; } async function exec(nativeDatabaseId: number, source: string) { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); await sqlite3.exec(dbEntity.pointer, source); } async function finalize(nativeDatabaseId: number, nativeStatementId: number): Promise<void> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const stmt = statementIdMap.get(nativeStatementId); if (!stmt) throw new Error(`Statement not found - nativeStatementId[${nativeStatementId}]`); statementIdMap.delete(nativeStatementId); if ((await sqlite3.finalize(stmt.pointer)) !== SQLITE_OK) { throw new Error('Error finalizing statement'); } } async function getAllRows( nativeDatabaseId: number, nativeStatementId: number ): Promise<SQLiteColumnValues[]> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const stmt = statementIdMap.get(nativeStatementId); if (!stmt) throw new Error(`Statement not found - nativeStatementId[${nativeStatementId}]`); const rows: SQLiteColumnValues[] = []; while (true) { const ret = await sqlite3.step(stmt.pointer); if (ret === SQLITE_ROW) { rows.push(getColumnValues(sqlite3, stmt.pointer)); continue; } else if (ret === SQLITE_DONE) { break; } throw new Error('Error executing statement'); } return rows; } async function getColumnNames(nativeStatementId: number): Promise<SQLiteColumnNames> { const { sqlite3 } = await maybeInitAsync(); const stmt = statementIdMap.get(nativeStatementId); if (!stmt) throw new Error(`Statement not found - nativeStatementId[${nativeStatementId}]`); const columnCount = sqlite3.column_count(stmt.pointer); const columnNames: SQLiteColumnNames = []; for (let i = 0; i < columnCount; i++) { columnNames.push(sqlite3.column_name(stmt.pointer, i)); } return columnNames; } async function importAssetDatabase( databasePath: string, assetDatabasePath: string, forceOverwrite: boolean ): Promise<void> { const { sqlite3, vfs } = await maybeInitAsync(); if (!forceOverwrite) { const buffer = new DataView(new ArrayBuffer(4)); await vfs.jAccess(databasePath, 0 /* unused arg for AccessHandlePoolVFS */, buffer); if (buffer.getUint8(0) === 1) { return; } } const response = await fetch(assetDatabasePath); if (!response.ok) { throw new Error( `[importAssetDatabaseAsync] Failed to fetch asset database: ${response.statusText}` ); } const serializedData = new Uint8Array(await response.arrayBuffer()); const srcDb = await sqlite3.open_v2( databasePath, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, VFS_NAME_PERSISTENT ); await sqlite3.deserialize(srcDb, 'main', serializedData); const destDb = await sqlite3.open_v2(databasePath); await sqlite3.backup(destDb, 'main', srcDb, 'main'); await sqlite3.close(srcDb); await sqlite3.close(destDb); } async function isInTransaction(nativeDatabaseId: number): Promise<boolean> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); return sqlite3.get_autocommit(dbEntity.pointer) === 0; } async function openDatabase( nativeDatabaseId: number, databasePath: string, options: SQLiteOptions, serializedData?: Uint8Array ) { const { sqlite3 } = await maybeInitAsync(); let pointer: DatabasePointer; if (serializedData) { pointer = await deserializeDatabase(sqlite3, serializedData); } else { const dbEntity = findCachedDatabase( (entity) => entity.databasePath === databasePath && entity.openOptions.equals(options) && !options.useNewConnection ); if (dbEntity) { databaseIdMap.set(nativeDatabaseId, dbEntity); await initDb(sqlite3, dbEntity); return; } const flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; const vfsName = databasePath === ':memory:' ? VFS_NAME_MEMORY : VFS_NAME_PERSISTENT; pointer = await sqlite3.open_v2(databasePath, flags, vfsName); } const dbEntity = { pointer, databasePath, openOptions: options, }; databaseIdMap.set(nativeDatabaseId, dbEntity); await initDb(sqlite3, dbEntity); } async function prepare( nativeDatabaseId: number, nativeStatementId: number, source: string ): Promise<ResultTypeMap['prepare']> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const asyncIterable = sqlite3.statements(dbEntity.pointer, source, { unscoped: true }); const asyncIterator = asyncIterable[Symbol.asyncIterator](); const { value: statementPointer } = await asyncIterator.next(); asyncIterator.return?.(); if (!statementPointer) throw new Error('Failed to prepare statement'); statementIdMap.set(nativeStatementId, { pointer: statementPointer }); } async function run( nativeDatabaseId: number, nativeStatementId: number, bindParams: any, bindBlobParams: any, shouldPassAsArray: boolean ) { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const stmt = statementIdMap.get(nativeStatementId); if (!stmt) throw new Error(`Statement not found - nativeStatementId[${nativeStatementId}]`); sqlite3.reset(stmt.pointer); sqlite3.clear_bindings(stmt.pointer); for (const [key, param] of Object.entries(bindParams)) { const index = getBindParamIndex(sqlite3, stmt.pointer, key, shouldPassAsArray); if (index > 0) { bindStatementParam(sqlite3, stmt.pointer, param, index); } } for (const [key, param] of Object.entries(bindBlobParams)) { const index = getBindParamIndex(sqlite3, stmt.pointer, key, shouldPassAsArray); if (index > 0) { bindStatementParam(sqlite3, stmt.pointer, param, index); } } const ret = await sqlite3.step(stmt.pointer); if (ret !== SQLITE_ROW && ret !== SQLITE_DONE) { throw new SQLiteErrorException('Error executing statement'); } const firstRowValues = ret === SQLITE_ROW ? getColumnValues(sqlite3, stmt.pointer) : []; return { lastInsertRowId: Number(sqlite3.last_insert_rowid(dbEntity.pointer)), changes: sqlite3.changes(dbEntity.pointer), firstRowValues, }; } async function reset(nativeDatabaseId: number, nativeStatementId: number): Promise<void> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const stmt = statementIdMap.get(nativeStatementId); if (!stmt) throw new Error(`Statement not found - nativeStatementId[${nativeStatementId}]`); if ((await sqlite3.reset(stmt.pointer)) !== SQLITE_OK) { throw new Error('Error resetting statement'); } } async function serializeDatabase( nativeDatabaseId: number, schemaName: string ): Promise<Uint8Array | null> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); return sqlite3.serialize(dbEntity.pointer, schemaName); } async function step( nativeDatabaseId: number, nativeStatementId: number ): Promise<SQLiteColumnValues | null> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const stmt = statementIdMap.get(nativeStatementId); if (!stmt) throw new Error(`Statement not found - nativeStatementId[${nativeStatementId}]`); const ret = await sqlite3.step(stmt.pointer); if (ret === SQLITE_ROW) { return getColumnValues(sqlite3, stmt.pointer); } if (ret !== SQLITE_DONE) { throw new Error('Error executing statement'); } return null; } async function sessionCreate( nativeDatabaseId: number, nativeSessionId: number, dbName: string ): Promise<void> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const session = sqlite3.session_create(dbEntity.pointer, dbName); sessionIdMap.set(nativeSessionId, { pointer: session }); } async function sessionAttach( nativeDatabaseId: number, nativeSessionId: number, table: string | null ): Promise<void> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const session = sessionIdMap.get(nativeSessionId); if (!session) throw new Error(`Session not found - nativeSessionId[${nativeSessionId}]`); sqlite3.session_attach(session.pointer, table); } async function sessionEnable( nativeDatabaseId: number, nativeSessionId: number, enabled: boolean ): Promise<void> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const session = sessionIdMap.get(nativeSessionId); if (!session) throw new Error(`Session not found - nativeSessionId[${nativeSessionId}]`); sqlite3.session_enable(session.pointer, enabled); } async function sessionClose(nativeDatabaseId: number, nativeSessionId: number): Promise<void> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const session = sessionIdMap.get(nativeSessionId); if (!session) throw new Error(`Session not found - nativeSessionId[${nativeSessionId}]`); sessionIdMap.delete(nativeSessionId); sqlite3.session_delete(session.pointer); } async function sessionCreateChangeset( nativeDatabaseId: number, nativeSessionId: number ): Promise<Changeset> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const session = sessionIdMap.get(nativeSessionId); if (!session) throw new Error(`Session not found - nativeSessionId[${nativeSessionId}]`); return sqlite3.session_changeset(session.pointer); } async function sessionCreateInvertedChangeset( nativeDatabaseId: number, nativeSessionId: number ): Promise<Changeset> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); const session = sessionIdMap.get(nativeSessionId); if (!session) throw new Error(`Session not found - nativeSessionId[${nativeSessionId}]`); return sqlite3.session_changeset_inverted(session.pointer); } async function sessionApplyChangeset( nativeDatabaseId: number, nativeSessionId: number, changeset: Changeset ): Promise<void> { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); sqlite3.changeset_apply(dbEntity.pointer, changeset); } async function sessionInvertChangeset( nativeDatabaseId: number, nativeSessionId: number, changeset: Changeset ): Promise<Changeset> { const { sqlite3 } = await maybeInitAsync(); return sqlite3.changeset_invert(changeset); } //#endregion Request handlers //#region Internal helpers function addUpdateHook(sqlite3: SQLiteAPI, dbEntity: DatabaseEntity) { sqlite3.update_hook( dbEntity.pointer, (updateType: number, dbName: string | null, tblName: string | null, rowId: bigint) => { const message: OnDatabaseChangeMessage = { type: 'onDatabaseChange', data: { databaseName: dbName, databaseFilePath: sqlite3.db_filename(dbEntity.pointer, dbName ?? 'main'), tableName: tblName, rowId: Number.isSafeInteger(rowId) ? rowId : Number(rowId), typeId: createSQLAction(updateType), }, }; self.postMessage(message); } ); } function bindStatementParam(sqlite3: SQLiteAPI, stmt: StatementPointer, param: any, index: number) { if (param == null) { sqlite3.bind_null(stmt, index); } else if (typeof param === 'number') { if (Number.isInteger(param)) { if (param > MAX_INT32 || param < MIN_INT32) { sqlite3.bind_int64(stmt, index, BigInt(param)); } else { sqlite3.bind_int(stmt, index, param); } } else { sqlite3.bind_double(stmt, index, param); } } else if (typeof param === 'string') { sqlite3.bind_text(stmt, index, param); } else if (param instanceof Uint8Array) { sqlite3.bind_blob(stmt, index, param); } else if (typeof param === 'boolean') { sqlite3.bind_int(stmt, index, param ? 1 : 0); } else { throw new Error(`Unsupported parameter type: ${typeof param}`); } } function findCachedDatabase(predicate: (entity: DatabaseEntity) => boolean): DatabaseEntity | null { for (const entity of databaseIdMap.values()) { if (predicate(entity)) { return entity; } } return null; } function getBindParamIndex( sqlite3: SQLiteAPI, stmt: StatementPointer, key: string, shouldPassAsArray: boolean ): number { let index: number; if (shouldPassAsArray) { const intKey = parseInt(key, 10); if (isNaN(intKey)) { throw new Error('Invalid bind parameter'); } index = intKey + 1; } else { index = sqlite3.bind_parameter_index(stmt, key); } return index; } function getColumnValue(sqlite3: SQLiteAPI, stmt: StatementPointer, index: number): any { const type = sqlite3.column_type(stmt, index); let value: any; switch (type) { case SQLite.SQLITE_INTEGER: { value = sqlite3.column_int_safe(stmt, index); break; } case SQLite.SQLITE_FLOAT: { value = sqlite3.column_double(stmt, index); break; } case SQLite.SQLITE_TEXT: { value = sqlite3.column_text(stmt, index); break; } case SQLite.SQLITE_BLOB: { value = sqlite3.column_blob(stmt, index); break; } case SQLite.SQLITE_NULL: { value = null; break; } default: { throw new Error(`Unsupported column type: ${type}`); } } return value; } function getColumnValues(sqlite3: SQLiteAPI, stmt: StatementPointer): SQLiteColumnValues { const columnCount = sqlite3.column_count(stmt); const columnValues: SQLiteColumnValues = []; for (let i = 0; i < columnCount; i++) { columnValues[i] = getColumnValue(sqlite3, stmt, i); } return columnValues; } async function initDb(sqlite3: SQLiteAPI, dbEntity: DatabaseEntity) { if (dbEntity.openOptions.enableChangeListener) { addUpdateHook(sqlite3, dbEntity); } } async function maybeFinalizeAllStatements(nativeDatabaseId: number) { const { sqlite3 } = await maybeInitAsync(); const dbEntity = databaseIdMap.get(nativeDatabaseId); if (!dbEntity) throw new Error(`Database not found - nativeDatabaseId[${nativeDatabaseId}]`); if (!dbEntity.openOptions.finalizeUnusedStatementsBeforeClosing) { return; } let error: Error | null = null; const finalizedStatements: StatementPointer[] = []; let stmt: StatementPointer | null = sqlite3.next_stmt(dbEntity.pointer, null); while (stmt != null && stmt !== 0) { const nextStmt: StatementPointer = sqlite3.next_stmt(dbEntity.pointer, stmt); try { sqlite3.finalize(stmt); finalizedStatements.push(stmt); } catch (e) { error = e; } stmt = nextStmt; } // Delete finalized statements from the map const statementsToDelete: number[] = []; for (const [nativeStatementId, stmtEntity] of statementIdMap.entries()) { if (finalizedStatements.includes(stmtEntity.pointer)) { statementsToDelete.push(nativeStatementId); } } for (const nativeStatementId of statementsToDelete) { statementIdMap.delete(nativeStatementId); } if (error) throw error; } async function maybeInitAsync(): Promise<{ sqlite3: SQLiteAPI; vfs: AccessHandlePoolVFS; vfsMemory: MemoryVFS; }> { if (!_sqlite3) { const module = await WaSQLiteFactory({ locateFile: () => wasmModule, }); _sqlite3 = SQLite.Factory(module) as SQLiteAPI; if (!_sqlite3) { throw new Error('Failed to initialize wa-sqlite'); } if (_vfs == null) { _vfs = await AccessHandlePoolVFS.create(VFS_NAME_PERSISTENT, module); if (_vfs == null) { throw new Error('Failed to initialize AccessHandlePoolVFS'); } } _sqlite3.vfs_register(_vfs, true); if (_vfsMemory == null) { _vfsMemory = await MemoryVFS.create(VFS_NAME_MEMORY, module); if (_vfsMemory == null) { throw new Error('Failed to initialize MemoryVFS'); } } _sqlite3.vfs_register(_vfsMemory, false); } if (_vfs == null || _vfsMemory == null) { throw new Error('Invalid VFS state'); } return { sqlite3: _sqlite3, vfs: _vfs, vfsMemory: _vfsMemory }; } //#endregion Internal helpers