UNPKG

custom-idle-queue

Version:

Optimize the speed of important tasks on limited ressources

264 lines (224 loc) 6.82 kB
/** * Creates a new Idle-Queue * @constructor * @param {number} [parallels=1] amount of parrallel runs of the limited-ressource */ export const IdleQueue = function (parallels = 1) { this._parallels = parallels || 1; /** * _queueCounter * each lock() increased this number * each unlock() decreases this number * If _qC==0, the state is in idle * @type {Number} */ this._qC = 0; /** * _idleCalls * contains all promises that where added via requestIdlePromise() * and not have been resolved * @type {Set<Promise>} _iC with oldest promise first */ this._iC = new Set(); /** * _lastHandleNumber * @type {Number} */ this._lHN = 0; /** * _handlePromiseMap * Contains the handleNumber on the left * And the assigned promise on the right. * This is stored so you can use cancelIdleCallback(handleNumber) * to stop executing the callback. * @type {Map<Number><Promise>} */ this._hPM = new Map(); this._pHM = new Map(); // _promiseHandleMap }; IdleQueue.prototype = { isIdle() { return this._qC < this._parallels; }, /** * creates a lock in the queue * and returns an unlock-function to remove the lock from the queue * @return {function} unlock function than must be called afterwards */ lock() { this._qC++; }, unlock() { this._qC--; _tryIdleCall(this); }, /** * wraps a function with lock/unlock and runs it * @param {function} fun * @return {Promise<any>} */ wrapCall(fun) { this.lock(); let maybePromise; try { maybePromise = fun(); } catch (err) { this.unlock(); throw err; } if (!maybePromise.then || typeof maybePromise.then !== 'function') { // no promise this.unlock(); return maybePromise; } else { // promise return maybePromise .then(ret => { // sucessfull -> unlock before return this.unlock(); return ret; }) .catch(err => { // not sucessfull -> unlock before throwing this.unlock(); throw err; }); } }, /** * does the same as requestIdleCallback() but uses promises instead of the callback * @param {{timeout?: number}} options like timeout * @return {Promise<void>} promise that resolves when the database is in idle-mode */ requestIdlePromise(options) { options = options || {}; let resolve; const prom = new Promise(res => resolve = res); const resolveFromOutside = () => { _removeIdlePromise(this, prom); resolve(); }; prom._manRes = resolveFromOutside; if (options.timeout) { // if timeout has passed, resolve promise even if not idle const timeoutObj = setTimeout(() => { prom._manRes(); }, options.timeout); prom._timeoutObj = timeoutObj; } this._iC.add(prom); _tryIdleCall(this); return prom; }, /** * remove the promise so it will never be resolved * @param {Promise} promise from requestIdlePromise() * @return {void} */ cancelIdlePromise(promise) { _removeIdlePromise(this, promise); }, /** * api equal to * @link https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback * @param {Function} callback * @param {options} options [description] * @return {number} handle which can be used with cancelIdleCallback() */ requestIdleCallback(callback, options) { const handle = this._lHN++; const promise = this.requestIdlePromise(options); this._hPM.set(handle, promise); this._pHM.set(promise, handle); promise.then(() => callback()); return handle; }, /** * API equal to * @link https://developer.mozilla.org/en-US/docs/Web/API/Window/cancelIdleCallback * @param {number} handle returned from requestIdleCallback() * @return {void} */ cancelIdleCallback(handle) { const promise = this._hPM.get(handle); this.cancelIdlePromise(promise); }, /** * clears and resets everything * @return {void} */ clear() { // remove all non-cleared this._iC .forEach(promise => _removeIdlePromise(this, promise)); this._qC = 0; this._iC.clear(); this._hPM = new Map(); this._pHM = new Map(); } }; /** * processes the oldest call of the idleCalls-queue * @return {Promise<void>} */ function _resolveOneIdleCall(idleQueue) { if (idleQueue._iC.size === 0) return; const iterator = idleQueue._iC.values(); const oldestPromise = iterator.next().value; oldestPromise._manRes(); // try to call the next tick setTimeout(() => _tryIdleCall(idleQueue), 0); } /** * removes the promise from the queue and maps and also its corresponding handle-number * @param {Promise} promise from requestIdlePromise() * @return {void} */ function _removeIdlePromise(idleQueue, promise) { if (!promise) return; // remove timeout if exists if (promise._timeoutObj) clearTimeout(promise._timeoutObj); // remove handle-nr if exists if (idleQueue._pHM.has(promise)) { const handle = idleQueue._pHM.get(promise); idleQueue._hPM.delete(handle); idleQueue._pHM.delete(promise); } // remove from queue idleQueue._iC.delete(promise); } /** * resolves the last entry of this._iC * but only if the queue is empty * @return {Promise} */ function _tryIdleCall(idleQueue) { // ensure this does not run in parallel if (idleQueue._tryIR || idleQueue._iC.size === 0) return; idleQueue._tryIR = true; // w8 one tick setTimeout(() => { // check if queue empty if (!idleQueue.isIdle()) { idleQueue._tryIR = false; return; } /** * wait 1 tick here * because many functions do IO->CPU->IO * which means the queue is empty for a short time * but the ressource is not idle */ setTimeout(() => { // check if queue still empty if (!idleQueue.isIdle()) { idleQueue._tryIR = false; return; } // ressource is idle _resolveOneIdleCall(idleQueue); idleQueue._tryIR = false; }, 0); }, 0); }