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

@asaidimu/utils-sync

v2.1.0

Published

A collection of sync utilities.

Readme

@asaidimu/utils-sync

Synchronization primitives for TypeScript/JavaScript – Mutex, Once, and Serializer utilities with fine‑grained concurrency control.

npm version license TypeScript vitest

Table of Contents


Overview & Features

Modern JavaScript applications often face subtle concurrency issues – race conditions, duplicate work, or unexpected interleaving of async operations. @asaidimu/utils-sync provides three battle‑tested primitives to tame asynchronous chaos:

  • Mutex – A mutual exclusion lock that allows only one task at a time to access a shared resource. Configurable handoff scheduling (microtask vs macrotask) prevents microtask starvation under heavy contention.
  • Once – Guarantees that a given asynchronous operation runs exactly once, even when called concurrently from many places. Ideal for lazy initialisation, cache population, or one‑time setup. Supports optional retry on failure and sync/async functions.
  • Serializer – Processes queued tasks sequentially (FIFO) while maintaining the last successful result. Built‑in backpressure protection and the ability to permanently close the queue. Perfect for rate‑limited APIs, write serialisation, or sequential job processing.

All utilities are written in strict TypeScript, fully typed, and come with zero runtime dependencies.

Key Features

  • Mutual exclusionMutex with optional timeout and queue capacity limits.
  • Configurable yield behaviour – Choose "macrotask" (default, prevents starvation) or "microtask" (zero‑delay handoff) per instance.
  • Once‑only executionOnce deduplicates concurrent calls, caches success/failure, and optionally retries on error.
  • Sequential task processingSerializer maintains order, provides last‑result peeking, and can be closed permanently.
  • Timeout support – All operations accept a timeout parameter (lock acquisition + execution).
  • Backpressure – Configurable queue size to prevent uncontrolled growth.
  • Tiny & focused – No external dependencies, tree‑shakeable exports.
  • First‑class TypeScript – Generics, error types, and accurate return types.

Installation & Setup

Prerequisites

  • Node.js 18+ (or any modern environment with Promise, queueMicrotask, and setTimeout)
  • TypeScript 4.7+ (if using types, but not required)

Installation

npm install @asaidimu/utils-sync
pnpm add @asaidimu/utils-sync
yarn add @asaidimu/utils-sync

Verification

After installation, you can test that the library works correctly:

import { Mutex } from '@asaidimu/utils-sync';

const mutex = new Mutex();
console.log(mutex.locked()); // false

If the import runs without errors, the package is ready.


Usage Documentation

All examples assume ES module import syntax:

import { Mutex, Once, Serializer } from '@asaidimu/utils-sync';

For CommonJS:

const { Mutex, Once, Serializer } = require('@asaidimu/utils-sync');

Mutex

A mutual exclusion lock. Use it to protect critical sections where only one async operation should run at a time.

Basic example

const mutex = new Mutex();

async function criticalSection() {
  await mutex.lock();
  try {
    // Only one caller executes this block at a time
    await doSomething();
  } finally {
    mutex.unlock();
  }
}

With timeout

try {
  await mutex.lock(1000); // wait max 1 second
  // ... work ...
  mutex.unlock();
} catch (err) {
  if (err instanceof TimeoutError) {
    console.log('Could not acquire lock in time');
  }
}

Non‑blocking attempt

if (mutex.tryLock()) {
  try {
    // lock acquired immediately
  } finally {
    mutex.unlock();
  }
} else {
  // lock was already held – do something else
}

Options

| Option | Type | Default | Description | |--------------|--------------------------|---------------|-----------------------------------------------------------------------------------------------| | capacity | number | Infinity | Max pending waiters. If exceeded, lock() throws an error. | | yieldMode | "macrotask" | "microtask" | "macrotask" | "macrotask" yields via setTimeout(…,0) (prevents starvation). "microtask" uses queueMicrotask for lower latency. |

API

| Method | Return type | Description | |------------------------------|-----------------------|---------------------------------------------------------------------------------------------------------| | lock(timeout?: number) | Promise<void> | Acquire lock, waiting if necessary. Throws TimeoutError if timeout elapses or queue is full. | | tryLock() | boolean | Attempt to acquire lock without waiting. Returns true if acquired. | | unlock() | void | Release the lock. Throws if not locked. Schedules next waiter according to yieldMode. | | locked() | boolean | Returns true if the lock is currently held. | | pending() | number | Number of tasks waiting for the lock. |


