https://github.com/paperstrike/async-shared-mutex
https://github.com/paperstrike/async-shared-mutex
Last synced: 4 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/paperstrike/async-shared-mutex
- Owner: PaperStrike
- License: mit
- Created: 2025-08-31T11:19:13.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-09-10T16:01:30.000Z (9 months ago)
- Last Synced: 2025-09-10T20:08:16.519Z (9 months ago)
- Language: TypeScript
- Homepage:
- Size: 42 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# async-shared-mutex
[](https://www.npmjs.com/package/async-shared-mutex)
Lightweight shared (reader) / exclusive (writer) mutex for TypeScript / ESM. Two flavors:
* `SharedMutex` – low‑level handle based API (you manage the critical section).
* `AsyncSharedMutex` – convenience API that runs a function while holding the mutex.
Both support shared (concurrent) and exclusive (mutually exclusive) acquisition.
## Quick start (task style)
```ts
import { AsyncSharedMutex } from 'async-shared-mutex'
const mtx = new AsyncSharedMutex()
// Exclusive (writer)
await mtx.lock(async () => {
// only one task may run here
await doWrite()
})
// Shared (reader) – many may run together
const [a, b, c] = await Promise.all([
mtx.lockShared(() => readValue('a')),
mtx.lockShared(() => readValue('b')),
mtx.lockShared(() => readValue('c')),
])
```
## Quick start (handle style)
```ts
import { SharedMutex } from 'async-shared-mutex'
const mtx = new SharedMutex()
// Exclusive
const exclusive = await mtx.lock()
try {
await doWrite()
}
finally {
exclusive.unlock()
}
// Shared
const shared = await mtx.lockShared()
try {
const v = await readValue()
console.log(v)
}
finally {
shared.unlock()
}
```
### With TypeScript `using` (TS 5.2+)
```ts
import { SharedMutex } from 'async-shared-mutex'
const mtx = new SharedMutex()
async function doStuff() {
using lock = await mtx.lock() // unlocks automatically at end of scope
await mutate()
}
```
> If your runtime lacks native `Symbol.dispose`, add a small polyfill (see `test/helpers/patchDisposable.ts` for an example) or keep calling `unlock()` manually.
## When to use
Use for coordinating access to a resource where:
* Multiple readers may safely proceed concurrently.
* Writers need full exclusivity (no readers or other writers).
* Writers should not starve behind an ever‑arriving stream of readers.
## Semantics
* Shared acquisitions overlap with other shared acquisitions provided no earlier exclusive is pending / active.
* An exclusive waits for all currently active (or already queued *before it*) shared holders to finish, then runs alone.
* Shared acquisitions requested **after** an exclusive has queued must wait until that exclusive finishes.
* Exclusives are serialized in request order.
* Errors inside a task (or your critical section) propagate; the mutex is still unlocked.
* `try*` variants attempt an instantaneous acquisition; they return `null` if not immediately possible (no waiting side effects).
This gives predictable writer progress (no writer starvation) while still batching readers that arrive before the next writer.
## API
### `class SharedMutex`
Low level; you get locks you must unlock.
| Method | Returns | Description |
| ------ | ------- | ----------- |
| `lock()` | `Promise` | Await for an exclusive (writer) handle. |
| `tryLock()` | `LockHandle \| null` | Immediate exclusive attempt. `null` if busy. |
| `lockShared()` | `Promise` | Await for a shared (reader) handle. |
| `tryLockShared()` | `LockHandle \| null` | Immediate shared attempt (fails if an exclusive is active/pending). |
`LockHandle`:
* `unlock(): void` – idempotent; may be called multiple times.
* `[Symbol.dispose]()` – same as `unlock()` enabling `using`.
### `class AsyncSharedMutex`
Wraps `SharedMutex` and runs a function while holding the mutex.
| Method | Returns | Description |
| ------ | ------- | ----------- |
| `lock(task)` | `Promise` | Run `task` exclusively. |
| `tryLock(task)` | `Promise \| null` | Immediate exclusive attempt. If acquired, runs `task`; else `null`. |
| `lockShared(task)` | `Promise` | Run `task` under a shared lock. |
| `tryLockShared(task)` | `Promise \| null` | Immediate shared attempt. |
`task` signature: `() => T | PromiseLike`
### Error handling
If `task` throws / rejects, the mutex is unlocked and the error is re-thrown. No additional wrapping.
### Ordering example
```txt
time →
S S S (queued) E (queued after those S) S S (queued after E)
|<--- overlap --->|<--- exclusive alone --->|<--- overlap --->|
```
## Patterns
Debounce writes while permitting many simultaneous reads:
```ts
const stateMtx = new AsyncSharedMutex()
let state: Data
export const readState = () => stateMtx.lockShared(() => state)
export const updateState = (patch: Partial) => stateMtx.lock(async () => {
state = { ...state, ...patch }
})
```
Attempt a fast read path that falls back to waiting if a writer is in flight:
```ts
const mtx = new SharedMutex()
export async function getSnapshot(): Promise {
const h = mtx.tryLockShared() || await mtx.lockShared()
try {
return snapshot()
}
finally {
h.unlock()
}
}
```
## Target
Modern Node / browsers, ES2022.
## Limitations / Notes
* Not reentrant – calling lock methods from inside an already held lock will deadlock your logic (no detection performed).
* Fairness beyond the described ordering is not attempted (e.g. readers arriving while a long queue of writers exists will wait until those writers finish).
* No timeout / cancellation primitive provided. Compose with `AbortController` in your tasks if required.
## Comparison
| | `SharedMutex` | `AsyncSharedMutex` |
| - | - | - |
| Style | Manual handles | Higher level task runner |
| Cleanup | Call `unlock()` / `using` | Automatic around function |
| Overhead | Slightly lower | Wrapper promise per task |
## License
MIT
---
Feel free to open issues / PRs for ideas (timeouts, cancellation helpers, metrics, etc.).