protomux-rpc-client
Version:
Connect to protomux-rpc servers
93 lines (77 loc) • 2.23 kB
JavaScript
const Signal = require('signal-promise')
class ConcurrentLimiterError extends Error {
static NEVER_PROMISE = new Promise(() => {})
constructor (msg, code, fn = ConcurrentLimiterError) {
super(`${code}: ${msg}`)
this.code = code
if (Error.captureStackTrace) {
Error.captureStackTrace(this, fn)
}
}
get name () {
return 'ConcurrentLimiterError'
}
static CONCURRENT_LIMITER_DESTROYED () {
return new ConcurrentLimiterError(
'The concurrent limiter is destroyed',
'CONCURRENT_LIMITER_DESTROYED',
ConcurrentLimiterError.CONCURRENT_LIMITER_DESTROYED
)
}
}
module.exports = class ConcurrentLimiter {
/**
* @param {object} options
* @param {number} options.maxConcurrent - Maximum concurrent executions.
*/
constructor ({ maxConcurrent } = {}) {
this.maxConcurrent = maxConcurrent
this.active = 0
this._releaseSignal = new Signal()
this.destroyed = false
}
_tryAcquire () {
if (this.active < this.maxConcurrent) {
this.active++
return true
}
return false
}
_release () {
this.active--
this._releaseSignal.notify()
}
/**
* Execute an async function with a timeout.
*
* @template T
* @param {() => Promise<T>} fn
* @param {object} [options] - Options for the execution.
* @param {Promise<void>} [options.abortSignalPromise] - Promise that rejects when the execution should be aborted.
* @returns {Promise<T>}
*/
async execute (fn, { abortSignalPromise = ConcurrentLimiterError.NEVER_PROMISE } = {}) {
while (!this._tryAcquire()) {
if (this.destroyed) {
throw ConcurrentLimiterError.CONCURRENT_LIMITER_DESTROYED()
}
await Promise.race([this._releaseSignal.wait(), abortSignalPromise])
}
if (this.destroyed) {
throw ConcurrentLimiterError.CONCURRENT_LIMITER_DESTROYED()
}
try {
return await fn()
} finally {
this._release()
}
}
destroy () {
if (this.destroyed) {
throw ConcurrentLimiterError.CONCURRENT_LIMITER_DESTROYED()
}
this.destroyed = true
// notify any waiting acquire calls so the calling function can fail gracefully
this._releaseSignal.notify()
}
}