Once

Guarantees a function runs exactly once, even when many callers invoke do() concurrently. The result (or error) is cached and returned to all future callers.

Basic example

const once = new Once<string>();

async function getConfig() {
  const result = await once.do(async () => {
    const res = await fetch('/api/config');
    return res.json();
  });
  // result.value contains the config, or result.error if failed
  return result.value;
}

With retry on failure

const once = new Once<string>({ retry: true });

// If the first attempt fails, the next call will retry
await once.do(failingFn); // fails, but _done = false
await once.do(successFn); // runs again, succeeds, caches result

Synchronous functions

const once = new Once<number>();
const result = await once.do(() => 42); // works with sync return

Checking state without awaiting

if (once.ready()) {
  const { value, error } = once.peek();
  // safely inspect cached result
}

Options

| Option | Type | Default | Description | |-----------|-----------|---------|----------------------------------------------------------------------------------| | retry | boolean | false | If true, a failed execution does not mark the instance as done – next call will retry. | | throws | boolean | false | If true, the do() method will throw the error instead of returning it in the result object. |

API

| Method | Return type | Description | |-----------------------------------|-------------------------------------|---------------------------------------------------------------------------------------------------| | do(fn, timeout?) | Promise<OnceResult<T>> | Executes fn once. Returns { value, error } (unless throws:true). Timeout covers lock + execution. | | ready() | boolean | true if operation has completed (success or non‑retryable failure) and no execution is running. | | running() | boolean | true if the operation is currently executing. | | peek() | OnceResult<T> | Returns current cached { value, error } without waiting. | | get() | T \| null | Returns cached value if done, otherwise throws. Throws cached error if present. | | reset() | void | Clears state – next do() will run again. | | done() | boolean | true if finished (success or final failure). | | current() | Promise<OnceResult<T>> \| null | Returns the underlying promise if running, otherwise null. |


Serializer

Processes tasks sequentially (FIFO order). Each task runs only after all previous tasks have completed. Use it to serialise writes to a file, throttle API calls, or enforce ordering.

Basic example

const serializer = new Serializer<string>();

async function log(message: string) {
  const result = await serializer.do(async () => {
    await appendToFile('log.txt', message);
    return message;
  });
  return result.value; // last successful result
}

Handling failures

Even if a task fails, the serializer continues processing the next queued tasks:

await serializer.do(failingFn);      // returns { error: ... }
await serializer.do(successfulFn);   // still runs

Peeking at the last result

const { value, error } = serializer.peek();

Closing the serializer permanently

serializer.close();
const result = await serializer.do(anyFn);
// result.error instanceof SerializerExecutionDone

Options

| Option | Type | Default | Description | |--------------|--------------------------|------------|------------------------------------------------------------------| | capacity | number | 1000 | Max pending tasks. When full, do() returns an error immediately. | | yieldMode | "macrotask" | "microtask" | "macrotask" | Handoff scheduling for the internal mutex. Default prevents microtask starvation. |

API

| Method | Return type | Description | |-------------------------------|--------------------------------------|------------------------------------------------------------------------------------------------------| | do(fn, timeout?) | Promise<SerializerResult<T\|null>> | Enqueues fn. Returns { value, error }. If closed or queue full, error is SerializerExecutionDone. | | peek() | SerializerResult<T\|null> | Returns the last successful result or last error. | | close() | void | Permanently closes the serializer. All subsequent do() calls fail immediately. | | pending() | number | Number of tasks waiting in the queue. | | running() | boolean | true if a task is currently executing. |


Project Architecture

The library is written in TypeScript and follows a simple, functional‑object design. Each class is independent and does not rely on shared global state.

Core Components

  • Mutex – Implements the lock with a FIFO waiter queue. Handoff uses either setTimeout (macrotask) or queueMicrotask to give callers control over fairness vs. latency.
  • Once – Built on top of Mutex with microtask yield mode for minimal overhead. Tracks execution state (_done, _value, _error) and returns a cached promise to concurrent callers.
  • Serializer – Also uses Mutex (default macrotask yield) to serialise work. Maintains the last result and supports backpressure via capacity.

