'use strict';

var common = require('@powersync/common');
var os = require('node:os');
var bson = require('bson');
var undici = require('undici');
var asyncMutex = require('async-mutex');
var Comlink = require('comlink');
var fs = require('node:fs/promises');
var path = require('node:path');
var node_worker_threads = require('node:worker_threads');
var node_async_hooks = require('node:async_hooks');
var fs$1 = require('fs');
var path$1 = require('path');

function _interopNamespaceDefault(e) {
    var n = Object.create(null);
    if (e) {
        Object.keys(e).forEach(function (k) {
            if (k !== 'default') {
                var d = Object.getOwnPropertyDescriptor(e, k);
                Object.defineProperty(n, k, d.get ? d : {
                    enumerable: true,
                    get: function () { return e[k]; }
                });
            }
        });
    }
    n.default = e;
    return Object.freeze(n);
}

var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
var Comlink__namespace = /*#__PURE__*/_interopNamespaceDefault(Comlink);
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
var path__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(path$1);

class NodeFetchProvider extends common.FetchImplementationProvider {
    getFetch() {
        return fetch.bind(globalThis);
    }
}
class NodeRemote extends common.AbstractRemote {
    connector;
    logger;
    wsDispatcher;
    constructor(connector, logger = common.DEFAULT_REMOTE_LOGGER, options) {
        const fetchDispatcher = options?.dispatcher ?? defaultFetchDispatcher();
        super(connector, logger, {
            fetchImplementation: options?.fetchImplementation ?? new NodeFetchProvider(),
            fetchOptions: {
                dispatcher: fetchDispatcher
            },
            ...(options ?? {})
        });
        this.connector = connector;
        this.logger = logger;
        this.wsDispatcher = options?.dispatcher;
    }
    createSocket(url) {
        // Create dedicated dispatcher for this WebSocket
        const baseDispatcher = this.getWebsocketDispatcher(url);
        // Create WebSocket with dedicated dispatcher
        const ws = new undici.WebSocket(url, {
            dispatcher: baseDispatcher,
            headers: {
                'User-Agent': this.getUserAgent()
            }
        });
        return ws;
    }
    getWebsocketDispatcher(url) {
        if (this.wsDispatcher != null) {
            return this.wsDispatcher;
        }
        const protocol = new URL(url).protocol.replace(':', '');
        const proxy = getProxyForProtocol(protocol);
        if (proxy != null) {
            return new undici.ProxyAgent(proxy);
        }
        else {
            return undici.getGlobalDispatcher();
        }
    }
    getUserAgent() {
        return [
            super.getUserAgent(),
            `powersync-node`,
            `node/${process.versions.node}`,
            `${os__namespace.platform()}/${os__namespace.release()}`
        ].join(' ');
    }
    async getBSON() {
        return bson.BSON;
    }
}
function defaultFetchDispatcher() {
    // EnvHttpProxyAgent automatically uses HTTP_PROXY, HTTPS_PROXY and NO_PROXY env vars by default.
    // We add ALL_PROXY support.
    return new undici.EnvHttpProxyAgent({
        httpProxy: getProxyForProtocol('http'),
        httpsProxy: getProxyForProtocol('https')
    });
}
function getProxyForProtocol(protocol) {
    return (process.env[`${protocol.toLowerCase()}_proxy`] ??
        process.env[`${protocol.toUpperCase()}_PROXY`] ??
        process.env[`all_proxy`] ??
        process.env[`ALL_PROXY`]);
}

/**
 * Global locks which prevent multiple instances from syncing
 * concurrently.
 */
