Async Mutex With FIFO Waiters

Serialize critical sections with an async mutex that queues waiters in FIFO order, supports acquisition timeouts and safe double release.

JavascriptResilience

Code

snippet
export class AsyncMutex {
  #locked = false;
  #waiters = [];

  get locked() {
    return this.#locked;
  }

  get queued() {
    return this.#waiters.length;
  }

  async acquire(options = {}) {
    const { timeoutMs } = options;
    if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
      throw new RangeError("timeoutMs must be a positive finite number");
    }
    if (!this.#locked) {
      this.#locked = true;
      return this.#createRelease();
    }
    return new Promise((resolve, reject) => {
      const waiter = { resolve, reject, timer: undefined };
      if (timeoutMs !== undefined) {
        waiter.timer = setTimeout(() => {
          const index = this.#waiters.indexOf(waiter);
          if (index === -1) return;
          this.#waiters.splice(index, 1);
          reject(new Error("timed out waiting for mutex after " + timeoutMs + "ms"));
        }, timeoutMs);
      }
      this.#waiters.push(waiter);
    });
  }

  #createRelease() {
    let released = false;
    return () => {
      if (released) return;
      released = true;
      const next = this.#waiters.shift();
      if (next) {
        if (next.timer !== undefined) clearTimeout(next.timer);
        next.resolve(this.#createRelease());
      } else {
        this.#locked = false;
      }
    };
  }

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