sqlocal
Version:
SQLocal makes it easy to run SQLite3 in the browser, backed by the origin private file system.
536 lines • 20.8 kB
JavaScript
import coincident from 'coincident';
import { createMutex } from './lib/create-mutex.js';
import { SQLiteMemoryDriver } from './drivers/sqlite-memory-driver.js';
import { debounce } from './lib/debounce.js';
import { getDatabaseKey } from './lib/get-database-key.js';
/**
* The `SQLocal` client exchanges messages with a paired instance
* of `SQLocalProcessor` to interact with databases.
* @see {@link https://sqlocal.dev/guide/setup}
*/
export class SQLocalProcessor {
constructor(driver) {
Object.defineProperty(this, "driver", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "config", {
enumerable: true,
configurable: true,
writable: true,
value: {}
});
Object.defineProperty(this, "userFunctions", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
Object.defineProperty(this, "initMutex", {
enumerable: true,
configurable: true,
writable: true,
value: createMutex()
});
Object.defineProperty(this, "transactionMutex", {
enumerable: true,
configurable: true,
writable: true,
value: createMutex()
});
Object.defineProperty(this, "transactionKey", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "proxy", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "dirtyTables", {
enumerable: true,
configurable: true,
writable: true,
value: new Set()
});
Object.defineProperty(this, "effectsChannel", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "reinitChannel", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
/**
* After an `InputMessage` has been processed, the resulting
* `OutputMessage` is emitted to the function passed to `onmessage`.
*/
Object.defineProperty(this, "onmessage", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "init", {
enumerable: true,
configurable: true,
writable: true,
value: async (reason) => {
if (!this.config.databasePath || !this.config.clientKey)
return;
await this.initMutex.lock();
try {
try {
await this.driver.init(this.config);
}
catch {
console.warn(`Persistence failed, so ${this.config.databasePath} will not be saved. For origin private file system persistence, make sure your web server is configured to use the correct HTTP response headers (See https://sqlocal.dev/guide/setup#cross-origin-isolation).`);
this.config.databasePath = ':memory:';
this.driver = new SQLiteMemoryDriver();
await this.driver.init(this.config);
}
const dbKey = getDatabaseKey(this.config.databasePath, this.config.clientKey);
this.reinitChannel = new BroadcastChannel(`_sqlocal_reinit_(${dbKey})`);
this.reinitChannel.onmessage = (event) => {
const message = event.data;
if (this.config.clientKey === message.clientKey)
return;
switch (message.type) {
case 'reinit':
this.init(message.reason);
break;
case 'close':
this.driver.destroy();
break;
}
};
if (this.config.reactive) {
this.effectsChannel = new BroadcastChannel(`_sqlocal_effects_(${dbKey})`);
this.driver.onWrite((change) => {
this.dirtyTables.add(change.table);
this.emitEffectsDebounced();
});
}
await Promise.all(Array.from(this.userFunctions.values()).map((fn) => {
return this.initUserFunction(fn);
}));
await this.execInitStatements();
this.emitMessage({ type: 'event', event: 'connect', reason });
}
catch (error) {
this.emitMessage({
type: 'error',
error,
queryKey: null,
});
await this.destroy();
}
finally {
await this.initMutex.unlock();
}
}
});
/**
* To interact with a database, an `InputMessage` is passed to
* `postMessage` for processing.
*/
Object.defineProperty(this, "postMessage", {
enumerable: true,
configurable: true,
writable: true,
value: async (event, _transfer) => {
const message = event instanceof MessageEvent ? event.data : event;
await this.initMutex.lock();
switch (message.type) {
case 'config':
this.editConfig(message);
break;
case 'query':
case 'batch':
case 'transaction':
this.exec(message);
break;
case 'function':
this.createUserFunction(message);
break;
case 'getinfo':
this.getDatabaseInfo(message);
break;
case 'import':
this.importDb(message);
break;
case 'export':
this.exportDb(message);
break;
case 'delete':
this.deleteDb(message);
break;
case 'destroy':
this.destroy(message);
break;
}
await this.initMutex.unlock();
}
});
Object.defineProperty(this, "emitMessage", {
enumerable: true,
configurable: true,
writable: true,
value: (message, transfer = []) => {
if (this.onmessage) {
this.onmessage(message, transfer);
}
}
});
Object.defineProperty(this, "emitEffects", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
if (!this.effectsChannel || this.dirtyTables.size === 0)
return;
this.effectsChannel.postMessage({
type: 'effects',
tables: [...this.dirtyTables],
});
this.dirtyTables.clear();
}
});
Object.defineProperty(this, "emitEffectsDebounced", {
enumerable: true,
configurable: true,
writable: true,
value: debounce(async () => {
await this.transactionMutex.lock();
this.emitEffects();
await this.transactionMutex.unlock();
}, 32, { maxWait: 180 })
});
Object.defineProperty(this, "editConfig", {
enumerable: true,
configurable: true,
writable: true,
value: (message) => {
this.config = message.config;
this.init('initial');
}
});
Object.defineProperty(this, "exec", {
enumerable: true,
configurable: true,
writable: true,
value: async (message) => {
try {
const response = {
type: 'data',
queryKey: message.queryKey,
data: [],
};
const partOfTransaction = this.transactionKey !== null &&
this.transactionKey === message.transactionKey;
switch (message.type) {
case 'query':
try {
if (!partOfTransaction) {
await this.transactionMutex.lock();
}
const statementData = await this.driver.exec(message);
response.data.push(statementData);
}
finally {
if (!partOfTransaction) {
await this.transactionMutex.unlock();
}
}
break;
case 'batch':
try {
if (!partOfTransaction) {
await this.transactionMutex.lock();
}
const results = await this.driver.execBatch(message.statements, partOfTransaction ? 'savepoint' : 'transaction');
response.data.push(...results);
}
finally {
if (!partOfTransaction) {
await this.transactionMutex.unlock();
}
}
break;
case 'transaction':
if (message.action === 'begin') {
await this.transactionMutex.lock();
this.transactionKey = message.transactionKey;
await this.driver.exec({ sql: 'BEGIN' });
}
if ((message.action === 'commit' || message.action === 'rollback') &&
this.transactionKey !== null &&
this.transactionKey === message.transactionKey) {
const sql = message.action === 'commit' ? 'COMMIT' : 'ROLLBACK';
await this.driver.exec({ sql });
this.transactionKey = null;
await this.transactionMutex.unlock();
}
break;
}
this.emitMessage(response);
}
catch (error) {
this.emitMessage({
type: 'error',
error,
queryKey: message.queryKey,
});
}
}
});
Object.defineProperty(this, "execInitStatements", {
enumerable: true,
configurable: true,
writable: true,
value: async () => {
if (this.config.onInitStatements) {
for (let statement of this.config.onInitStatements) {
await this.driver.exec(statement);
}
}
}
});
Object.defineProperty(this, "getDatabaseInfo", {
enumerable: true,
configurable: true,
writable: true,
value: async (message) => {
try {
this.emitMessage({
type: 'info',
queryKey: message.queryKey,
info: {
databasePath: this.config.databasePath,
storageType: this.driver.storageType,
databaseSizeBytes: await this.driver.getDatabaseSizeBytes(),
persisted: await this.driver.isDatabasePersisted(),
},
});
}
catch (error) {
this.emitMessage({
type: 'error',
queryKey: message.queryKey,
error,
});
}
}
});
Object.defineProperty(this, "createUserFunction", {
enumerable: true,
configurable: true,
writable: true,
value: async (message) => {
const { functionName: name, functionType: type, queryKey } = message;
let fn;
if (this.userFunctions.has(name)) {
this.emitMessage({
type: 'error',
error: new Error(`A user-defined function with the name "${name}" has already been created for this SQLocal instance.`),
queryKey,
});
return;
}
switch (type) {
case 'callback':
fn = {
type,
name,
func: (...args) => {
this.emitMessage({ type: 'callback', name, args });
},
};
break;
case 'scalar':
fn = {
type,
name,
func: this.proxy[`_sqlocal_func_${name}`],
};
break;
case 'aggregate':
fn = {
type,
name,
func: {
step: this.proxy[`_sqlocal_func_${name}_step`],
final: this.proxy[`_sqlocal_func_${name}_final`],
},
};
break;
case 'window':
fn = {
type,
name,
func: {
step: this.proxy[`_sqlocal_func_${name}_step`],
value: this.proxy[`_sqlocal_func_${name}_value`],
inverse: this.proxy[`_sqlocal_func_${name}_inverse`],
final: this.proxy[`_sqlocal_func_${name}_final`],
},
};
break;
}
try {
await this.initUserFunction(fn);
this.emitMessage({
type: 'success',
queryKey,
});
}
catch (error) {
this.emitMessage({
type: 'error',
error,
queryKey,
});
}
}
});
Object.defineProperty(this, "initUserFunction", {
enumerable: true,
configurable: true,
writable: true,
value: async (fn) => {
await this.driver.createFunction(fn);
this.userFunctions.set(fn.name, fn);
}
});
Object.defineProperty(this, "importDb", {
enumerable: true,
configurable: true,
writable: true,
value: async (message) => {
const { queryKey, database } = message;
let errored = false;
try {
await this.driver.import(database);
if (this.driver.storageType === 'memory') {
await this.execInitStatements();
}
}
catch (error) {
this.emitMessage({
type: 'error',
error,
queryKey,
});
errored = true;
}
finally {
if (this.driver.storageType !== 'memory') {
await this.init('overwrite');
}
}
if (!errored) {
this.emitMessage({
type: 'success',
queryKey,
});
}
}
});
Object.defineProperty(this, "exportDb", {
enumerable: true,
configurable: true,
writable: true,
value: async (message) => {
const { queryKey } = message;
try {
const { name, data } = await this.driver.export();
this.emitMessage({
type: 'buffer',
queryKey,
bufferName: name,
buffer: data,
}, [data]);
}
catch (error) {
this.emitMessage({
type: 'error',
error,
queryKey,
});
}
}
});
Object.defineProperty(this, "deleteDb", {
enumerable: true,
configurable: true,
writable: true,
value: async (message) => {
const { queryKey, destroy } = message;
let errored = false;
try {
await this.driver.clear();
}
catch (error) {
this.emitMessage({
type: 'error',
error,
queryKey,
});
errored = true;
}
finally {
if (!destroy) {
await this.init('delete');
}
}
if (!errored) {
this.emitMessage({
type: 'success',
queryKey,
});
}
}
});
Object.defineProperty(this, "destroy", {
enumerable: true,
configurable: true,
writable: true,
value: async (message) => {
if (!message?.skipOptimize) {
await this.driver.exec({ sql: 'PRAGMA optimize' });
}
await this.driver.destroy();
if (this.effectsChannel) {
this.emitEffectsDebounced.flush();
this.effectsChannel.close();
this.effectsChannel = undefined;
}
if (this.reinitChannel) {
this.reinitChannel.close();
this.reinitChannel = undefined;
}
if (message) {
this.emitMessage({
type: 'success',
queryKey: message.queryKey,
});
}
}
});
const isInWorker = typeof WorkerGlobalScope !== 'undefined' &&
globalThis instanceof WorkerGlobalScope;
const proxy = isInWorker ? coincident(globalThis) : globalThis;
this.proxy = proxy;
this.driver = driver;
}
}
//# sourceMappingURL=processor.js.map