npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@p-vbordei/async-mutex

v0.2.1

Published

Tiny async mutex, RWLock (writer-preference), and counting semaphore. Zero dependencies.

Readme

async-mutex

ci

npm downloads bundle

Tiny async-aware concurrency primitives — Mutex, RWLock (writer-preference, no starvation), and Semaphore. Zero dependencies.

import { Mutex, RWLock, Semaphore } from "@p-vbordei/async-mutex";

// Mutex: serialize critical section
const m = new Mutex();
await m.run(async () => {
  // exclusive access
});

// RWLock: readers concurrent, writer exclusive
const lock = new RWLock();
await lock.withRead(async () => { /* parallel reads */ });
await lock.withWrite(async () => { /* exclusive write */ });

// Semaphore: bounded concurrency
const sem = new Semaphore(5);
await Promise.all(urls.map((u) => sem.run(() => fetch(u))));

Install

npm install @p-vbordei/async-mutex

> Published on npm under the scope `@p-vbordei/async-mutex` because the bare name `async-mutex` was already taken.

Works with Node 20+, browsers, Bun, Deno. ESM + CJS.

## Why

JavaScript is single-threaded but async operations interleave — code between `await` points runs while other coroutines wait. That means a race condition CAN happen between two async functions reading-modifying-writing the same data:

```ts
// Bug: two concurrent calls can both read the old count
async function increment() {
  const v = await db.get("count");
  await db.set("count", v + 1);
}

You need a mutex even in single-threaded JS. Most existing libraries are CJS-only or ship with event-emitter dependencies. This is ~150 lines, ESM-first, fully typed.

Recipes

Read-modify-write under a mutex

import { Mutex } from "@p-vbordei/async-mutex";

const m = new Mutex();

async function increment() {
  await m.run(async () => {
    const v = await db.get("count");
    await db.set("count", v + 1);
  });
}

Init-once pattern

import { Mutex } from "@p-vbordei/async-mutex";

let inited: Config | null = null;
const initMutex = new Mutex();

async function getConfig(): Promise<Config> {
  if (inited) return inited;
  return await initMutex.run(async () => {
    if (inited) return inited;  // re-check inside lock (double-checked locking)
    inited = await loadConfig();
    return inited;
  });
}

RWLock for cache with occasional invalidation

import { RWLock } from "@p-vbordei/async-mutex";

const lock = new RWLock();
let cache: Snapshot | null = null;

async function read(): Promise<Snapshot> {
  return await lock.withRead(async () => {
    if (cache) return cache;
    return await fetchSnapshot();
  });
}

async function invalidate() {
  await lock.withWrite(async () => { cache = null; });
}

Bounded concurrency with Semaphore

import { Semaphore } from "@p-vbordei/async-mutex";

const sem = new Semaphore(10);

async function processAll(items: Item[]) {
  return Promise.all(items.map((item) => sem.run(() => process(item))));
}

Manual acquire/release (when run isn't enough)

import { Mutex } from "@p-vbordei/async-mutex";

const m = new Mutex();
const release = await m.acquire();
try {
  await criticalSection();
} finally {
  release();  // safe to call multiple times — no-op after first
}

API

class Mutex

m.acquire(): Promise<release>      // returns release function
m.run(fn): Promise<T>              // safe form: lock released on resolve OR reject
m.isLocked: boolean
m.waitingCount: number

class RWLock

lock.acquireRead(): Promise<release>
lock.acquireWrite(): Promise<release>
lock.withRead(fn): Promise<T>
lock.withWrite(fn): Promise<T>
lock.isWriteLocked: boolean
lock.readerCount: number

Writer preference: when a writer is waiting, new readers also wait. Prevents writer starvation under sustained reads.

class Semaphore

new Semaphore(permits)             // throws if permits is not a positive integer
sem.acquire(): Promise<release>
sem.run(fn): Promise<T>
sem.availablePermits: number

Caveats

  • Single-process scope. These are in-memory locks. For cross-process coordination, use Redis Redlock or a database advisory lock.
  • No timeouts on acquire. If you need a deadline, wrap with @p-vbordei/cancellable:
    await withTimeout(m.acquire(), 5000);
  • No reentrancy. Calling acquire() from inside a held lock will deadlock. Track state at your application layer if you need reentry.

License

Apache-2.0 © Vlad Bordei