Keyed Mutex for Per-Key Serialization

Serialize asynchronous work per string key while allowing different keys to run concurrently, with idle lock cleanup.

TypescriptResilience

Code

snippet
export class KeyedMutex {
  private readonly tails = new Map<string, Promise<void>>();

  async run<T>(key: string, task: () => Promise<T>): Promise<T> {
    if (typeof key !== "string" || key.length === 0) {
      throw new TypeError("key must be a non-empty string");
    }
    if (typeof task !== "function") throw new TypeError("task must be a function");
    const previous = this.tails.get(key) ?? Promise.resolve();
    let release: () => void = () => {};
    const current = new Promise<void>((resolve) => {
      release = resolve;
    });
    const tail = previous.then(() => current);
    this.tails.set(key, tail);
    await previous;
    try {
      return await task();
    } finally {
      release();
      if (this.tails.get(key) === tail) this.tails.delete(key);
    }
  }

  isLocked(key: string): boolean {
    return this.tails.has(key);
  }

  get size(): number {
    return this.tails.size;
  }
}