const LOCKS = new Map();
new Set(Object.values(common.LockType));
class NodeStreamingSyncImplementation extends common.AbstractStreamingSyncImplementation {
    locks;
    constructor(options) {
        super(options);
        this.initLocks();
    }
    /**
     *  Configures global locks on sync process
     */
    initLocks() {
        const { identifier } = this.options;
        if (identifier && LOCKS.has(identifier)) {
            this.locks = LOCKS.get(identifier);
            return;
        }
        this.locks = new Map();
        this.locks.set(common.LockType.CRUD, new asyncMutex.Mutex());
        this.locks.set(common.LockType.SYNC, new asyncMutex.Mutex());
        if (identifier) {
            LOCKS.set(identifier, this.locks);
        }
    }
    obtainLock(lockOptions) {
        const lock = this.locks.get(lockOptions.type);
        if (!lock) {
            throw new Error(`Lock type ${lockOptions.type} not found`);
        }
        return lock.runExclusive(async () => {
            if (lockOptions.signal?.aborted) {
                throw new Error('Aborted');
            }
            return lockOptions.callback();
        });
    }
}

/**
 * A PowerSync database connection implemented with RPC calls to a background worker.
 */
class RemoteConnection {
    isBusy = false;
    worker;
    comlink;
    database;
    notifyWorkerClosed = new AbortController();
    constructor(worker, comlink, database) {
        this.worker = worker;
        this.comlink = comlink;
        this.database = database;
        this.worker.once('exit', (_) => {
            this.notifyWorkerClosed.abort();
        });
    }
    /**
     * Runs the inner function, but appends the stack trace where this function was called. This is useful for workers
     * because stack traces from worker errors are otherwise unrelated to the application issue that has caused them.
     */
    withRemote(inner) {
        const trace = {};
        Error.captureStackTrace(trace);
        const controller = this.notifyWorkerClosed;
        return new Promise((resolve, reject) => {
            if (controller.signal.aborted) {
                reject(new common.ConnectionClosedError('Called operation on closed remote'));
            }
            function handleAbort() {
                reject(new common.ConnectionClosedError('Remote peer closed with request in flight'));
            }
            function completePromise(action) {
                controller.signal.removeEventListener('abort', handleAbort);
                action();
            }
            controller.signal.addEventListener('abort', handleAbort);
            inner()
                .then((data) => completePromise(() => resolve(data)))
                .catch((e) => {
                if (e instanceof Error && e.stack) {
                    e.stack += trace.stack;
                }
                return completePromise(() => reject(e));
            });
        });
    }
    executeBatch(query, params = []) {
        return this.withRemote(async () => {
            const result = await this.database.executeBatch(query, params ?? []);
            return RemoteConnection.wrapQueryResult(result);
        });
    }
    execute(query, params) {
        return this.withRemote(async () => {
            const result = await this.database.execute(query, params ?? []);
            return RemoteConnection.wrapQueryResult(result);
        });
    }
    executeRaw(query, params) {
        return this.withRemote(async () => {
            return await this.database.executeRaw(query, params ?? []);
        });
    }
    async getAll(sql, parameters) {
        const res = await this.execute(sql, parameters);
        return res.rows?._array ?? [];
    }
    async getOptional(sql, parameters) {
        const res = await this.execute(sql, parameters);
        return res.rows?.item(0) ?? null;
    }
    async get(sql, parameters) {
        const res = await this.execute(sql, parameters);
        const first = res.rows?.item(0);
        if (!first) {
            throw new Error('Result set is empty');
        }
        return first;
    }
    async refreshSchema() {
        await this.execute("pragma table_info('sqlite_master')");
    }
    async close() {
        await this.database.close();
        this.database[Comlink.releaseProxy]();
        this.comlink[Comlink.releaseProxy]();
        await this.worker.terminate();
    }
    static wrapQueryResult(result) {
        let rows = undefined;
        if (result.rows) {
            rows = {
                ...result.rows,
                item: (idx) => result.rows?._array[idx]
            };
        }
        return {
            ...result,
            rows
        };
    }
}

const READ_CONNECTIONS = 5;
const defaultDatabaseImplementation = {
    type: 'better-sqlite3'
};
/**
 * Adapter for better-sqlite3
 */
