@imqueue/rpc
Version:
RPC-like client-service implementation over messaging queue
156 lines • 4.95 kB
JavaScript
;
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