@chriscdn/promise-semaphore
Limit or throttle the simultaneous execution of asynchronous code in separate iterations of the event loop.
Installing
Using npm:
npm install @chriscdn/promise-semaphore
Using yarn:
yarn add @chriscdn/promise-semaphore
API
Create an instance
import Semaphore from "@chriscdn/promise-semaphore";
const semaphore = new Semaphore([maxConcurrent]);
The maxConcurrent
parameter is optional and defaults to 1
(making it an exclusive lock or binary semaphore). An integer greater than 1
can be used to allow multiple concurrent executions from separate iterations of the event loop.
Acquire a lock
semaphore.acquire([key]);
This returns a Promise
that resolves once a lock is acquired. The key
parameter is optional and allows the same Semaphore
instance to manage locks in different contexts. Additional details are provided in the second example.
Release a lock
semaphore.release([key]);
The release
method should be called within a finally
block (whether using promises or a try/catch
block) to ensure the lock is released.
Check if a lock can be acquired
semaphore.canAcquire([key]);
This synchronous method returns true
if a lock can be immediately acquired, and false
otherwise.
request
function
const results = await semaphore.request(fn [, key]);
This function reduces boilerplate when using acquire
and release
. It returns a promise that resolves when fn
completes. It is functionally equivalent to:
try {
await semaphore.acquire([key]);
const results = await fn();
} finally {
semaphore.release([key]);
}
requestIfAvailable
function
const results = await semaphore.requestIfAvailable(fn [, key]);
This is functionally equivalent to:
const results = semaphore.canAcquire([key])
? await semaphore.request(fn, [key])
: null;
This is useful in scenarios where only one instance of a function block should run while discarding additional attempts. For example, handling repeated button clicks.
Example 1
import Semaphore from "@chriscdn/promise-semaphore";
const semaphore = new Semaphore();
semaphore
.acquire()
.then(() => {
})
.finally(() => {
semaphore.release();
});
try {
await semaphore.acquire();
} finally {
semaphore.release();
}
await semaphore.request(() => {
});
Example 2
Consider an asynchronous function that downloads a file and saves it to disk:
const downloadAndSave = async (url) => {
const filePath = urlToFilePath(url);
if (await pathExists(filePath)) {
return filePath;
}
await downloadAndSaveToFilepath(url, filePath);
return filePath;
};
This approach works as expected until downloadAndSave()
is called multiple times with the same url
in quick succession. Without control, it could initiate simultaneous downloads that attempt to write to the same file at the same time.
This issue can be resolved by using a Semaphore
with the key
parameter:
import Semaphore from "@chriscdn/promise-semaphore";
const semaphore = new Semaphore();
const downloadAndSave = async (url) => {
try {
await semaphore.acquire(url);
const filePath = urlToFilePath(url);
if (await pathExists(filePath)) {
} else {
await downloadAndSaveToFilepath(url, filePath);
}
return filePath;
} finally {
semaphore.release(url);
}
};
The same outcome can be achieved using the request
function:
const downloadAndSave = (url) => {
return semaphore.request(async () => {
const filePath = urlToFilePath(url);
if (await pathExists(filePath)) {
return filePath;
}
await downloadAndSaveToFilepath(url, filePath);
return filePath;
}, url);
};
License
MIT