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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@henrygd/semaphore

v0.0.2

Published

Fast inline semaphores and mutexes

Downloads

2,598

Readme

@henrygd/semaphore

File Size MIT license JSR Score 100%

Fast inline semaphores and mutexes. See comparisons and benchmarks.

Semaphores limit simultaneous access to code and resources (e.g. a file) among multiple concurrent tasks.

Works with:

Usage

Create or retrieve a semaphore by calling getSemaphore with optional key and concurrency limit.

const sem = getSemaphore('key', 1)

Use the acquire and release methods to limit access.

await sem.acquire()
// access here is limited to one task at a time
sem.release()

Full example

We use semaphores here to prevent multiple requests to an API for the same resource.

The first calls to fetchPokemon will acquire access to the protected code. Subsequent calls will wait, then return the data from the cache.

We use a key to allow access based on the name. This lets ditto and snorlax run simultaneously.

import { getSemaphore } from '@henrygd/semaphore'

const cache = new Map()

for (let i = 0; i < 5; i++) {
    fetchPokemon('ditto')
    fetchPokemon('snorlax')
}

async function fetchPokemon(name) {
    // get semaphore with key based on name
    const sem = getSemaphore(name)
    // acquire access from the semaphore
    await sem.acquire()
    try {
        // return data from cache if available
        if (cache.has(name)) {
            console.log('Cache hit:', name)
            return cache.get(name)
        }
        // otherwise fetch from API
        console.warn('Fetching from API:', name)
        const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`)
        const json = await res.json()
        cache.set(name, json)
        return json
    } finally {
        // release access when done
        sem.release()
    }
}

Interface

/**
 * Creates or retrieves existing semaphore with optional key and concurrency level.
 *
 * key - Key used to identify the semaphore. Defaults to `Symbol()`.
 * concurrency - Maximum concurrent tasks allowed access. Defaults to `1`.
 */
function getSemaphore(key?: any, concurrency?: number): Semaphore

interface Semaphore {
    /** Returns a promise that resolves when access is acquired */
    acquire(): Promise<void>
    /** Release access to the semaphore */
    release(): void
    /** Returns the total number of tasks active or waiting for access */
    size(): number
}

Keys and persistence

Keyed semaphores are held in a Map and deleted from the Map once they've been acquired and fully released (no waiting tasks).

If you need to reuse the same semaphore even after deletion from the Map, use an persistent variable instead of calling getSemaphore again.

Concurrency

Concurrency is set for each semaphore on first creation via getSemaphore. If called again using the key for an active semaphore, the concurrency argument is ignored and the existing semaphore is returned.

Comparisons and benchmarks

Note that we're looking at libraries which provide a promise-based locking mechanism, not callbacks.

| Library | Version | Bundle size (B) | Keys | Weekly Downloads | | :--------------------------------------------------------------------- | :------ | :-------------- | :--- | :--------------- | | @henrygd/semaphore | 0.0.1 | 267 | yes | ¯\_(ツ)_/¯ | | async-mutex | 0.5.0 | 4,758 | no | 1,639,071 | | async-sema | 3.1.1 | 3,532 | no | 1,258,877 | | await-semaphore | 0.1.3 | 1,184 | no | 60,449 | | @shopify/semaphore | 3.1.0 | 604 | no | 29,089 |

If there's a library you'd like added to the table or benchmarks, please open an issue.

Benchmarks

All libraries run the same test. Each operation measures how long it takes a binary semaphore with 1,000 queued acquire requests to allow and release all requests.

Browser benchmark

This test was run in Chromium. Chrome and Edge are the same. Safari is more lopsided with Vercel's async-sema dropping to third. Firefox, though I love and respect it, seems to be hard capped by slow promise handling, with async-mutex not far behind.

You can run or tweak for yourself here: https://jsbm.dev/8bBxR1pBLw0TM

@henrygd/queue - 13,665 Ops/s. async-sema - 8,077 Ops/s. async-mutex - 5,576 Ops/s. @shopify/semaphore - 4,099 Ops/s.

Note: await-semaphore is extremely slow for some reason and I didn't want to include it in the image because it seems excessive. Not sure what's happening there.

Node.js benchmark

@henrygd/queue - 1.7x faster than async-sema. 2.66x async-mutex. 3.08x async-semaphore. 3.47x @shopify/semaphore.

Bun benchmark

@henrygd/queue - 2x faster than async-semaphore 2.63x asynsc-mutex. 2.68x async-sema. 3.77x @shopify/semaphore.

Deno benchmark

@henrygd/queue - 1.7x faster than async-sema. 2.7x async-mutex. 2.72x await-semaphore. 4.01x @shopify/semaphore.

Cloudflare Workers benchmark

Uses oha to make 1,000 requests to each worker. Each request creates a semaphore and resolves 5,000 acquisitions / releases.

This was run locally using Wrangler. Wrangler uses the same workerd runtime as workers deployed to Cloudflare, so the relative difference should be accurate. Here's the repo for this benchmark.

| Library | Requests/sec | Total (sec) | Average | Slowest | | :----------------- | :----------- | :---------- | :------ | :------ | | @henrygd/semaphore | 941.8135 | 1.0618 | 0.0521 | 0.0788 | | async-mutex | 569.5130 | 1.7559 | 0.0862 | 0.1251 | | async-sema | 375.7332 | 2.6615 | 0.1308 | 0.1818 | | @shopify/semaphore | 167.8239 | 5.9586 | 0.2925 | 0.4063 | | await-semaphore* | n/a | n/a | n/a | n/a |

* await-semaphore does not work with concurrent requests.

Related

@henrygd/queue - Tiny async queue with concurrency control. Like p-limit or fastq, but smaller and faster.

License

MIT license