class WorkerConnectionPool extends common.BaseObserver {
    options;
    name;
    readConnections;
    writeConnection;
    readQueue = [];
    writeQueue = [];
    constructor(options) {
        super();
        if (options.readWorkerCount != null && options.readWorkerCount < 1) {
            throw `Needs at least one worker for reads, got ${options.readWorkerCount}`;
        }
        this.options = options;
        this.name = options.dbFilename;
    }
    async initialize() {
        let dbFilePath = this.options.dbFilename;
        if (this.options.dbLocation !== undefined) {
            // Make sure the dbLocation exists, we get a TypeError from better-sqlite3 otherwise.
            let directoryExists = false;
            try {
                const stat = await fs.stat(this.options.dbLocation);
                directoryExists = stat.isDirectory();
            }
            catch (_) {
                // If we can't even stat, the directory won't be accessible to SQLite either.
            }
            if (!directoryExists) {
                throw new Error(`The dbLocation directory at "${this.options.dbLocation}" does not exist. Please create it before opening the PowerSync database!`);
            }
            dbFilePath = path__namespace.join(this.options.dbLocation, dbFilePath);
        }
        const openWorker = async (isWriter) => {
            let worker;
            const workerName = isWriter ? `write ${dbFilePath}` : `read ${dbFilePath}`;
            const workerFactory = this.options.openWorker ?? ((...args) => new node_worker_threads.Worker(...args));
            {
                worker = workerFactory(path__namespace.resolve(__dirname, 'DefaultWorker.cjs'), { name: workerName });
            }
            const listeners = new WeakMap();
            const comlink = Comlink__namespace.wrap({
                postMessage: worker.postMessage.bind(worker),
                addEventListener: (type, listener) => {
                    let resolved = 'handleEvent' in listener ? listener.handleEvent.bind(listener) : listener;
                    // Comlink wants message events, but the message event on workers in Node returns the data only.
                    if (type === 'message') {
                        const original = resolved;
                        resolved = (data) => {
                            original({ data });
                        };
                    }
                    listeners.set(listener, resolved);
                    worker.addListener(type, resolved);
                },
                removeEventListener: (type, listener) => {
                    const resolved = listeners.get(listener);
                    if (!resolved) {
                        return;
                    }
                    worker.removeListener(type, resolved);
                }
            });
            worker.once('error', (e) => {
                console.error('Unexpected PowerSync database worker error', e);
            });
            const database = (await comlink.open({
                path: dbFilePath,
                isWriter,
                implementation: this.options.implementation ?? defaultDatabaseImplementation
            }));
            if (isWriter) {
                await database.execute("SELECT powersync_update_hooks('install');", []);
            }
            const connection = new RemoteConnection(worker, comlink, database);
            if (this.options.initializeConnection) {
                await this.options.initializeConnection(connection, isWriter);
            }
            if (!isWriter) {
                await connection.execute('pragma query_only = true');
            }
            else {
                // We only need to enable this on the writer connection.
                // We can get `database is locked` errors if we enable this on concurrently opening read connections.
                await connection.execute('pragma journal_mode = WAL');
            }
            return connection;
        };
        // Open the writer first to avoid multiple threads enabling WAL concurrently (causing "database is locked" errors).
        this.writeConnection = await openWorker(true);
        const createWorkers = [];
        const amountOfReaders = this.options.readWorkerCount ?? READ_CONNECTIONS;
        for (let i = 0; i < amountOfReaders; i++) {
            createWorkers.push(openWorker(false));
        }
        this.readConnections = await Promise.all(createWorkers);
    }
    async close() {
        await this.writeConnection.close();
        for (const connection of this.readConnections) {
            await connection.close();
        }
    }
    readLock(fn, _options) {
        let resolveConnectionPromise;
        const connectionPromise = new Promise((resolve, _reject) => {
            resolveConnectionPromise = node_async_hooks.AsyncResource.bind(resolve);
        });
        const connection = this.readConnections.find((connection) => !connection.isBusy);
        if (connection) {
            connection.isBusy = true;
            resolveConnectionPromise(connection);
        }
        else {
            this.readQueue.push(resolveConnectionPromise);
        }
        return (async () => {
            const connection = await connectionPromise;
            try {
                return await fn(connection);
            }
            finally {
                const next = this.readQueue.shift();
                if (next) {
                    next(connection);
                }
                else {
                    connection.isBusy = false;
                }
            }
        })();
    }
    writeLock(fn, _options) {
        let resolveLockPromise;
        const lockPromise = new Promise((resolve, _reject) => {
            resolveLockPromise = node_async_hooks.AsyncResource.bind(resolve);
        });
        if (!this.writeConnection.isBusy) {
            this.writeConnection.isBusy = true;
            resolveLockPromise();
        }
        else {
            this.writeQueue.push(resolveLockPromise);
        }
        return (async () => {
            await lockPromise;
            try {
                try {
                    return await fn(this.writeConnection);
                }
                finally {
                    const serializedUpdates = await this.writeConnection.executeRaw("SELECT powersync_update_hooks('get');", []);
                    const updates = JSON.parse(serializedUpdates[0][0]);
                    if (updates.length > 0) {
                        const event = {
                            tables: updates,
                            groupedUpdates: {},
                            rawUpdates: []
                        };
                        this.iterateListeners((cb) => cb.tablesUpdated?.(event));
                    }
                }
            }
            finally {
                const next = this.writeQueue.shift();
                if (next) {
                    next();
                }
                else {
                    this.writeConnection.isBusy = false;
                }
            }
        })();
    }
    readTransaction(fn, _options) {
        return this.readLock((ctx) => this.internalTransaction(ctx, fn));
    }
    writeTransaction(fn, _options) {
        return this.writeLock((ctx) => this.internalTransaction(ctx, fn));
    }
    async internalTransaction(connection, fn) {
        let finalized = false;
        const commit = async () => {
            if (!finalized) {
                finalized = true;
                await connection.execute('COMMIT');
            }
            return { rowsAffected: 0 };
        };
        const rollback = async () => {
            if (!finalized) {
                finalized = true;
                await connection.execute('ROLLBACK');
            }
            return { rowsAffected: 0 };
        };
        try {
            await connection.execute('BEGIN');
            const result = await fn({
                execute: (query, params) => connection.execute(query, params),
                executeRaw: (query, params) => connection.executeRaw(query, params),
                executeBatch: (query, params) => connection.executeBatch(query, params),
                get: (query, params) => connection.get(query, params),
                getAll: (query, params) => connection.getAll(query, params),
                getOptional: (query, params) => connection.getOptional(query, params),
                commit,
                rollback
            });
            await commit();
            return result;
        }
        catch (ex) {
            try {
                await rollback();
            }
            catch (ex2) {
                // In rare cases, a rollback may fail.
                // Safe to ignore.
            }
            throw ex;
        }
    }
    getAll(sql, parameters) {
        return this.readLock((ctx) => ctx.getAll(sql, parameters));
    }
    getOptional(sql, parameters) {
        return this.readLock((ctx) => ctx.getOptional(sql, parameters));
    }
    get(sql, parameters) {
        return this.readLock((ctx) => ctx.get(sql, parameters));
    }
    execute(query, params) {
        return this.writeLock((ctx) => ctx.execute(query, params));
    }
    executeRaw(query, params) {
        return this.writeLock((ctx) => ctx.executeRaw(query, params));
    }
    executeBatch(query, params) {
        return this.writeTransaction((ctx) => ctx.executeBatch(query, params));
    }
    async refreshSchema() {
        await this.writeConnection.refreshSchema();
        for (const readConnection of this.readConnections) {
            await readConnection.refreshSchema();
        }
    }
}