Data Flow

  1. Mutex – Callers invoke lock(). If unlocked, they acquire immediately. Otherwise they are added to waiters. When unlock() is called, the next waiter is scheduled according to yieldMode.
  2. Once – First caller acquires the mutex, runs the function, and stores the promise. Later callers see the existing promise and await it directly (no mutex contention). After completion, the promise is cleared and _done is set.
  3. Serializer – Each do() call attempts to lock the internal mutex. Only one task holds the lock at a time. When a task finishes (success or error), the lock is released, allowing the next queued task to run.

Extension Points

The library is designed to be used as‑is, but you can easily compose the primitives:

  • Use Mutex to build your own synchronisation patterns (e.g., read‑write locks).
  • Extend Once or Serializer by subclassing (both are standard ES6 classes).
  • Replace the underlying promise scheduling by providing a custom Mutex with different yieldMode logic (though the built‑in modes cover most needs).

Development & Contributing

Development Setup

git clone https://github.com/asaidimu/erp-utils.git
cd erp-utils/src/sync
npm install

Scripts

| Command | Description | |----------------------|--------------------------------------------------| | npm test | Run tests once (Vitest) | | npm run test:watch | Run tests in watch mode | | npm run test:browser | Run tests in a browser environment (Vitest) |

Testing

Tests are written with Vitest and cover:

  • Once – deduplication, retry behaviour, state transitions, error handling.
  • Serializer – FIFO ordering, backpressure, closing, error resilience.
  • Mutex – locking, timeout, capacity, yield modes (implicitly tested via Serializer and Once).

To run the full suite:

npm test

Contributing Guidelines

  1. Fork the repository and create a feature branch.
  2. Write tests for any new functionality or bug fixes.
  3. Ensure existing tests pass (npm test).
  4. Follow the existing code style (Prettier / ESLint – see root of monorepo).
  5. Commit messages should follow Conventional Commits (e.g., feat: add timeout to Mutex).
  6. Open a Pull Request against the main branch.

Issue Reporting

Report bugs or request features via GitHub Issues. Please include:

  • A clear description of the problem.
  • Minimal code to reproduce (if bug).
  • Environment details (Node version, package manager, OS).

Additional Information

Troubleshooting

| Problem | Possible solution | |----------------------------------------------|-------------------------------------------------------------------------------------------------------| | Mutex lock never resolves | Check that unlock() is always called (e.g., use try/finally). | | Serializer tasks stop running | Did you call close()? Once closed, all new tasks fail immediately. | | Once returns stale error even after retry | Ensure retry: true is set. Without it, a failure marks _done = true and never retries. | | TimeoutError when queue seems small | Increase capacity in Mutex or Serializer options. | | Microtask starvation in high‑contention code | Set yieldMode: "macrotask" (default for Serializer and Mutex). Once uses microtask by design. |

FAQ

Q: Can I use Once with a synchronous function?
Yes – Once.do() accepts both () => Promise<T> and () => T. Synchronous return values are automatically wrapped in a resolved promise.

Q: What happens if Once.do() times out?
The timeout applies to the entire operation (including waiting for the mutex and execution). If a timeout occurs, the do() call rejects with TimeoutError, but the background execution (if already started) continues. Future callers will receive the final result.

Q: Is Serializer safe for long‑running tasks?
Absolutely. Tasks run sequentially, so a long task will delay subsequent tasks. Use timeout if you need to enforce a maximum wait per task.

Q: Can I reuse a Once instance after a non‑retryable failure?
Yes – call reset() to clear the cached error and allow a fresh execution.

Q: Does Mutex re‑entrant?
No – attempting to lock() from the same execution context that already holds the lock will deadlock. Use a single lock acquisition per critical section.

Changelog & Roadmap

See the CHANGELOG.md for version history. Future plans include:

  • Semaphore implementation.
  • AsyncCondition variable.
  • DebouncedSerializer for coalescing rapid consecutive calls.

License

MIT © Saidimu

Acknowledgments

Inspired by similar synchronisation primitives in Rust (Mutex, OnceCell), Go (sync.Mutex), and the classic async patterns of the JavaScript ecosystem.