Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add withSemaphore & withMutex function #331

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/next-minor.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ The `####` headline should be short and descriptive of the new functionality. In

## New Functions

####
#### Add `withMutex`

## New Features
https://github.com/radashi-org/radashi/pull/331

####
#### Add `withSemaphore`

https://github.com/radashi-org/radashi/pull/331
52 changes: 52 additions & 0 deletions docs/async/withMutex.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: withSemaphore
description: A synchronization primitive for limiting concurrent usage to one
since: 12.3.0
---

### Usage

Creates a mutex-protected async function that limits concurrent execution to a single use at a time. Additional calls will wait for the mutex to be released before executing.

```ts
import * as _ from 'radashi'

const exclusiveFn = _.withMutex(async () => {
// Do stuff
})

exclusiveFn() // run immediatly
exclusiveFn() // wait until mutex is released
```

#### Using with task based functions

Execution function can be passed as a task parameter and ignored at the mutex creation.

```ts
import * as _ from 'radashi'

const exclusiveFn = _.withMutex()

exclusiveFn(async permit => {
// Do stuff
})
```

### Manual lock management

The mutex can be manually acquired and released for fine-grained control over locking behavior.

```ts
import * as _ from 'radashi'

const mutex = _.withMutex()

const permit = await mutex.acquire()
mutex.isLocked() // true

// Do stuff

permit.release()
mutex.isLocked() // false
```
66 changes: 66 additions & 0 deletions docs/async/withSemaphore.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: withSemaphore
description: A synchronization primitive for limiting concurrent usage
since: 12.3.0
---

### Usage

Creates a [semaphore-protected](<https://en.wikipedia.org/wiki/Semaphore_(programming)>) async function that limits concurrent execution to a specified number of active uses.
Additional calls will wait for previous executions to complete.

```ts
import * as _ from 'radashi'

const exclusiveFn = _.withSemaphore(2, async () => {
// Do stuff
})

exclusiveFn() // run immediatly
exclusiveFn() // run immediatly
exclusiveFn() // wait until semaphore is released
```

#### Using with task based functions

Execution function can be passed as a task parameter and ignored at the semaphore creation.

```ts
import * as _ from 'radashi'

const exclusiveFn = _.withSemaphore({ capacity: 2 })

exclusiveFn(async permit => {
// Do stuff
})
```

#### Weighted tasks

Each task can require a specific weight from a semaphore. In this example two tasks each weighted with 2 from
a semaphore with a capacity of 2. As a result they are mutually exclusive.

```ts
import * as _ from 'radashi'

const exclusiveFn = _.withSemaphore({ capacity: 2 })

exclusiveFn({ weight: 2 }, async permit => {}) // run immediatly
exclusiveFn({ weight: 2 }, async permit => {}) // wait until semaphore is released
```

### Manual lock management

The semaphore can be manually acquired and released for fine-grained control over locking behavior.

```ts
import * as _ from 'radashi'

const semaphore = _.withSemaphore({ capacity: 2 })

const permit = await semaphore.acquire()

// Do stuff

permit.release()
```
58 changes: 58 additions & 0 deletions src/async/withMutex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { withSemaphore, type SemaphorePermit } from 'radashi'

type AnyFn<T = unknown> = (permit: SemaphorePermit) => Promise<T>

export interface Mutex {
isLocked(): boolean
acquire(): Promise<SemaphorePermit>
release(): void
}

/**
* Creates a mutex-protected instance with supplied function that limits concurrent execution to a single active use.
*
* @see https://radashi.js.org/reference/async/withSemaphore
* @example
* ```ts
* const limitedFn = withMutex()
* limitedFn(() => ...)
* ```
*/
export function withMutex(): ExclusiveFn

/**
* Creates a mutex-protected instance with supplied function that limits concurrent execution to a single active use.
* Supports direct invocation and dynamic function passing.
*
* @see https://radashi.js.org/reference/async/withMutex
* @example
* ```ts
* const limitedFn = withMutex(() => ...)
* limitedFn()
* limitedFn(() => ...)
Comment on lines +30 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This usage is not supported by the current type definitions. I assume we'll want to update this example, rather than allow overriding the base function?

* ```
*/
export function withMutex<T>(fn: AnyFn<T>): PrebuiltExclusiveFn<T>
export function withMutex(baseFn?: AnyFn): PrebuiltExclusiveFn<unknown> {
// @ts-expect-error because baseFn is not optional
const semaphore = withSemaphore({ capacity: 1 }, baseFn)

async function runExclusive(innerFn?: AnyFn): Promise<unknown> {
// @ts-expect-error because innerFn is not optional
return semaphore({ weight: 1 }, innerFn)
}

runExclusive.isLocked = () => semaphore.getRunning() > 0
runExclusive.acquire = () => semaphore.acquire(1)
runExclusive.release = () => semaphore.release(1)

return runExclusive
}

interface ExclusiveFn extends Mutex {
<T>(fn: AnyFn<T>): Promise<T>
}

interface PrebuiltExclusiveFn<T> extends ExclusiveFn {
(): Promise<T>
}
Loading
Loading