503 lines
19 KiB
JavaScript
503 lines
19 KiB
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';
|
|
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
|
|
});
|
|
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(async (change) => {
|
|
this.dirtyTables.add(change.table);
|
|
await this.transactionMutex.lock();
|
|
this.emitEffectsDebounced();
|
|
await this.transactionMutex.unlock();
|
|
});
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
});
|
|
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(() => this.emitEffects(), 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: [],
|
|
};
|
|
switch (message.type) {
|
|
case 'query':
|
|
const partOfTransaction = this.transactionKey !== null &&
|
|
this.transactionKey === message.transactionKey;
|
|
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 {
|
|
await this.transactionMutex.lock();
|
|
const results = await this.driver.execBatch(message.statements);
|
|
response.data.push(...results);
|
|
}
|
|
finally {
|
|
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;
|
|
}
|
|
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 } = message;
|
|
let errored = false;
|
|
try {
|
|
await this.driver.clear();
|
|
}
|
|
catch (error) {
|
|
this.emitMessage({
|
|
type: 'error',
|
|
error,
|
|
queryKey,
|
|
});
|
|
errored = true;
|
|
}
|
|
finally {
|
|
await this.init('delete');
|
|
}
|
|
if (!errored) {
|
|
this.emitMessage({
|
|
type: 'success',
|
|
queryKey,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Object.defineProperty(this, "destroy", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: async (message) => {
|
|
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
|