{"id":19635494,"url":"https://github.com/henrygd/semaphore","last_synced_at":"2025-04-28T08:30:36.584Z","repository":{"id":246435513,"uuid":"821157053","full_name":"henrygd/semaphore","owner":"henrygd","description":"Faster inline semaphores and mutexes in javascript ","archived":false,"fork":false,"pushed_at":"2024-06-30T20:13:29.000Z","size":32,"stargazers_count":20,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-10-06T23:35:12.134Z","etag":null,"topics":["async","async-await","concurrency","mutex","promise","semaphore"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/henrygd.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-27T23:55:40.000Z","updated_at":"2024-10-05T10:14:52.000Z","dependencies_parsed_at":"2024-06-28T00:16:31.792Z","dependency_job_id":"0e441384-d716-43d2-91e7-5ee0fbaa1b2a","html_url":"https://github.com/henrygd/semaphore","commit_stats":null,"previous_names":["henrygd/semaphore"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrygd%2Fsemaphore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrygd%2Fsemaphore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrygd%2Fsemaphore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/henrygd%2Fsemaphore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/henrygd","download_url":"https://codeload.github.com/henrygd/semaphore/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224101763,"owners_count":17256022,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","async-await","concurrency","mutex","promise","semaphore"],"created_at":"2024-11-11T12:25:34.152Z","updated_at":"2024-11-11T12:25:35.537Z","avatar_url":"https://github.com/henrygd.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[size-image]: https://img.shields.io/github/size/henrygd/semaphore/dist/index.min.js?style=flat\n[license-image]: https://img.shields.io/github/license/henrygd/semaphore?style=flat\u0026color=%2349ac0c\n[license-url]: /LICENSE\n\n# @henrygd/semaphore\n\n[![File Size][size-image]](https://github.com/henrygd/semaphore/blob/main/dist/index.min.js) [![MIT license][license-image]][license-url] [![JSR Score 100%](https://jsr.io/badges/@henrygd/semaphore/score?v1)](https://jsr.io/@henrygd/semaphore)\n\nFast inline semaphores and mutexes. See [comparisons and benchmarks](#comparisons-and-benchmarks).\n\nSemaphores limit simultaneous access to code and resources (e.g. a file) among multiple concurrent tasks.\n\nWorks with: \u003cimg alt=\"browsers\" title=\"This package works with browsers.\" height=\"16px\" src=\"https://jsr.io/logos/browsers.svg\" /\u003e \u003cimg alt=\"Deno\" title=\"This package works with Deno.\" height=\"16px\" src=\"https://jsr.io/logos/deno.svg\" /\u003e \u003cimg alt=\"Node.js\" title=\"This package works with Node.js\" height=\"16px\" src=\"https://jsr.io/logos/node.svg\" /\u003e \u003cimg alt=\"Cloudflare Workers\" title=\"This package works with Cloudflare Workers.\" height=\"16px\" src=\"https://jsr.io/logos/cloudflare-workers.svg\" /\u003e \u003cimg alt=\"Bun\" title=\"This package works with Bun.\" height=\"16px\" src=\"https://jsr.io/logos/bun.svg\" /\u003e\n\n## Usage\n\nCreate or retrieve a semaphore by calling `getSemaphore` with optional key and concurrency limit.\n\n```js\nconst sem = getSemaphore('key', 1)\n```\n\nUse the `acquire` and `release` methods to limit access.\n\n```js\nawait sem.acquire()\n// access here is limited to one task at a time\nsem.release()\n```\n\n## Full example\n\nWe use semaphores here to prevent multiple requests to an API for the same resource.\n\nThe first calls to `fetchPokemon` will acquire access to the protected code. Subsequent calls will wait, then return the data from the cache.\n\nWe use a key to allow access based on the name. This lets `ditto` and `snorlax` run simultaneously.\n\n\u003c!-- prettier-ignore --\u003e\n```js\nimport { getSemaphore } from '@henrygd/semaphore'\n\nconst cache = new Map()\n\nfor (let i = 0; i \u003c 5; i++) {\n    fetchPokemon('ditto')\n    fetchPokemon('snorlax')\n}\n\nasync function fetchPokemon(name) {\n    // get semaphore with key based on name\n    const sem = getSemaphore(name)\n    // acquire access from the semaphore\n    await sem.acquire()\n    try {\n        // return data from cache if available\n        if (cache.has(name)) {\n            console.log('Cache hit:', name)\n            return cache.get(name)\n        }\n        // otherwise fetch from API\n        console.warn('Fetching from API:', name)\n        const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`)\n        const json = await res.json()\n        cache.set(name, json)\n        return json\n    } finally {\n        // release access when done\n        sem.release()\n    }\n}\n```\n\n## Interface\n\n\u003c!-- prettier-ignore --\u003e\n```ts\n/**\n * Creates or retrieves existing semaphore with optional key and concurrency level.\n *\n * key - Key used to identify the semaphore. Defaults to `Symbol()`.\n * concurrency - Maximum concurrent tasks allowed access. Defaults to `1`.\n */\nfunction getSemaphore(key?: any, concurrency?: number): Semaphore\n\ninterface Semaphore {\n    /** Returns a promise that resolves when access is acquired */\n    acquire(): Promise\u003cvoid\u003e\n    /** Release access to the semaphore */\n    release(): void\n    /** Returns the total number of tasks active or waiting for access */\n    size(): number\n}\n```\n\n### Keys and persistence\n\nKeyed semaphores are held in a `Map` and deleted from the `Map` once they've been acquired and fully released (no waiting tasks).\n\nIf you need to reuse the same semaphore even after deletion from the `Map`, use a persistent variable instead of calling `getSemaphore` again.\n\n### Concurrency\n\nConcurrency 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.\n\n## Comparisons and benchmarks\n\nNote that we're looking at libraries which provide a promise-based locking mechanism, not callbacks.\n\n| Library                                                                | Version | Bundle size (B) | Keys | Weekly Downloads |\n| :--------------------------------------------------------------------- | :------ | :-------------- | :--- | :--------------- |\n| @henrygd/semaphore                                                     | 0.0.1   | 267             | yes  | ¯\\\\\\_(ツ)\\_/¯    |\n| [async-mutex](https://www.npmjs.com/package/async-mutex)               | 0.5.0   | 4,758           | no   | 1,639,071        |\n| [async-sema](https://www.npmjs.com/package/async-sema)                 | 3.1.1   | 3,532           | no   | 1,258,877        |\n| [await-semaphore](https://www.npmjs.com/package/await-semaphore)       | 0.1.3   | 1,184           | no   | 60,449           |\n| [@shopify/semaphore](https://www.npmjs.com/package/@shopify/semaphore) | 3.1.0   | 604             | no   | 29,089           |\n\n\u003e If there's a library you'd like added to the table or benchmarks, please open an issue.\n\n## Benchmarks\n\nAll 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.\n\n### Browser benchmark\n\nThis 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.\n\nYou can run or tweak for yourself here: https://jsbm.dev/8bBxR1pBLw0TM\n\n![@henrygd/queue - 13,665 Ops/s. async-sema - 8,077 Ops/s. async-mutex - 5,576 Ops/s. @shopify/semaphore - 4,099 Ops/s.](https://henrygd-assets.b-cdn.net/semaphore/browser.png)\n\n\u003e 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.\n\n### Node.js benchmark\n\n![@henrygd/queue - 1.7x faster than async-sema. 2.66x async-mutex. 3.08x async-semaphore. 3.47x @shopify/semaphore.](https://henrygd-assets.b-cdn.net/semaphore/node.png)\n\n### Bun benchmark\n\n![@henrygd/queue - 2x faster than async-semaphore 2.63x asynsc-mutex. 2.68x async-sema. 3.77x @shopify/semaphore.](https://henrygd-assets.b-cdn.net/semaphore/bun-bench.png)\n\n### Deno benchmark\n\n![@henrygd/queue - 1.7x faster than async-sema. 2.7x async-mutex. 2.72x await-semaphore. 4.01x @shopify/semaphore.](https://henrygd-assets.b-cdn.net/semaphore/deno-bench.png)\n\n### Cloudflare Workers benchmark\n\nUses [oha](https://github.com/hatoo/oha) to make 1,000 requests to each worker. Each request creates a semaphore and resolves 5,000 acquisitions / releases.\n\nThis was run locally using [Wrangler](https://developers.cloudflare.com/workers/get-started/guide/). Wrangler uses the same [workerd](https://github.com/cloudflare/workerd) runtime as workers deployed to Cloudflare, so the relative difference should be accurate. Here's the [repo for this benchmark](https://github.com/henrygd/semaphore-wrangler-benchmark).\n\n| Library            | Requests/sec | Total (sec) | Average | Slowest |\n| :----------------- | :----------- | :---------- | :------ | :------ |\n| @henrygd/semaphore | 941.8135     | 1.0618      | 0.0521  | 0.0788  |\n| async-mutex        | 569.5130     | 1.7559      | 0.0862  | 0.1251  |\n| async-sema         | 375.7332     | 2.6615      | 0.1308  | 0.1818  |\n| @shopify/semaphore | 167.8239     | 5.9586      | 0.2925  | 0.4063  |\n| await-semaphore\\*  | n/a          | n/a         | n/a     | n/a     |\n\n\u003e \\* `await-semaphore` does not work with concurrent requests.\n\n## Related\n\n[`@henrygd/queue`](https://github.com/henrygd/queue) - Tiny async queue with concurrency control. Like p-limit or fastq, but smaller and faster.\n\n## License\n\n[MIT license](/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhenrygd%2Fsemaphore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhenrygd%2Fsemaphore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhenrygd%2Fsemaphore/lists"}