/**
 * FIFO semaphore.
 *
 * Bounds the number of concurrent holders to `maxPermits`. Callers `acquire`
 * a slot and invoke the returned function to release it. Slots are granted
 * to waiters in arrival order. `Infinity` short-circuits to a no-op so
 * callers can disable throttling without branching at every call site.
 *
 * The release function is idempotent: calling it more than once does
 * nothing. This makes try/finally bookkeeping safe even if cleanup paths
 * overlap.
 *
 * AbortSignal: a queued `acquire` that observes its signal aborting will
 * remove itself from the queue and reject. A signal that aborts after
 * `acquire` has resolved is the caller's responsibility — they already hold
 * the slot and must call release.
 */
const NO_OP_RELEASE = () => { };
const NO_OP_SEMAPHORE = {
    acquire: async () => NO_OP_RELEASE,
    tryAcquire: () => NO_OP_RELEASE,
};
export function createSemaphore(maxPermits) {
    if (maxPermits === Infinity) {
        return NO_OP_SEMAPHORE;
    }
    if (!Number.isInteger(maxPermits) || maxPermits <= 0) {
        throw new Error(`maxPermits must be a positive integer or Infinity, got: ${maxPermits}`);
    }
    let permits = maxPermits;
    const waiters = [];
    const release = () => {
        const next = waiters.shift();
        if (next) {
            // Hand the slot directly to the next waiter — permits stays the same.
            next.grant();
        }
        else {
            permits++;
        }
    };
    const makeReleaseOnce = () => {
        let released = false;
        return () => {
            if (released)
                return;
            released = true;
            release();
        };
    };
    return {
        tryAcquire() {
            if (permits > 0) {
                permits--;
                return makeReleaseOnce();
            }
            return null;
        },
        async acquire(signal) {
            if (signal?.aborted) {
                throw signal.reason ?? new DOMException("Aborted", "AbortError");
            }
            if (permits > 0) {
                permits--;
                return makeReleaseOnce();
            }
            return new Promise((resolve, reject) => {
                const onAbort = () => {
                    const idx = waiters.indexOf(waiter);
                    if (idx !== -1) {
                        waiters.splice(idx, 1);
                        waiter.cancel(signal?.reason ?? new DOMException("Aborted", "AbortError"));
                    }
                };
                const waiter = {
                    grant: () => {
                        signal?.removeEventListener("abort", onAbort);
                        resolve(makeReleaseOnce());
                    },
                    cancel: (reason) => {
                        signal?.removeEventListener("abort", onAbort);
                        reject(reason);
                    },
                };
                signal?.addEventListener("abort", onAbort);
                waiters.push(waiter);
            });
        },
    };
}
