Skip to content

Class Method Utilities ​

Decorators ​

INFO

When using asyncmux as a decorator, you must use an environment that supports Stage 3 decorators or an equivalent implementation.

@asyncmux ​

The @asyncmux class method decorator acquires a write lock and executes the class method exclusively.

Signature ​

ts
function asyncmux<TMethod extends AsyncClassMethod>(method: TMethod, context: unknown): TMethod;

Arguments ​

method

  • Type: (this: any, ...args: any) => Promise<any>

A class method that returns a Promise object.

context

  • Type: unknown

The context object for Stage 3 decorators.

Exceptions ​

DecoratorSupportError

Thrown if the context argument is determined not to be a Stage 3 decorator context object.

Examples ​

In the following example, Task B would normally output its ID to the console faster than Task A. however, because concurrency is controlled, Task A outputs its ID first, followed by Task B.

ts
import { asyncmux } from "asyncmux";

class Service {
  @asyncmux
  async update(duration: string, id: string) {
    await sleep(duration);
    console.log(`update: ${id}`);
  }
}

const service = new Service();

const updatePromiseA = service.update("3s", "A");
const updatePromiseB = service.update("1s", "B");

await Promise.all([updatePromiseA, updatePromiseB]);
// update: A
// update: B

WARNING

Reentrancy (re-acquiring a lock in a way that would deadlock) is not supported. While a class method that holds a write lock is running, calling another class method on the same instance that requires a write lock (a method decorated with @asyncmux) or a read lock (a method decorated with @asyncmux.readonly) throws a ReentrantLockError. The same applies when a write lock is requested while a read lock is held. Read locks are shared locks, so a method decorated with @asyncmux.readonly can call another method decorated with @asyncmux.readonly on the same instance.

The following example demonstrates that executing another class method that requires a write lock from within a class method that already holds a write lock throws a ReentrantLockError.

ts
import { asyncmux } from "asyncmux";

class Service {
  @asyncmux
  async create() {
    // Requesting a write lock while holding one would deadlock.
    // A ReentrantLockError is thrown as soon as the lock acquisition is requested.
    const updatePromise = service.update("3s", "A");

    await updatePromise;
  }

  @asyncmux
  async update(duration: string, id: string) {
    await sleep(duration);
    console.log(`update: ${id}`);
  }
}

const service = new Service();

await service.create(); // Error: Cannot acquire a lock while the same lock is already held

INFO

ReentrantLockError detection uses an async context. On runtimes where process.getBuiltinModule is available, such as Node.js 22.3+ and Bun, reentrancy across await boundaries is also detected. On environments where an async context is unavailable, such as browsers, only synchronous call ranges are detected and other reentrant calls still deadlock.

@asyncmux.readonly ​

The @asyncmux.readonly class method decorator acquires a read lock and executes the class method. Multiple methods decorated with @asyncmux.readonly can execute concurrently.

Signature ​

ts
function asyncmux.readonly<TMethod extends AsyncClassMethod>(
  method: TMethod,
  context: unknown,
): TMethod;

Arguments ​

method

  • Type: (this: any, ...args: any) => Promise<any>

A class method that returns a Promise object.

context

  • Type: unknown

The context object for Stage 3 decorators.

Exceptions ​

DecoratorSupportError

Thrown if the context argument is determined not to be a Stage 3 decorator context object.

Examples ​

In the following example, Task B has a shorter execution time than Task A. Since multiple read locks can be held simultaneously, they process in parallel, and Task B outputs its ID to the console first.

ts
import { asyncmux } from "asyncmux";

class Service {
  @mutex.readonly
  async read(duration: string, id: string) {
    await sleep(duration);
    console.log(`read: ${id}`);
  }
}

const service = new Service();

const readPromiseA = service.read("3s", "A");
const readPromiseB = service.read("1s", "B");

await Promise.all([readPromiseA, readPromiseB]);
// read: B
// read: A

The following example shows a class method acquiring a read lock and then executing other methods that also require read locks. These are all treated as shared locks and execute in parallel.

ts
import { asyncmux } from "asyncmux";

class Service {
  @mutex.readonly
  async list() {
    const readPromiseA = service.read("3s", "A");
    const readPromiseB = service.read("1s", "B");

    await Promise.all([readPromiseA, readPromiseB]);
  }

  @mutex.readonly
  async read(duration: string, id: string) {
    await sleep(duration);
    console.log(`read: ${id}`);
  }
}

const service = new Service();

await service.list();
// read: B
// read: A

Functional API ​

asyncmux() ​

The asyncmux function acquires a write lock within a class method.

Signature ​

ts
function asyncmux(
  this_: object,
  signal?: AbortSignal,
): Promise<
  Disposable & {
    release(): void;
  }
>;

Arguments ​

this_

  • Type: object

The instance of the class.

options.signal

  • Type: AbortSignal

An optional signal to abort the lock acquisition.

Return Value ​

A Promise that resolves to an object used to release the lock. You can release the lock either by using the using statement or by calling the .release() method on this object. Note that .release() cannot be called more than once.

Exceptions ​

If the options.signal is already aborted, it throws options.signal.reason.

Examples ​

The following example uses the using statement to acquire a write lock within a class method.

ts
import { asyncmux } from "asyncmux";

class Service {
  async create(data: string, signal?: AbortSignal) {
    using _ = await asyncmux(this, signal);
    // ...
  }
}

The following example manually acquires and releases a write lock within a class method.

ts
import { asyncmux } from "asyncmux";

class Service {
  async create(data: string, signal?: AbortSignal) {
    let lock;
    if (__STRICT_MODE__) {
      lock = await asyncmux(this, signal);
    }

    try {
      // ...
    } finally {
      lock?.release();
    }
  }
}

asyncmux.readonly() ​

The asyncmux.readonly function acquires a read lock within a class method.

Signature ​

ts
function asyncmux.readonly(
  this_: object,
  signal?: AbortSignal,
): Promise<Disposable & {
  release(): void;
}>;

Arguments ​

this_

  • Type: object

The instance of the class.

options.signal

  • Type: AbortSignal

An optional signal to abort the lock acquisition.

Return Value ​

A Promise that resolves to an object used to release the lock. You can release the lock either by using the using statement or by calling the .release() method. Once released, the .release() method cannot be called again.

Exceptions ​

If the options.signal is already aborted, it throws options.signal.reason.

Examples ​

The following example uses the using statement to acquire a read lock within a class method.

ts
import { asyncmux } from "asyncmux";

class Service {
  async read(data: string, signal?: AbortSignal) {
    using _ = await asyncmux.readonly(this, signal);
    // ...
  }
}

The following example manually acquires and releases a read lock within a class method.

ts
import { asyncmux } from "asyncmux";

class Service {
  async read(data: string, signal?: AbortSignal) {
    let lock;
    if (__STRICT_MODE__) {
      lock = await asyncmux.readonly(this, signal);
    }

    try {
      // ...
    } finally {
      lock?.release();
    }
  }
}