sqlocal
Version:
SQLocal makes it easy to run SQLite3 in the browser, backed by the origin private file system.
756 lines • 30.7 kB
JavaScript
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