631 lines
25 KiB
JavaScript
631 lines
25 KiB
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 { normalizeSql } from './lib/normalize-sql.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';
|
|
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('shared', this.bypassMutationLock ||
|
|
message.type === 'import' ||
|
|
message.type === 'delete', this.config, 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);
|
|
}
|
|
});
|
|
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') {
|
|
data.rows = message.data[0]?.rows ?? [];
|
|
data.columns = message.data[0]?.columns ?? [];
|
|
}
|
|
return data;
|
|
}
|
|
});
|
|
Object.defineProperty(this, "execBatch", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (statements) => {
|
|
const message = await this.createQuery({
|
|
type: 'batch',
|
|
statements,
|
|
});
|
|
const data = new Array(statements.length).fill({
|
|
rows: [],
|
|
columns: [],
|
|
});
|
|
if (message.type === 'data') {
|
|
message.data.forEach((result, resultIndex) => {
|
|
data[resultIndex] = result;
|
|
});
|
|
}
|
|
return data;
|
|
}
|
|
});
|
|
Object.defineProperty(this, "sql", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (queryTemplate, ...params) => {
|
|
const statement = normalizeSql(queryTemplate, params);
|
|
const { rows, columns } = await this.exec(statement.sql, statement.params, 'all');
|
|
const resultRecords = convertRowsToObjects(rows, columns);
|
|
return resultRecords;
|
|
}
|
|
});
|
|
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 }) => {
|
|
const resultRecords = convertRowsToObjects(rows, columns);
|
|
return resultRecords;
|
|
});
|
|
}
|
|
});
|
|
Object.defineProperty(this, "beginTransaction", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async () => {
|
|
const transactionKey = getQueryKey();
|
|
await this.createQuery({
|
|
type: 'transaction',
|
|
transactionKey,
|
|
action: 'begin',
|
|
});
|
|
const query = async (passStatement) => {
|
|
const statement = normalizeStatement(passStatement);
|
|
if (statement.exec) {
|
|
this.transactionQueryKeyQueue.push(transactionKey);
|
|
return statement.exec();
|
|
}
|
|
const { rows, columns } = await this.exec(statement.sql, statement.params, 'all', transactionKey);
|
|
const resultRecords = convertRowsToObjects(rows, columns);
|
|
return resultRecords;
|
|
};
|
|
const sql = async (queryTemplate, ...params) => {
|
|
const statement = normalizeSql(queryTemplate, params);
|
|
const resultRecords = await query(statement);
|
|
return resultRecords;
|
|
};
|
|
const commit = async () => {
|
|
await this.createQuery({
|
|
type: 'transaction',
|
|
transactionKey,
|
|
action: 'commit',
|
|
});
|
|
};
|
|
const rollback = async () => {
|
|
await this.createQuery({
|
|
type: 'transaction',
|
|
transactionKey,
|
|
action: 'rollback',
|
|
});
|
|
};
|
|
return {
|
|
query,
|
|
sql,
|
|
commit,
|
|
rollback,
|
|
};
|
|
}
|
|
});
|
|
Object.defineProperty(this, "transaction", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (transaction) => {
|
|
return mutationLock('exclusive', false, this.config, async () => {
|
|
let tx;
|
|
this.bypassMutationLock = true;
|
|
try {
|
|
tx = await this.beginTransaction();
|
|
const result = await transaction({
|
|
sql: tx.sql,
|
|
query: tx.query,
|
|
});
|
|
await tx.commit();
|
|
return result;
|
|
}
|
|
catch (err) {
|
|
await tx?.rollback();
|
|
throw err;
|
|
}
|
|
finally {
|
|
this.bypassMutationLock = false;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
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, "createCallbackFunction", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (funcName, func) => {
|
|
await this.createQuery({
|
|
type: 'function',
|
|
functionName: funcName,
|
|
functionType: 'callback',
|
|
});
|
|
this.userCallbacks.set(funcName, func);
|
|
}
|
|
});
|
|
Object.defineProperty(this, "createScalarFunction", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (funcName, func) => {
|
|
const key = `_sqlocal_func_${funcName}`;
|
|
const attachFunction = () => {
|
|
this.proxy[key] = func;
|
|
};
|
|
if (this.proxy === globalThis) {
|
|
attachFunction();
|
|
}
|
|
await this.createQuery({
|
|
type: 'function',
|
|
functionName: funcName,
|
|
functionType: 'scalar',
|
|
});
|
|
if (this.proxy !== globalThis) {
|
|
attachFunction();
|
|
}
|
|
}
|
|
});
|
|
Object.defineProperty(this, "createAggregateFunction", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (funcName, func) => {
|
|
const key = `_sqlocal_func_${funcName}`;
|
|
const attachFunction = () => {
|
|
this.proxy[`${key}_step`] = func.step;
|
|
this.proxy[`${key}_final`] = func.final;
|
|
};
|
|
if (this.proxy === globalThis) {
|
|
attachFunction();
|
|
}
|
|
await this.createQuery({
|
|
type: 'function',
|
|
functionName: funcName,
|
|
functionType: 'aggregate',
|
|
});
|
|
if (this.proxy !== globalThis) {
|
|
attachFunction();
|
|
}
|
|
}
|
|
});
|
|
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.');
|
|
}
|
|
}
|
|
});
|
|
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.');
|
|
}
|
|
}
|
|
});
|
|
Object.defineProperty(this, "overwriteDatabaseFile", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (databaseFile, beforeUnlock) => {
|
|
await mutationLock('exclusive', false, this.config, 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;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Object.defineProperty(this, "deleteDatabaseFile", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (beforeUnlock) => {
|
|
await mutationLock('exclusive', false, this.config, async () => {
|
|
try {
|
|
this.broadcast({
|
|
type: 'close',
|
|
clientKey: this.clientKey,
|
|
});
|
|
await this.createQuery({
|
|
type: 'delete',
|
|
});
|
|
if (typeof beforeUnlock === 'function') {
|
|
this.bypassMutationLock = true;
|
|
await beforeUnlock();
|
|
}
|
|
this.broadcast({
|
|
type: 'reinit',
|
|
clientKey: this.clientKey,
|
|
reason: 'delete',
|
|
});
|
|
}
|
|
finally {
|
|
this.bypassMutationLock = false;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Object.defineProperty(this, "destroy", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async () => {
|
|
await this.createQuery({ type: 'destroy' });
|
|
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;
|
|
}
|
|
});
|
|
Object.defineProperty(this, _a, {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: () => {
|
|
this.destroy();
|
|
}
|
|
});
|
|
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
|