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