import { DBAdapterDefaultMixin, Mutex, Semaphore } from '@powersync/common';
/**
 * A connection pool implementation delegating to another pool opened asynchronnously.
 */
class AsyncConnectionPool {
    name;
    state;
    resolvedWriter;
    pendingListeners = new Set();
    constructor(inner, name) {
        this.name = name;
        this.state = inner.then((client) => {
            for (const pending of this.pendingListeners) {
                pending.closeAfterRegisteredOnResolvedPool = client.writer.registerListener(pending.listener);
            }
            this.pendingListeners.clear();
            this.resolvedWriter = client.writer;
            if (client.additionalReaders.length) {
                return readWritePoolState(client.writer, client.additionalReaders);
            }
            return singleConnectionPoolState(client.writer);
        });
    }
    async init() {
        await this.state;
    }
    async close() {
        const state = await this.state;
        await state.close();
    }
    async readLock(fn, options) {
        const state = await this.state;
        return state.withConnection(true, fn, options);
    }
    async writeLock(fn, options) {
        const state = await this.state;
        return state.withConnection(false, fn, options);
    }
    async refreshSchema() {
        const state = await this.state;
        await state.refreshSchema();
    }
    registerListener(listener) {
        if (this.resolvedWriter) {
            return this.resolvedWriter.registerListener(listener);
        }
        else {
            const pending = { listener };
            this.pendingListeners.add(pending);
            return () => {
                if (pending.closeAfterRegisteredOnResolvedPool) {
                    return pending.closeAfterRegisteredOnResolvedPool();
                }
                else {
                    // Has not been registered yet, we can just remove the pending listener.
                    this.pendingListeners.delete(pending);
                }
            };
        }
    }
}
function singleConnectionPoolState(connection) {
    return {
        writer: connection,
        withConnection: (allowReadOnly, fn, options) => {
            if (allowReadOnly) {
                return connection.readLock(fn, options);
            }
            else {
                return connection.writeLock(fn, options);
            }
        },
        close: () => connection.close(),
        refreshSchema: () => connection.refreshSchema()
    };
}
function readWritePoolState(writer, readers) {
    // DatabaseClients have locks internally, so these aren't necessary for correctness. However, our mutex and semaphore
    // implementations are very cheap to cancel, which we use to dispatch reads to the first available connection (by
    // simply requesting all of them and sticking with the first connection we get).
    const writerMutex = new Mutex();
    const readerSemaphore = new Semaphore(readers);
    return {
        writer,
        async withConnection(allowReadOnly, fn, options) {
            const abortController = new AbortController();
            const abortSignal = abortController.signal;
            let timeout = null;
            let release;
            if (options?.timeoutMs) {
                timeout = setTimeout(() => abortController.abort, options.timeoutMs);
            }
            try {
                if (allowReadOnly) {
                    let connection;
                    // Even if we have a pool of read connections, it's typically very small and we assume that most queries are
                    // reads. So, we want to request any connection from the read pool and the dedicated write connection (which
                    // can also serve reads). We race for the first connection we can obtain this way, and then abort the other
                    // request.
                    [connection, release] = await new Promise((resolve, reject) => {
                        let didComplete = false;
                        function complete() {
                            didComplete = true;
                            abortController.abort();
                        }
                        function completeSuccess(connection, returnFn) {
                            if (didComplete) {
                                // We're not going to use this connection, so return it immediately.
                                returnFn();
                            }
                            else {
                                complete();
                                resolve([connection, returnFn]);
                            }
                        }
                        function completeError(error) {
                            // We either have a working connection already, or we've rejected the promise. Either way, we don't need
                            // to do either thing again.
                            if (didComplete)
                                return;
                            complete();
                            reject(error);
                        }
                        writerMutex.acquire(abortSignal).then((unlock) => completeSuccess(writer, unlock), completeError);
                        readerSemaphore
                            .requestOne(abortSignal)
                            .then(({ item, release }) => completeSuccess(item, release), completeError);
                    });
                    return await connection.readLock(fn);
                }
                else {
                    return await writerMutex.runExclusive(() => writer.writeLock(fn), abortSignal);
                }
            }
            finally {
                if (timeout != null) {
                    clearTimeout(timeout);
                }
                release?.();
            }
        },
        async close() {
            await writer.close();
            await Promise.all(readers.map((r) => r.close()));
        },
        async refreshSchema() {
            await writer.refreshSchema();
            await Promise.all(readers.map((r) => r.refreshSchema()));
        }
    };
}
export class AsyncDbAdapter extends DBAdapterDefaultMixin(AsyncConnectionPool) {
    async shareConnection() {
        const state = await this.state;
        return state.writer.shareConnection();
    }
    getConfiguration() {
        if (this.resolvedWriter) {
            return this.resolvedWriter.getConfiguration();
        }
        throw new Error('AsyncDbAdapter.getConfiguration() can only be called after initializing it.');
    }
}
