UNPKG

@imqueue/rpc

Version:

RPC-like client-service implementation over messaging queue

156 lines 4.95 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.IMQLock = void 0; /** * Class IMQLock. * Implements promise-based locks. * * @example * ~~~typescript * import { IMQLock, AcquiredLock } from '.'; * * async function doSomething(): Promise<number | AcquiredLock<number>> { * const lock: AcquiredLock<number> = await IMQLock.acquire<number>('doSomething'); * * if (IMQLock.locked('doSomething')) { * // avoiding err handling in this way can cause ded-locks * // so it is good always try catch locked calls! * // BTW, IMQLock uses timeouts to avoid dead-locks * try { * // this code will be called only once per multiple async calls * // so all promises will be resolved with the same value * const res = Math.random(); * IMQLock.release('doSomething', res); * return res; * } * * catch (err) { * // release acquired locks with error * IMQLock.release('doSomething', null, err); * throw err; * } * } * * return lock; * } * * (async () => { * for (let i = 0; i < 10; ++i) { * // run doSomething() asynchronously 10 times * doSomething().then((res) => console.log(res)); * } * })(); * ~~~ */ class IMQLock { /** * Acquires a lock for a given key * * @param {string} key * @param {(...args: any[]) => any} [callback] * @param {IMQLockMetadataItem} [metadata] * @returns {AcquiredLock} */ static async acquire(key, callback, metadata) { IMQLock.queues[key] = IMQLock.queues[key] || []; if (metadata) { IMQLock.metadata[key] = metadata; } if (IMQLock.locked(key)) { return new Promise((resolve, reject) => { let timer = null; // istanbul ignore else if (IMQLock.deadlockTimeout) { // avoid dead-locks using timeouts timer = setTimeout(() => { let dumpStr = ''; try { dumpStr = JSON.stringify(IMQLock.metadata[key]); } catch (err) { dumpStr = 'Unable to stringify metadata'; } const err = new Error(`Lock timeout, "${key}" call rejected, metadata: ${dumpStr}`); clearTimeout(timer); timer = null; IMQLock.release(key, null, err); }, IMQLock.deadlockTimeout); } IMQLock.queues[key].push([ // istanbul ignore next (result) => { try { timer && clearTimeout(timer); timer = null; callback && callback(null, result); } catch (err) { IMQLock.logger.error(err); } resolve(result); }, // istanbul ignore next (err) => { try { timer && clearTimeout(timer); timer = null; callback && callback(err); } catch (e) { err = e; } reject(err); } ]); }); } IMQLock.acquiredLocks[key] = true; return true; } /** * Releases previously acquired lock for a given key * * @param {string} key * @param {T} value * @param {E} err */ static release(key, value, err) { const queue = IMQLock.queues[key]; IMQLock.queues[key] = []; delete IMQLock.acquiredLocks[key]; delete IMQLock.metadata[key]; let task; const processor = err ? 1 : 0; const arg = err ? err : value; while ((task = queue.shift())) { task[processor](arg); } } /** * Returns true if given key is locked, false otherwise * * @param {string} key * @returns {boolean} */ static locked(key) { // noinspection PointlessBooleanExpressionJS return !!IMQLock.acquiredLocks[key]; } } exports.IMQLock = IMQLock; IMQLock.acquiredLocks = {}; IMQLock.queues = {}; IMQLock.metadata = {}; /** * Deadlock timeout in milliseconds * * @type {number} */ IMQLock.deadlockTimeout = 10000; /** * Logger used to log errors which appears during locked calls * * @type {ILogger} */ IMQLock.logger = console; //# sourceMappingURL=IMQLock.js.map