/** * Simple counting semaphore, for blocking async ops. * cribbed mostly from {@link https://github.com/ComFreek/async-playground} */ export class Semaphore { #counter: number = 0 #resolvers: Array<(taken: boolean) => void> = [] constructor(count = 0) { this.#counter = count } /** * try to take from the semaphore, reducing it's count * if the semaphore is empty, blocks until available, or the given signal aborts. * * @param signal - a signal to use to abort the block * @returns true if the semaphore was successfully taken, false if aborted. */ take(signal?: AbortSignal): Promise { return new Promise((resolve) => { if (signal?.aborted) { resolve(false) return } // if there's resources available, use them this.#counter-- if (this.#counter >= 0) { resolve(true) return } // otherwise add to pending // and explicitly remove the resolver from the list on abort this.#resolvers.push(resolve) signal?.addEventListener('abort', () => { const index = this.#resolvers.indexOf(resolve) if (index >= 0) { this.#resolvers.splice(index, 1) this.#counter++ } resolve(false) }) }) } /** try to take from the semaphore, reducing it's count, *without blocking*. */ poll(): boolean { if (this.#counter <= 0) return false this.#counter-- return true } /** announce that the semaphore is free to be taken by another awaiter. */ free() { this.#counter++ if (this.#resolvers.length > 0) { const resolver = this.#resolvers.shift() if (resolver) queueMicrotask(() => { resolver(true) }) } } }