UNPKG

sqlocal

Version:

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

756 lines 30.7 kB
var _a, _b; import coincident from 'coincident'; import { SQLocalProcessor } from './processor.js'; import { sqlTag } from './lib/sql-tag.js'; import { convertRowsToObjects } from './lib/convert-rows-to-objects.js'; import { normalizeStatement } from './lib/normalize-statement.js'; import { getQueryKey } from './lib/get-query-key.js'; import { mutationLock } from './lib/mutation-lock.js'; import { normalizeDatabaseFile } from './lib/normalize-database-file.js'; import { SQLiteMemoryDriver } from './drivers/sqlite-memory-driver.js'; import { SQLiteKvvfsDriver } from './drivers/sqlite-kvvfs-driver.js'; import { getDatabaseKey } from './lib/get-database-key.js'; /** * This class is your entry point for connecting to and * interacting with an on-device SQLite database in the browser. * @see {@link https://sqlocal.dev/guide/setup} */ export class SQLocal { constructor(config) { Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "clientKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "processor", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "isDestroyed", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "bypassMutationLock", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "transactionQueryKeyQueue", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "userCallbacks", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "queriesInProgress", { enumerable: true, configurable: true, writable: true, value: new Map() }); Object.defineProperty(this, "proxy", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "reinitChannel", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "effectsChannel", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "processMessageEvent", { enumerable: true, configurable: true, writable: true, value: (event) => { const message = event instanceof MessageEvent ? event.data : event; const queries = this.queriesInProgress; switch (message.type) { case 'success': case 'data': case 'buffer': case 'info': case 'error': if (message.queryKey && queries.has(message.queryKey)) { const [resolve, reject] = queries.get(message.queryKey); if (message.type === 'error') { reject(message.error); } else { resolve(message); } queries.delete(message.queryKey); } else if (message.type === 'error') { throw message.error; } break; case 'callback': const userCallback = this.userCallbacks.get(message.name); if (userCallback) { userCallback(...(message.args ?? [])); } break; case 'event': this.config.onConnect?.(message.reason); break; } } }); Object.defineProperty(this, "createQuery", { enumerable: true, configurable: true, writable: true, value: async (message) => { return mutationLock({ mode: 'shared', key: getDatabaseKey(this.config.databasePath, this.clientKey), bypass: this.bypassMutationLock || message.type === 'import' || message.type === 'delete', }, async () => { if (this.isDestroyed === true) { throw new Error('This SQLocal client has been destroyed. You will need to initialize a new client in order to make further queries.'); } const queryKey = getQueryKey(); switch (message.type) { case 'import': this.processor.postMessage({ ...message, queryKey, }, [message.database]); break; default: this.processor.postMessage({ ...message, queryKey, }); break; } return new Promise((resolve, reject) => { this.queriesInProgress.set(queryKey, [resolve, reject]); }); }); } }); Object.defineProperty(this, "broadcast", { enumerable: true, configurable: true, writable: true, value: (message) => { this.reinitChannel.postMessage(message); } }); /** @internal */ Object.defineProperty(this, "exec", { enumerable: true, configurable: true, writable: true, value: async (sql, params, method = 'all', transactionKey) => { const message = await this.createQuery({ type: 'query', transactionKey, sql, params, method, }); const data = { rows: [], columns: [], }; if (message.type === 'data') { const results = message.data[0]; data.rows = results?.rows ?? []; data.columns = results?.columns ?? []; data.numAffectedRows = results?.numAffectedRows; } return data; } }); Object.defineProperty(this, "execBatch", { enumerable: true, configurable: true, writable: true, value: async (statements, transactionKey) => { const message = await this.createQuery({ type: 'batch', transactionKey, statements, }); const data = new Array(statements.length).fill({ rows: [], columns: [], }); if (message.type === 'data') { message.data.forEach((result, resultIndex) => { data[resultIndex] = result; }); } return data; } }); /** * Execute SQL queries against the database. * @see {@link https://sqlocal.dev/api/sql} */ Object.defineProperty(this, "sql", { enumerable: true, configurable: true, writable: true, value: async (queryTemplate, ...params) => { const statement = sqlTag(queryTemplate, params); const { rows, columns } = await this.exec(statement.sql, statement.params, 'all'); return convertRowsToObjects(rows, columns); } }); /** * Execute a batch of SQL queries against the database in an atomic way. * @see {@link https://sqlocal.dev/api/batch} */ Object.defineProperty(this, "batch", { enumerable: true, configurable: true, writable: true, value: async (passStatements) => { const statements = passStatements(sqlTag); const data = await this.execBatch(statements); return data.map(({ rows, columns }) => { return convertRowsToObjects(rows, columns); }); } }); /** @internal */ Object.defineProperty(this, "beginTransaction", { enumerable: true, configurable: true, writable: true, value: async () => { const transactionKey = getQueryKey(); await this.createQuery({ type: 'transaction', transactionKey, action: 'begin', }); const transaction = { lastAffectedRows: undefined, }; const query = async (passStatement) => { const statement = normalizeStatement(passStatement); if (statement.exec) { this.transactionQueryKeyQueue.push(transactionKey); return statement.exec(); } const { rows, columns, numAffectedRows } = await this.exec(statement.sql, statement.params, 'all', transactionKey); transaction.lastAffectedRows = numAffectedRows; return convertRowsToObjects(rows, columns); }; const sql = async (queryTemplate, ...params) => { const statement = sqlTag(queryTemplate, params); return query(statement); }; const batch = async (passStatements) => { const statements = passStatements(sqlTag); const data = await this.execBatch(statements, transactionKey); return data.map(({ rows, columns }) => { return convertRowsToObjects(rows, columns); }); }; const commit = async () => { await this.createQuery({ type: 'transaction', transactionKey, action: 'commit', }); }; const rollback = async () => { await this.createQuery({ type: 'transaction', transactionKey, action: 'rollback', }); }; return Object.assign(transaction, { transactionKey, query, sql, batch, commit, rollback, }); } }); /** * Execute SQL transactions against the database. * @see {@link https://sqlocal.dev/api/transaction} */ Object.defineProperty(this, "transaction", { enumerable: true, configurable: true, writable: true, value: async (transaction) => { const dbLockOptions = { mode: this.processor instanceof Worker ? 'shared' : 'exclusive', bypass: false, key: getDatabaseKey(this.config.databasePath, this.clientKey), }; const connectionLockOptions = { mode: 'exclusive', bypass: false, key: this.clientKey, }; return mutationLock(dbLockOptions, async () => { return mutationLock(connectionLockOptions, async () => { let tx; this.bypassMutationLock = true; try { tx = await this.beginTransaction(); const result = await transaction({ query: tx.query, sql: tx.sql, batch: tx.batch, }); await tx.commit(); return result; } catch (err) { await tx?.rollback(); throw err; } finally { this.bypassMutationLock = false; } }); }); } }); /** * Subscribe to a SQL query and receive the latest results * whenever the read tables change. * @see {@link https://sqlocal.dev/api/reactivequery} */ Object.defineProperty(this, "reactiveQuery", { enumerable: true, configurable: true, writable: true, value: (passStatement) => { let value = []; let gotFirstValue = false; let isListening = false; let updateCount = 0; const statement = normalizeStatement(passStatement); const watchedTables = new Set(); const subObservers = new Set(); const errObservers = new Set(); const runStatement = async () => { try { const updateOrder = ++updateCount; if (watchedTables.size === 0) { const usedTables = await this.sql("SELECT name, wr FROM tables_used(?) WHERE type = 'table'", statement.sql); const readTables = new Set(); const writtenTables = new Set(); usedTables.forEach((table) => { if (typeof table.name !== 'string') return; table.wr ? writtenTables.add(table.name) : readTables.add(table.name); }); if (readTables.size === 0) { throw new Error('The passed SQL does not read any tables.'); } if (Array.from(writtenTables).some((table) => readTables.has(table))) { throw new Error('The passed SQL would mutate one or more of the tables that it reads. Doing this in a reactive query would create an infinite loop.'); } readTables.forEach((name) => watchedTables.add(name)); } const results = statement.exec ? await statement.exec() : await this.sql(statement.sql, ...statement.params); if (updateOrder === updateCount) { value = results; gotFirstValue = true; subObservers.forEach((observer) => observer(value)); } } catch (err) { errObservers.forEach((observer) => { observer(err instanceof Error ? err : new Error(String(err))); }); } }; const onEffect = (message) => { if (message.data.tables.some((table) => watchedTables.has(table))) { runStatement(); } }; return { get value() { return value; }, subscribe: (onData, onError) => { if (!this.effectsChannel) { throw new Error('This SQLocal instance is not configured for reactive queries. Set the "reactive" option to enable them.'); } if (!onError) { onError = (err) => { throw err; }; } subObservers.add(onData); errObservers.add(onError); if (!isListening) { this.effectsChannel.addEventListener('message', onEffect); isListening = true; runStatement(); } else if (gotFirstValue) { onData(value); } return { unsubscribe: () => { subObservers.delete(onData); errObservers.delete(onError); if (subObservers.size !== 0) return; this.effectsChannel?.removeEventListener('message', onEffect); isListening = false; }, }; }, }; } }); Object.defineProperty(this, "createFunction", { enumerable: true, configurable: true, writable: true, value: async (fn) => { const key = `_sqlocal_func_${fn.name}`; const attachFunction = () => { if (fn.type === 'scalar') { this.proxy[key] = fn.func; } if (fn.type === 'aggregate' || fn.type === 'window') { this.proxy[`${key}_step`] = fn.func.step; this.proxy[`${key}_final`] = fn.func.final; } if (fn.type === 'window') { this.proxy[`${key}_value`] = fn.func.value; this.proxy[`${key}_inverse`] = fn.func.inverse; } }; if (fn.type !== 'callback' && this.proxy === globalThis) { attachFunction(); } await this.createQuery({ type: 'function', functionName: fn.name, functionType: fn.type, }); if (fn.type !== 'callback' && this.proxy !== globalThis) { attachFunction(); } if (fn.type === 'callback') { this.userCallbacks.set(fn.name, fn.func); } } }); /** * Create a SQL function that can be called from queries to * trigger a JavaScript callback. * @see {@link https://sqlocal.dev/api/createcallbackfunction} */ Object.defineProperty(this, "createCallbackFunction", { enumerable: true, configurable: true, writable: true, value: async (funcName, func) => { await this.createFunction({ name: funcName, type: 'callback', func }); } }); /** * Create a SQL function that can be called from queries to * transform column values or to filter rows. * @see {@link https://sqlocal.dev/api/createscalarfunction} */ Object.defineProperty(this, "createScalarFunction", { enumerable: true, configurable: true, writable: true, value: async (funcName, func) => { await this.createFunction({ name: funcName, type: 'scalar', func }); } }); /** * Create a SQL function that can be called from queries to * combine multiple rows into a single result row. * @see {@link https://sqlocal.dev/api/createaggregatefunction} */ Object.defineProperty(this, "createAggregateFunction", { enumerable: true, configurable: true, writable: true, value: async (funcName, func) => { await this.createFunction({ name: funcName, type: 'aggregate', func }); } }); /** * Create a SQL function that can be called from queries to * perform calculations for rows using data from related rows. * @see {@link https://sqlocal.dev/api/createwindowfunction} */ Object.defineProperty(this, "createWindowFunction", { enumerable: true, configurable: true, writable: true, value: async (funcName, func) => { await this.createFunction({ name: funcName, type: 'window', func }); } }); /** * Retrieve information about the SQLite database file. * @see {@link https://sqlocal.dev/api/getdatabaseinfo} */ Object.defineProperty(this, "getDatabaseInfo", { enumerable: true, configurable: true, writable: true, value: async () => { const message = await this.createQuery({ type: 'getinfo' }); if (message.type === 'info') { return message.info; } else { throw new Error('The database failed to return valid information.'); } } }); /** * Access the SQLite database file so that it can be uploaded * to the server or allowed to be downloaded by the user. * @see {@link https://sqlocal.dev/api/getdatabasefile} */ Object.defineProperty(this, "getDatabaseFile", { enumerable: true, configurable: true, writable: true, value: async () => { const message = await this.createQuery({ type: 'export' }); if (message.type === 'buffer') { return new File([message.buffer], message.bufferName, { type: 'application/x-sqlite3', }); } else { throw new Error('The database failed to export.'); } } }); /** * Replace the contents of the SQLite database file. * @see {@link https://sqlocal.dev/api/overwritedatabasefile} */ Object.defineProperty(this, "overwriteDatabaseFile", { enumerable: true, configurable: true, writable: true, value: async (databaseFile, beforeUnlock) => { await mutationLock({ mode: 'exclusive', bypass: false, key: getDatabaseKey(this.config.databasePath, this.clientKey), }, async () => { try { this.broadcast({ type: 'close', clientKey: this.clientKey, }); const database = await normalizeDatabaseFile(databaseFile, 'buffer'); await this.createQuery({ type: 'import', database, }); if (typeof beforeUnlock === 'function') { this.bypassMutationLock = true; await beforeUnlock(); } this.broadcast({ type: 'reinit', clientKey: this.clientKey, reason: 'overwrite', }); } finally { this.bypassMutationLock = false; } }); } }); /** * Delete the SQLite database file. * @see {@link https://sqlocal.dev/api/deletedatabasefile} */ Object.defineProperty(this, "deleteDatabaseFile", { enumerable: true, configurable: true, writable: true, value: async (beforeUnlock, destroy = false) => { await mutationLock({ mode: 'exclusive', bypass: false, key: getDatabaseKey(this.config.databasePath, this.clientKey), }, async () => { try { this.broadcast({ type: 'close', clientKey: this.clientKey, }); await this.createQuery({ type: 'delete', destroy, }); if (typeof beforeUnlock === 'function') { this.bypassMutationLock = true; await beforeUnlock(); } this.broadcast({ type: 'reinit', clientKey: this.clientKey, reason: 'delete', }); } finally { this.bypassMutationLock = false; } }); if (destroy) { await this.destroy(true); } } }); /** * Disconnect this SQLocal client from the database and terminate * its worker thread. * @see {@link https://sqlocal.dev/api/destroy} */ Object.defineProperty(this, "destroy", { enumerable: true, configurable: true, writable: true, value: async (skipOptimize = false) => { await this.createQuery({ type: 'destroy', skipOptimize }); if (typeof globalThis.Worker !== 'undefined' && this.processor instanceof Worker) { this.processor.removeEventListener('message', this.processMessageEvent); this.processor.terminate(); } this.queriesInProgress.clear(); this.userCallbacks.clear(); this.reinitChannel.close(); this.effectsChannel?.close(); this.isDestroyed = true; } }); /** * Handles cleaning up the SQLocal client with JavaScript * Explicit Resource Management (`using`). * @internal */ Object.defineProperty(this, _a, { enumerable: true, configurable: true, writable: true, value: () => { this.destroy(); } }); /** * Handles cleaning up the SQLocal client with JavaScript * Explicit Resource Management (`await using`). * @internal */ Object.defineProperty(this, _b, { enumerable: true, configurable: true, writable: true, value: async () => { await this.destroy(); } }); const clientConfig = typeof config === 'string' ? { databasePath: config } : config; const { onInit, onConnect, processor, ...commonConfig } = clientConfig; const { databasePath } = commonConfig; this.config = clientConfig; this.clientKey = getQueryKey(); const dbKey = getDatabaseKey(databasePath, this.clientKey); this.reinitChannel = new BroadcastChannel(`_sqlocal_reinit_(${dbKey})`); if (commonConfig.reactive) { this.effectsChannel = new BroadcastChannel(`_sqlocal_effects_(${dbKey})`); } if (typeof processor !== 'undefined') { this.processor = processor; } else if (databasePath === 'local' || databasePath === ':localStorage:') { const driver = new SQLiteKvvfsDriver('local'); this.processor = new SQLocalProcessor(driver); } else if (databasePath === 'session' || databasePath === ':sessionStorage:') { const driver = new SQLiteKvvfsDriver('session'); this.processor = new SQLocalProcessor(driver); } else if (typeof globalThis.Worker !== 'undefined' && databasePath !== ':memory:') { this.processor = new Worker(new URL('./worker', import.meta.url), { type: 'module', }); } else { const driver = new SQLiteMemoryDriver(); this.processor = new SQLocalProcessor(driver); } if (this.processor instanceof SQLocalProcessor) { this.processor.onmessage = (message) => this.processMessageEvent(message); this.proxy = globalThis; } else { this.processor.addEventListener('message', this.processMessageEvent); this.proxy = coincident(this.processor); } this.processor.postMessage({ type: 'config', config: { ...commonConfig, clientKey: this.clientKey, onInitStatements: onInit?.(sqlTag) ?? [], }, }); } } _a = Symbol.dispose, _b = Symbol.asyncDispose; //# sourceMappingURL=client.js.map