securityos/node_modules/async-lock/lib/index.js

311 lines
6.8 KiB
JavaScript

'use strict';
var AsyncLock = function (opts) {
opts = opts || {};
this.Promise = opts.Promise || Promise;
// format: {key : [fn, fn]}
// queues[key] = null indicates no job running for key
this.queues = Object.create(null);
// lock is reentrant for same domain
this.domainReentrant = opts.domainReentrant || false;
if (this.domainReentrant) {
if (typeof process === 'undefined' || typeof process.domain === 'undefined') {
throw new Error(
'Domain-reentrant locks require `process.domain` to exist. Please flip `opts.domainReentrant = false`, ' +
'use a NodeJS version that still implements Domain, or install a browser polyfill.');
}
// domain of current running func {key : fn}
this.domains = Object.create(null);
}
this.timeout = opts.timeout || AsyncLock.DEFAULT_TIMEOUT;
this.maxOccupationTime = opts.maxOccupationTime || AsyncLock.DEFAULT_MAX_OCCUPATION_TIME;
this.maxExecutionTime = opts.maxExecutionTime || AsyncLock.DEFAULT_MAX_EXECUTION_TIME;
if (opts.maxPending === Infinity || (Number.isInteger(opts.maxPending) && opts.maxPending >= 0)) {
this.maxPending = opts.maxPending;
} else {
this.maxPending = AsyncLock.DEFAULT_MAX_PENDING;
}
};
AsyncLock.DEFAULT_TIMEOUT = 0; //Never
AsyncLock.DEFAULT_MAX_OCCUPATION_TIME = 0; //Never
AsyncLock.DEFAULT_MAX_EXECUTION_TIME = 0; //Never
AsyncLock.DEFAULT_MAX_PENDING = 1000;
/**
* Acquire Locks
*
* @param {String|Array} key resource key or keys to lock
* @param {function} fn async function
* @param {function} cb callback function, otherwise will return a promise
* @param {Object} opts options
*/
AsyncLock.prototype.acquire = function (key, fn, cb, opts) {
if (Array.isArray(key)) {
return this._acquireBatch(key, fn, cb, opts);
}
if (typeof (fn) !== 'function') {
throw new Error('You must pass a function to execute');
}
// faux-deferred promise using new Promise() (as Promise.defer is deprecated)
var deferredResolve = null;
var deferredReject = null;
var deferred = null;
if (typeof (cb) !== 'function') {
opts = cb;
cb = null;
// will return a promise
deferred = new this.Promise(function(resolve, reject) {
deferredResolve = resolve;
deferredReject = reject;
});
}
opts = opts || {};
var resolved = false;
var timer = null;
var occupationTimer = null;
var executionTimer = null;
var self = this;
var done = function (locked, err, ret) {
if (occupationTimer) {
clearTimeout(occupationTimer);
occupationTimer = null;
}
if (executionTimer) {
clearTimeout(executionTimer);
executionTimer = null;
}
if (locked) {
if (!!self.queues[key] && self.queues[key].length === 0) {
delete self.queues[key];
}
if (self.domainReentrant) {
delete self.domains[key];
}
}
if (!resolved) {
if (!deferred) {
if (typeof (cb) === 'function') {
cb(err, ret);
}
}
else {
//promise mode
if (err) {
deferredReject(err);
}
else {
deferredResolve(ret);
}
}
resolved = true;
}
if (locked) {
//run next func
if (!!self.queues[key] && self.queues[key].length > 0) {
self.queues[key].shift()();
}
}
};
var exec = function (locked) {
if (resolved) { // may due to timed out
return done(locked);
}
if (timer) {
clearTimeout(timer);
timer = null;
}
if (self.domainReentrant && locked) {
self.domains[key] = process.domain;
}
var maxExecutionTime = opts.maxExecutionTime || self.maxExecutionTime;
if (maxExecutionTime) {
executionTimer = setTimeout(function () {
if (!!self.queues[key]) {
done(locked, new Error('Maximum execution time is exceeded ' + key));
}
}, maxExecutionTime);
}
// Callback mode
if (fn.length === 1) {
var called = false;
try {
fn(function (err, ret) {
if (!called) {
called = true;
done(locked, err, ret);
}
});
} catch (err) {
// catching error thrown in user function fn
if (!called) {
called = true;
done(locked, err);
}
}
}
else {
// Promise mode
self._promiseTry(function () {
return fn();
})
.then(function(ret){
done(locked, undefined, ret);
}, function(error){
done(locked, error);
});
}
};
if (self.domainReentrant && !!process.domain) {
exec = process.domain.bind(exec);
}
if (!self.queues[key]) {
self.queues[key] = [];
exec(true);
}
else if (self.domainReentrant && !!process.domain && process.domain === self.domains[key]) {
// If code is in the same domain of current running task, run it directly
// Since lock is re-enterable
exec(false);
}
else if (self.queues[key].length >= self.maxPending) {
done(false, new Error('Too many pending tasks in queue ' + key));
}
else {
var taskFn = function () {
exec(true);
};
if (opts.skipQueue) {
self.queues[key].unshift(taskFn);
} else {
self.queues[key].push(taskFn);
}
var timeout = opts.timeout || self.timeout;
if (timeout) {
timer = setTimeout(function () {
timer = null;
done(false, new Error('async-lock timed out in queue ' + key));
}, timeout);
}
}
var maxOccupationTime = opts.maxOccupationTime || self.maxOccupationTime;
if (maxOccupationTime) {
occupationTimer = setTimeout(function () {
if (!!self.queues[key]) {
done(false, new Error('Maximum occupation time is exceeded in queue ' + key));
}
}, maxOccupationTime);
}
if (deferred) {
return deferred;
}
};
/*
* Below is how this function works:
*
* Equivalent code:
* self.acquire(key1, function(cb){
* self.acquire(key2, function(cb){
* self.acquire(key3, fn, cb);
* }, cb);
* }, cb);
*
* Equivalent code:
* var fn3 = getFn(key3, fn);
* var fn2 = getFn(key2, fn3);
* var fn1 = getFn(key1, fn2);
* fn1(cb);
*/
AsyncLock.prototype._acquireBatch = function (keys, fn, cb, opts) {
if (typeof (cb) !== 'function') {
opts = cb;
cb = null;
}
var self = this;
var getFn = function (key, fn) {
return function (cb) {
self.acquire(key, fn, cb, opts);
};
};
var fnx = keys.reduceRight(function (prev, key) {
return getFn(key, prev);
}, fn);
if (typeof (cb) === 'function') {
fnx(cb);
}
else {
return new this.Promise(function (resolve, reject) {
// check for promise mode in case keys is empty array
if (fnx.length === 1) {
fnx(function (err, ret) {
if (err) {
reject(err);
}
else {
resolve(ret);
}
});
} else {
resolve(fnx());
}
});
}
};
/*
* Whether there is any running or pending asyncFunc
*
* @param {String} key
*/
AsyncLock.prototype.isBusy = function (key) {
if (!key) {
return Object.keys(this.queues).length > 0;
}
else {
return !!this.queues[key];
}
};
/**
* Promise.try() implementation to become independent of Q-specific methods
*/
AsyncLock.prototype._promiseTry = function(fn) {
try {
return this.Promise.resolve(fn());
} catch (e) {
return this.Promise.reject(e);
}
};
module.exports = AsyncLock;