UNPKG

sqlocal

Version:

SQLocal makes it easy to run SQLite3 in the browser, backed by the origin private file system.

130 lines (105 loc) 3.42 kB
import type { DriverConfig, Sqlite3InitModule, Sqlite3StorageType, SQLocalDriver, } from '../types.js'; import { normalizeDatabaseFile } from '../lib/normalize-database-file.js'; import { parseDatabasePath } from '../lib/parse-database-path.js'; import { SQLiteMemoryDriver } from './sqlite-memory-driver.js'; /** * A SQLocal driver that implements the interface needed for * interacting with SQLite databases in the origin private file system. */ export class SQLiteOpfsDriver extends SQLiteMemoryDriver implements SQLocalDriver { override readonly storageType: Sqlite3StorageType = 'opfs'; constructor(sqlite3InitModule?: Sqlite3InitModule) { super(sqlite3InitModule); } override async init(config: DriverConfig): Promise<void> { const { databasePath } = config; const flags = this.getFlags(config); if (!databasePath) { throw new Error('No databasePath specified'); } if (!this.sqlite3InitModule) { const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm'); this.sqlite3InitModule = sqlite3InitModule; } if (!this.sqlite3) { this.sqlite3 = await this.sqlite3InitModule(); } if (!('opfs' in this.sqlite3)) { throw new Error('OPFS not available'); } if (this.db) { await this.destroy(); } this.db = new this.sqlite3.oo1.OpfsDb(databasePath, flags); this.config = config; this.initWriteHook(); } override async isDatabasePersisted(): Promise<boolean> { return navigator.storage?.persisted(); } override async import( database: | ArrayBuffer | Uint8Array<ArrayBuffer> | ReadableStream<Uint8Array<ArrayBuffer>> ): Promise<void> { if (!this.sqlite3 || !this.config?.databasePath) { throw new Error('Driver not initialized'); } await this.destroy(); const data = await normalizeDatabaseFile(database, 'callback'); await this.sqlite3.oo1.OpfsDb.importDb(this.config.databasePath, data); } override async export(): Promise<{ name: string; data: ArrayBuffer | Uint8Array<ArrayBuffer>; }> { if (!this.db || !this.config?.databasePath) { throw new Error('Driver not initialized'); } let name, data; const path = parseDatabasePath(this.config.databasePath); const { directories, getDirectoryHandle } = path; name = path.fileName; const tempFileName = `backup-${Date.now()}--${name}`; const tempFilePath = `${directories.join('/')}/${tempFileName}`; this.db.exec({ sql: 'VACUUM INTO ?', bind: [tempFilePath] }); const dirHandle = await getDirectoryHandle(); const fileHandle = await dirHandle.getFileHandle(tempFileName); const file = await fileHandle.getFile(); data = await file.arrayBuffer(); await dirHandle.removeEntry(tempFileName); return { name, data }; } override async clear(): Promise<void> { if (!this.config?.databasePath) throw new Error('Driver not initialized'); await this.destroy(); const { getDirectoryHandle, fileName, tempFileNames } = parseDatabasePath( this.config.databasePath ); const dirHandle = await getDirectoryHandle(); const fileNames = [fileName, ...tempFileNames]; await Promise.all( fileNames.map(async (name) => { return dirHandle.removeEntry(name).catch((err) => { if (!(err instanceof DOMException && err.name === 'NotFoundError')) { throw err; } }); }) ); } override async destroy(): Promise<void> { this.closeDb(); this.writeCallbacks.clear(); } }