Async Semaphore Concurrency Limit

Cap concurrent async tasks with a promise-based semaphore that queues waiters and releases permits safely.

JavascriptResilience

Code

snippet
export class Semaphore {
  constructor(limit) {
    if (!Number.isInteger(limit) || limit <= 0) throw new RangeError('limit must be a positive integer');
    this.limit = limit;
    this.active = 0;
    this.waiters = [];
  }

  async acquire() {
    if (this.active < this.limit) {
      this.active += 1;
      return () => this.release();
    }
    await new Promise((resolve) => this.waiters.push(resolve));
    this.active += 1;
    return () => this.release();
  }

  release() {
    if (this.active === 0) throw new Error('release without acquire');
    this.active -= 1;
    const next = this.waiters.shift();
    if (next) {
      this.active += 1;
      next(() => this.release());
    }
  }

  async run(task) {
    const release = await this.acquire();
    try {
      return await task();
    } finally {
      release();
    }
  }
}