orange-orm
Version:
Object Relational Mapper
603 lines (535 loc) • 15.4 kB
JavaScript
/* eslint-disable @typescript-eslint/no-this-alias */
/* @ts-nocheck */
/**
* A helper function to schedule a callback in a cross-platform manner:
* - Uses setImmediate if available (Node).
* - Else uses queueMicrotask if available (Deno, modern browsers).
* - Else falls back to setTimeout(fn, 0).
*/
function queueTask(fn) {
if (typeof setImmediate === 'function') {
setImmediate(fn);
}
else if
(typeof queueMicrotask === 'function') {
queueMicrotask(fn);
} else {
setTimeout(fn, 0);
}
}
/**
* @class
* @private
*/
function PriorityQueue(size) {
if (!(this instanceof PriorityQueue)) {
return new PriorityQueue(size);
}
this._size = Math.max(+size | 0, 1);
this._slots = [];
this._total = null;
// initialize arrays to hold queue elements
for (let i = 0; i < this._size; i += 1) {
this._slots.push([]);
}
}
PriorityQueue.prototype.size = function size() {
if (this._total === null) {
this._total = 0;
for (let i = 0; i < this._size; i += 1) {
this._total += this._slots[i].length;
}
}
return this._total;
};
PriorityQueue.prototype.enqueue = function enqueue(obj, priority) {
// Convert to integer with a default value of 0.
priority = priority && +priority | 0 || 0;
this._total = null;
if (priority < 0 || priority >= this._size) {
console.error(
'invalid priority: ' + priority + ' must be between 0 and ' + (this._size - 1)
);
priority = this._size - 1; // put obj at the end of the line
}
this._slots[priority].push(obj);
};
PriorityQueue.prototype.dequeue = function dequeue() {
let obj = null;
this._total = null;
for (let i = 0, sl = this._slots.length; i < sl; i += 1) {
if (this._slots[i].length) {
obj = this._slots[i].shift();
break;
}
}
return obj;
};
function doWhileAsync(conditionFn, iterateFn, callbackFn) {
const next = function() {
if (conditionFn()) {
iterateFn(next);
} else {
callbackFn();
}
};
next();
}
/**
* Generate an Object pool with a specified `factory`.
*
* @class
* @param {Object} factory
* Factory to be used for generating and destroying the items.
* @param {String} factory.name
* @param {Function} factory.create
* @param {Function} factory.destroy
* @param {Function} factory.validate
* @param {Function} factory.validateAsync
* @param {Number} factory.max
* @param {Number} factory.min
* @param {Number} factory.idleTimeoutMillis
* @param {Number} factory.reapIntervalMillis
* @param {Boolean|Function} factory.log
* @param {Number} factory.priorityRange
* @param {Boolean} factory.refreshIdle
* @param {Boolean} [factory.returnToHead=false]
*/
function Pool(factory) {
if (!(this instanceof Pool)) {
return new Pool(factory);
}
if (factory.validate && factory.validateAsync) {
throw new Error('Only one of validate or validateAsync may be specified');
}
// defaults
factory.idleTimeoutMillis = factory.idleTimeoutMillis || 30000;
factory.returnToHead = factory.returnToHead || false;
factory.refreshIdle = ('refreshIdle' in factory) ? factory.refreshIdle : true;
factory.reapInterval = factory.reapIntervalMillis || 1000;
factory.priorityRange = factory.priorityRange || 1;
factory.validate = factory.validate || function() {
return true;
};
factory.max = parseInt(factory.max, 10);
factory.min = parseInt(factory.min, 10);
factory.max = Math.max(isNaN(factory.max) ? 1 : factory.max, 1);
factory.min = Math.min(isNaN(factory.min) ? 0 : factory.min, factory.max - 1);
this._factory = factory;
this._inUseObjects = [];
this._draining = false;
this._waitingClients = new PriorityQueue(factory.priorityRange);
this._availableObjects = [];
this._asyncTestObjects = [];
this._count = 0;
this._removeIdleTimer = null;
this._removeIdleScheduled = false;
// create initial resources (if factory.min > 0)
this._ensureMinimum();
}
/**
* logs to console or user-defined log function
* @private
* @param {string} str
* @param {string} level
*/
Pool.prototype._log = function _log(str, level) {
if (typeof this._factory.log === 'function') {
this._factory.log(str, level);
} else if (this._factory.log) {
console.log(level.toUpperCase() + ' pool ' + this._factory.name + ' - ' + str);
}
};
/**
* Request the client to be destroyed. The factory's destroy handler
* will also be called.
*
* This should be called within an acquire() block as an alternative to release().
*
* @param {Object} obj
* The acquired item to be destroyed.
* @param {Function} [cb]
* Optional. Callback invoked after client is destroyed
*/
Pool.prototype.destroy = function destroy(obj, cb) {
this._count -= 1;
if (this._count < 0) this._count = 0;
this._availableObjects = this._availableObjects.filter(
(objWithTimeout) => objWithTimeout.obj !== obj
);
this._inUseObjects = this._inUseObjects.filter(
(objInUse) => objInUse !== obj
);
this._factory.destroy(obj, cb);
// keep compatibility with old interface
if (this._factory.destroy.length === 1 && cb && typeof cb === 'function') {
cb();
}
this._ensureMinimum();
};
/**
* Checks and removes the available (idle) clients that have timed out.
* @private
*/
Pool.prototype._removeIdle = function _removeIdle() {
const now = new Date().getTime();
const refreshIdle = this._factory.refreshIdle;
const maxRemovable = this._count - this._factory.min;
const toRemove = [];
this._removeIdleScheduled = false;
for (let i = 0; i < this._availableObjects.length; i++) {
const objWithTimeout = this._availableObjects[i];
if (
now >= objWithTimeout.timeout &&
(refreshIdle || toRemove.length < maxRemovable)
) {
this._log(
'removeIdle() destroying obj - now:' +
now +
' timeout:' +
objWithTimeout.timeout,
'verbose'
);
toRemove.push(objWithTimeout.obj);
}
}
toRemove.forEach((obj) => this.destroy(obj));
if (this._availableObjects.length > 0) {
this._log(
'this._availableObjects.length=' + this._availableObjects.length,
'verbose'
);
this._scheduleRemoveIdle();
} else {
this._log('removeIdle() all objects removed', 'verbose');
}
};
/**
* Schedule removal of idle items in the pool.
*
* More schedules cannot run concurrently.
*/
Pool.prototype._scheduleRemoveIdle = function _scheduleRemoveIdle() {
if (!this._removeIdleScheduled) {
this._removeIdleScheduled = true;
this._removeIdleTimer = setTimeout(() => {
this._removeIdle();
}, this._factory.reapInterval);
}
};
/**
* Try to get a new client to work, and clean up pool unused (idle) items.
*
* - If there are available clients waiting, shift the first one out,
* and call its callback.
* - If there are no waiting clients, try to create one if it won't exceed
* the maximum number of clients.
* - If creating a new client would exceed the maximum, add the client to
* the wait list.
* @private
*/
Pool.prototype._dispense = function _dispense() {
const waitingCount = this._waitingClients.size();
this._log(
'dispense() clients=' +
waitingCount +
' available=' +
this._availableObjects.length,
'info'
);
if (waitingCount < 1) {
return;
}
if (this._factory.validateAsync) {
doWhileAsync(
() => this._availableObjects.length > 0,
this._createAsyncValidator(),
() => {
if (this._count < this._factory.max) {
this._createResource();
}
}
);
return;
}
while (this._availableObjects.length > 0) {
this._log('dispense() - reusing obj', 'verbose');
const objWithTimeout = this._availableObjects[0];
if (!this._factory.validate(objWithTimeout.obj)) {
this.destroy(objWithTimeout.obj);
continue;
}
this._availableObjects.shift();
this._inUseObjects.push(objWithTimeout.obj);
const clientCb = this._waitingClients.dequeue();
return clientCb(null, objWithTimeout.obj);
}
if (this._count < this._factory.max) {
this._createResource();
}
};
Pool.prototype._createAsyncValidator = function _createAsyncValidator() {
return (next) => {
this._log('dispense() - reusing obj', 'verbose');
const objWithTimeout = this._availableObjects.shift();
this._asyncTestObjects.push(objWithTimeout);
this._factory.validateAsync(objWithTimeout.obj, (valid) => {
const pos = this._asyncTestObjects.indexOf(objWithTimeout);
this._asyncTestObjects.splice(pos, 1);
if (!valid) {
this.destroy(objWithTimeout.obj);
return next();
}
if (this._waitingClients.size() < 1) {
// no longer anyone waiting for a resource
this._addResourceToAvailableObjects(objWithTimeout.obj);
return;
}
this._inUseObjects.push(objWithTimeout.obj);
const clientCb = this._waitingClients.dequeue();
clientCb(null, objWithTimeout.obj);
});
};
};
/**
* @private
*/
Pool.prototype._createResource = function _createResource() {
this._count += 1;
this._log(
'createResource() - creating obj - count=' +
this._count +
' min=' +
this._factory.min +
' max=' +
this._factory.max,
'verbose'
);
this._factory.create((...args) => {
let err, obj;
if (args.length > 1) {
[err, obj] = args;
} else {
err = args[0] instanceof Error ? args[0] : null;
obj = args[0] instanceof Error ? null : args[0];
}
const clientCb = this._waitingClients.dequeue();
if (err) {
this._count -= 1;
if (this._count < 0) this._count = 0;
if (clientCb) {
clientCb(err, obj);
}
// queueTask to simulate process.nextTick
queueTask(() => {
this._dispense();
});
} else {
this._inUseObjects.push(obj);
if (clientCb) {
clientCb(null, obj);
} else {
this._addResourceToAvailableObjects(obj);
}
}
});
};
Pool.prototype._addResourceToAvailableObjects = function(obj) {
const objWithTimeout = {
obj,
timeout: new Date().getTime() + this._factory.idleTimeoutMillis,
};
if (this._factory.returnToHead) {
this._availableObjects.unshift(objWithTimeout);
} else {
this._availableObjects.push(objWithTimeout);
}
this._dispense();
this._scheduleRemoveIdle();
};
/**
* @private
*/
Pool.prototype._ensureMinimum = function _ensureMinimum() {
if (!this._draining && this._count < this._factory.min) {
const diff = this._factory.min - this._count;
for (let i = 0; i < diff; i++) {
this._createResource();
}
}
};
/**
* Request a new client. The callback will be called
* when a new client is available.
*
* @param {Function} callback
* @param {Number} [priority]
* @returns {Boolean} true if the pool is not fully utilized, false otherwise
*/
Pool.prototype.acquire = function acquire(callback, priority) {
if (this._draining) {
throw new Error('pool is draining and cannot accept work');
}
this._waitingClients.enqueue(callback, priority);
this._dispense();
return this._count < this._factory.max;
};
/**
* @deprecated
*/
Pool.prototype.borrow = function borrow(callback, priority) {
this._log('borrow() is deprecated. use acquire() instead', 'warn');
return this.acquire(callback, priority);
};
/**
* Return the client to the pool, indicating it is no longer needed.
*
* @param {Object} obj
*/
Pool.prototype.release = function release(obj) {
// Check whether this object has already been released
const alreadyReleased = this._availableObjects.some(o => o.obj === obj);
if (alreadyReleased) {
this._log(
'release called twice for the same resource: ' + new Error().stack,
'error'
);
return;
}
// remove from in-use list
const index = this._inUseObjects.indexOf(obj);
if (index < 0) {
this._log(
'attempt to release an invalid resource: ' + new Error().stack,
'error'
);
return;
}
this._inUseObjects.splice(index, 1);
this._addResourceToAvailableObjects(obj);
};
/**
* @deprecated
*/
Pool.prototype.returnToPool = function returnToPool(obj) {
this._log('returnToPool() is deprecated. use release() instead', 'warn');
this.release(obj);
};
function invoke(cb) {
queueTask(cb);
}
/**
* Disallow any new requests and let the request backlog dissipate.
*
* @param {Function} [callback]
* Callback invoked when all work is done and all clients have been released.
*/
Pool.prototype.drain = function drain(callback) {
this._log('draining', 'info');
this._draining = true;
const check = () => {
if (this._waitingClients.size() > 0) {
// wait until all client requests have been satisfied
return setTimeout(check, 100);
}
if (this._asyncTestObjects.length > 0) {
// wait until async validations are done
return setTimeout(check, 100);
}
if (this._availableObjects.length !== this._count) {
// wait until in-use objects have been released
return setTimeout(check, 100);
}
if (callback) {
invoke(callback);
}
};
check();
};
/**
* Forcibly destroys all clients regardless of timeout.
* Does not prevent creation of new clients from subsequent calls to acquire.
*
* If factory.min > 0, the pool will destroy all idle resources
* but replace them with newly created resources up to factory.min.
* If this is not desired, set factory.min to zero before calling.
*
* @param {Function} [callback]
* Invoked after all existing clients are destroyed.
*/
Pool.prototype.destroyAllNow = function destroyAllNow(callback) {
this._log('force destroying all objects', 'info');
const willDie = this._availableObjects;
this._availableObjects = [];
const todo = willDie.length;
let done = 0;
this._removeIdleScheduled = false;
clearTimeout(this._removeIdleTimer);
if (todo === 0 && callback) {
invoke(callback);
return;
}
while (willDie.length > 0) {
const { obj } = willDie.shift();
this.destroy(obj, () => {
done += 1;
if (done === todo && callback) {
invoke(callback);
}
});
}
};
/**
* Decorates a function to use an acquired client from the pool when called.
*
* @param {Function} decorated
* @param {Number} [priority]
*/
Pool.prototype.pooled = function pooled(decorated, priority) {
return (...args) => {
const callerCallback = args[args.length - 1];
const callerHasCallback = typeof callerCallback === 'function';
this.acquire((err, client) => {
if (err) {
if (callerHasCallback) {
callerCallback(err);
}
return;
}
// We pass everything except the user's final callback
const invokeArgs = [client].concat(
args.slice(0, callerHasCallback ? -1 : undefined)
);
// then the final callback after we release the resource
invokeArgs.push((...cbArgs) => {
this.release(client);
if (callerHasCallback) {
callerCallback(...cbArgs);
}
});
decorated(...invokeArgs);
}, priority);
};
};
Pool.prototype.getPoolSize = function getPoolSize() {
return this._count;
};
Pool.prototype.getName = function getName() {
return this._factory.name;
};
Pool.prototype.availableObjectsCount = function availableObjectsCount() {
return this._availableObjects.length;
};
Pool.prototype.inUseObjectsCount = function inUseObjectsCount() {
return this._inUseObjects.length;
};
Pool.prototype.waitingClientsCount = function waitingClientsCount() {
return this._waitingClients.size();
};
Pool.prototype.getMaxPoolSize = function getMaxPoolSize() {
return this._factory.max;
};
Pool.prototype.getMinPoolSize = function getMinPoolSize() {
return this._factory.min;
};
module.exports = { Pool };