import { AbortError } from 'abort-error';
import { Queue } from 'it-queue';
import impl from './node.js';
const mutexes = new Map();
let implementation;
export function isMortice(obj) {
    return typeof obj?.readLock === 'function' && typeof obj?.writeLock === 'function';
}
export function getImplementation(opts) {
    if (implementation == null) {
        implementation = impl(opts);
        if (!isMortice(implementation)) {
            const emitter = implementation;
            // we are master, set up worker requests
            emitter.addEventListener('requestReadLock', (event) => {
                const mutexName = event.detail.name;
                const identifier = event.detail.identifier;
                const mutex = mutexes.get(mutexName);
                if (mutex == null) {
                    return;
                }
                const abortController = new AbortController();
                const abortListener = (event) => {
                    if (event.detail.name !== mutexName || event.detail.identifier !== identifier) {
                        return;
                    }
                    abortController.abort();
                };
                emitter.addEventListener('abortReadLockRequest', abortListener);
                void mutex.readLock({
                    signal: abortController.signal
                })
                    .then(async (release) => {
                    await event.detail.handler()
                        .finally(() => {
                        release();
                    });
                })
                    .catch(err => {
                    event.detail.onError(err);
                })
                    .finally(() => {
                    emitter.removeEventListener('abortReadLockRequest', abortListener);
                });
            });
            emitter.addEventListener('requestWriteLock', (event) => {
                const mutexName = event.detail.name;
                const identifier = event.detail.identifier;
                const mutex = mutexes.get(mutexName);
                if (mutex == null) {
                    return;
                }
                const abortController = new AbortController();
                const abortListener = (event) => {
                    if (event.detail.name !== mutexName || event.detail.identifier !== identifier) {
                        return;
                    }
                    abortController.abort();
                };
                emitter.addEventListener('abortWriteLockRequest', abortListener);
                void mutex.writeLock({
                    signal: abortController.signal
                })
                    .then(async (release) => {
                    await event.detail.handler()
                        .finally(() => {
                        release();
                    });
                })
                    .catch(err => {
                    event.detail.onError(err);
                })
                    .finally(() => {
                    emitter.removeEventListener('abortWriteLockRequest', abortListener);
                });
            });
            emitter.addEventListener('finalizeRequest', (event) => {
                const mutexName = event.detail.name;
                const mutex = mutexes.get(mutexName);
                if (mutex == null) {
                    return;
                }
                mutex.finalize();
            });
        }
    }
    return implementation;
}
async function createReleasable(queue, options) {
    let res;
    let rej;
    const p = new Promise((resolve, reject) => {
        res = resolve;
        rej = reject;
    });
    const listener = () => {
        rej(new AbortError());
    };
    options?.signal?.addEventListener('abort', listener, {
        once: true
    });
    queue.add(async () => {
        await new Promise((resolve) => {
            res(() => {
                options?.signal?.removeEventListener('abort', listener);
                resolve();
            });
        });
    }, {
        signal: options?.signal
    })
        .catch((err) => {
        rej(err);
    });
    return p;
}
export const createMutex = (name, options) => {
    let mutex = mutexes.get(name);
    if (mutex != null) {
        return mutex;
    }
    const implementation = getImplementation(options);
    // a Mortice instance will be returned if we are a worker, otherwise if we are
    // primary an event target will be returned that fires events when workers
    // request a lock
    if (isMortice(implementation)) {
        mutex = implementation;
        mutexes.set(name, mutex);
        return mutex;
    }
    const masterQueue = new Queue({
        concurrency: 1
    });
    let readQueue;
    mutex = {
        async readLock(opts) {
            // If there's already a read queue, just add the task to it
            if (readQueue != null) {
                return createReleasable(readQueue, opts);
            }
            // Create a new read queue
            readQueue = new Queue({
                concurrency: options.concurrency,
                autoStart: false
            });
            const localReadQueue = readQueue;
            // Add the task to the read queue
            const readPromise = createReleasable(readQueue, opts);
            void masterQueue.add(async () => {
                // Start the task only once the master queue has completed processing
                // any previous tasks
                localReadQueue.start();
                // Once all the tasks in the read queue have completed, remove it so
                // that the next read lock will occur after any write locks that were
                // started in the interim
                await localReadQueue.onIdle()
                    .then(() => {
                    if (readQueue === localReadQueue) {
                        readQueue = null;
                    }
                });
            });
            return readPromise;
        },
        async writeLock(opts) {
            // Remove the read queue reference, so that any later read locks will be
            // added to a new queue that starts after this write lock has been
            // released
            readQueue = null;
            return createReleasable(masterQueue, opts);
        },
        finalize: () => {
            mutexes.delete(name);
        },
        queue: masterQueue
    };
    mutexes.set(name, mutex);
    // if requested, finalize the lock once the last lock holder has released it
    if (options.autoFinalize === true) {
        masterQueue.addEventListener('idle', () => {
            mutex.finalize();
        }, {
            once: true
        });
    }
    return mutex;
};
//# sourceMappingURL=mortice.js.map