@toolbuilder/semaphore
Version:
Basic semaphore and mutex with both sync and async acquire methods.
137 lines (124 loc) • 3.31 kB
JavaScript
'use strict';
/**
* @typedef {() => void} Resolver
*/
/**
* Promise based semaphore.
*/
class Semaphore {
/**
* Create a semaphore.
*
* @param {number} [max] - maximum number of locks that can be acquired at any given time
*/
constructor (max = 1) {
/** @private */
this._max = max;
/** @private */
this._active = 0;
/**
* @private
* @type {Resolver[]}
*/
this._resolvers = []; // when locked, each acquire requires a new promise
}
/**
* Returns whether a lock is available. If one is available,
* acquireSync will succeed.
* @returns {boolean} - true if a lock is available, false otherwise
*/
available () { return !(this._active >= this._max) }
/**
* Acquires a lock synchronously.
* @returns {boolean} - true if lock was acquired, false otherwise
*/
acquireSync () {
if (this._active >= this._max) return false
this._active++;
return true
}
/**
* Acquires a lock asynchronously.
* @returns {PromiseLike<void>} - promise resolves when a lock has been acquired.
*/
acquire () {
this._active++;
if (this._active > this._max) {
let resolver;
const promise = new Promise(resolve => (resolver = resolve));
this._resolvers.push(resolver);
return promise
} else {
return Promise.resolve()
}
}
/**
* Releases a lock so that it is available to be acquired.
* Each acquire or acquireSync call must be matched by exactly one release call.
* @returns {void}
*/
release () {
this._active--;
if (this._resolvers.length > 0) {
this._resolvers.shift()(); // let awaiting code run by resolving a promise
}
}
}
/**
* @function
* @param {import('./semaphore.js').Resolver} fn - function to call only once
* @returns {() => void}
*/
const once = fn => {
let alreadyCalled = false;
return () => {
if (!alreadyCalled) {
alreadyCalled = true;
fn();
}
}
};
/**
* Simple mutex class.
* @example
* const mutex = new Mutex()
* const release = await mutex.acquire()
* release() // to release mutex
*/
class Mutex {
constructor () {
/** @private */
this._semaphore = new Semaphore(1);
}
/**
* Determine if the lock is available.
* @returns {boolean} - true if lock is available, false otherwise
*/
available () { return this._semaphore.available() }
/**
* Get a lock if available.
*
* @returns {() => void} - if the lock is available, returns a
* function to release it. Otherwise returns null. The release function can
* be called multiple times, it will only release once.
* @example
* const mutex = new Mutex()
* const release = mutex.acquireSync()
* if (release) release()
*/
acquireSync () {
const release = once(() => this._semaphore.release());
return (this._semaphore.acquireSync()) ? release : null
}
/**
* Acquire a lock.
* @returns {PromiseLike<() => void>} - returns a Promise that resolves to a release function. The release
* function can be called multiple times, it will only release once.
*/
acquire () {
const release = once(() => this._semaphore.release());
return this._semaphore.acquire().then(() => release)
}
}
exports.Mutex = Mutex;
exports.Semaphore = Semaphore;