/**
 * A PowerSync database which provides SQLite functionality
 * which is automatically synced.
 *
 * @example
 * ```typescript
 * export const db = new PowerSyncDatabase({
 *  schema: AppSchema,
 *  database: {
 *    dbFilename: 'example.db'
 *  }
 * });
 * ```
 */
class PowerSyncDatabase extends common.AbstractPowerSyncDatabase {
    constructor(options) {
        super(options);
    }
    async _initialize() {
        await this.database.initialize();
    }
    /**
     * Opens a DBAdapter using better-sqlite3 as the default SQLite open factory.
     */
    openDBAdapter(options) {
        return new WorkerConnectionPool(options.database);
    }
    generateBucketStorageAdapter() {
        return new common.SqliteBucketStorage(this.database, this.logger);
    }
    connect(connector, options) {
        return super.connect(connector, options);
    }
    generateSyncStreamImplementation(connector, options) {
        const logger = this.logger;
        const remote = new NodeRemote(connector, logger, {
            dispatcher: options.dispatcher,
            ...this.options.remoteOptions
        });
        return new NodeStreamingSyncImplementation({
            adapter: this.bucketStorageAdapter,
            remote,
            uploadCrud: async () => {
                await this.waitForReady();
                await connector.uploadData(this);
            },
            ...options,
            identifier: this.database.name,
            logger
        });
    }
}

/**
 * NodeFileSystemAdapter implements LocalStorageAdapter using Node.js filesystem.
 * Suitable for Node.js environments and Electron applications.
 */
class NodeFileSystemAdapter {
    storageDirectory;
    constructor(storageDirectory = './user_data') {
        this.storageDirectory = storageDirectory;
    }
    async initialize() {
        const dir = path__namespace$1.resolve(this.storageDirectory);
        await fs$1.promises.mkdir(dir, { recursive: true });
    }
    async clear() {
        const dir = path__namespace$1.resolve(this.storageDirectory);
        await fs$1.promises.rmdir(dir, { recursive: true });
    }
    getLocalUri(filename) {
        return path__namespace$1.join(path__namespace$1.resolve(this.storageDirectory), filename);
    }
    async uploadFile(filePath, data, options) {
        const buffer = Buffer.from(data);
        await fs$1.promises.writeFile(filePath, buffer, {
            encoding: options?.encoding
        });
    }
    async downloadFile(filePath) {
        const data = await fs$1.promises.readFile(filePath);
        return new Blob([new Uint8Array(data)]);
    }
    async saveFile(filePath, data, options) {
        let buffer;
        if (typeof data === 'string') {
            buffer = Buffer.from(data, options?.encoding ?? common.EncodingType.Base64);
        }
        else {
            buffer = Buffer.from(data);
        }
        await fs$1.promises.writeFile(filePath, buffer);
        return buffer.length;
    }
    async readFile(filePath, options) {
        const data = await fs$1.promises.readFile(filePath);
        if (options?.encoding === common.EncodingType.Base64) {
            return Buffer.from(data.toString(), 'base64').buffer;
        }
        else {
            return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
        }
    }
    async deleteFile(path, options) {
        await fs$1.promises.unlink(path).catch((err) => {
            if (err.code !== 'ENOENT') {
                throw err;
            }
        });
    }
    async fileExists(filePath) {
        try {
            await fs$1.promises.access(filePath);
            return true;
        }
        catch {
            return false;
        }
    }
    async makeDir(path) {
        await fs$1.promises.mkdir(path, { recursive: true });
    }
    async rmDir(path) {
        await fs$1.promises.rmdir(path, { recursive: true });
    }
}

exports.NodeFileSystemAdapter = NodeFileSystemAdapter;
exports.PowerSyncDatabase = PowerSyncDatabase;
Object.keys(common).forEach(function (k) {
    if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
        enumerable: true,
        get: function () { return common[k]; }
    });
});
//# sourceMappingURL=bundle.cjs.map
