UNPKG

promisesds

Version:

ES6 Promises data structures and utils

363 lines (332 loc) 11.5 kB
module.exports = function() { "use strict"; /** * Partial insertion sort function implemented to create a sorted array * of promises to evict on one pass * @param {array[Promise]} promises set of promises to scan * @param {int} nEvicted number of promises to return * @param {string} prop key name of the counter on the promise * @param {Boolean} desc To invert the sort order * @return {array[Promise]} promises to evict */ var evictionSort = function(promises, nEvicted, prop, desc){ desc = Boolean(desc); var limit; if (promises[0]) { limit = promises[0][prop]; } var evicted = []; var evictedSize = 0; Object.keys(promises).forEach(function(key){ var promise = promises[key]; if (desc === (limit <= promise[prop]) || evictedSize < nEvicted) { var i = 1; var notFilled = i < nEvicted && !evicted[i]; while (notFilled || (evicted[i] && desc === (evicted[i][prop]) <= promise[prop])) { evicted[i - 1] = evicted[i]; i += 1; notFilled = i < nEvicted && !evicted[i]; } evicted[i - 1] = {propKey: key}; evicted[i - 1][prop] = promise[prop]; evictedSize += 1; limit = evicted[0] ? evicted[0][prop] : limit; } }); return evicted; }; /** * Least recent used cache eviction implementation * @see PromiseCache::evict(int) */ var Lru = (function () { var LruCons = function () { this.counter = 0; }; LruCons.prototype.init = function (cache) { this.cache = cache; }; LruCons.prototype.set = function (_key, promise) { promise.lru = 0; }; LruCons.prototype.get = function (_key, promise) { this.counter += 1; promise.lru = this.counter; }; /** * The evict method is somewhat costly since get a set are * trivial, it finds nEvicted elements in a pass over the cache. * @param {int} nEvicted number of elements to evict from the cache */ LruCons.prototype.evict = function (nEvicted) { var cache = this.cache; var evicted = evictionSort(cache._promises, nEvicted, 'lru'); Object.keys(evicted).forEach(function (key) { cache.remove(evicted[key].propKey); }); }; return LruCons; })(); /** * Most recent used eviction algorithm * @see PromiseCache::evict() */ var Mru = (function () { var MruCons = function () { this.counter = 0; }; MruCons.prototype.init = function (cache) { this.cache = cache; }; MruCons.prototype.set = function (_key, promise) { promise.mru = 0; }; MruCons.prototype.get = function (_key, promise) { this.counter += 1; promise.mru = this.counter; }; /** * @see Lru::evict(int) * @param {int} nEvicted number of elements to evict from the cache */ MruCons.prototype.evict = function (nEvicted) { var cache = this.cache; var evicted = evictionSort(cache._promises, nEvicted, 'mru', true); Object.keys(evicted).forEach(function (key) { cache.remove(evicted[key].propKey); }); }; return MruCons; })(); /** * Least frequently used eviction algorithm * @see PromiseCache::evict() */ var Lfu = (function () { var LfuCons = function () {}; LfuCons.prototype.init = function (cache) { this.cache = cache; }; LfuCons.prototype.set = function (_key, promise) { promise.lfu = 0; }; LfuCons.prototype.get = function (_key, promise) { promise.lfu += 1; }; /** * @see Lru::evict(int) * @param {int} nEvicted number of elements to evict from the cache */ LfuCons.prototype.evict = function (nEvicted) { var cache = this.cache; var evicted = evictionSort(cache._promises, nEvicted, 'lfu'); Object.keys(evicted).forEach(function (key) { cache.remove(evicted[key].propKey); }); }; return LfuCons; })(); /** * Map from algorithms names to classes. * @type {Object} */ var algorithms = { lru: Lru, mru: Mru, lfu: Lfu }; /** * Aux function to emulate jQuery deferred functionality * @return {Object} Object with resolve and reject methods and a promise property */ var deferred = function(){ var dfr; var promise = new Promise(function(resolve, reject){ dfr = { resolve: resolve, reject: reject }; }); dfr.promise = promise; return dfr; }; var noop = function () {}; /** * The promise cache is a small cache implementation for with some features * to manage promises as failure management and expire time. * It has an eviction interface that decouples the algorithm and offers LRU, * MRU and LFU implementations. * The Deferred objects have a resolve and reject method that manages the underlying promise * @param {Object[key- > promise]} promises Initial set of promises to cache with the keys * present in the object * @param {Object} options { * eviction Object|string: eviction algorithm * ('lru', 'mru', 'lfu') or object implementing * the eviction interface @see PromiseCache::evict(int) * capacity int: Cache max number of promises, it will call * evict when full * evictRate int: Number of promises to evict when the cache * is full, it may be more efficient if the eviction algorithm * is costly. * discarded function(key, promise): optional default function * @see PromiseCache::set * expireTime int: optional default number of seconds before * the promise is removed from the cache * fail function(dfr: Deferred, key, promise): optional default * function @see PromiseCache::set * } */ var PromiseCache = function (promises, options) { options = options || {}; this._promises = {}; this.length = 0; var eviction; if (options.eviction) { if (algorithms[options.eviction]) { eviction = new algorithms[options.eviction](); } else { eviction = options.eviction; } } eviction = eviction || {}; eviction.init = eviction.init || noop; eviction.get = eviction.get || noop; eviction.set = eviction.set || noop; eviction.remove = eviction.remove || noop; if (options.capacity === undefined) { eviction.evict = eviction.evict || noop; } else if (!eviction.evict) { throw new Error('There is a capacity but no evict function set'); } this.eviction = eviction; this.eviction.init(this, promises, options); this.capacity = options.capacity; this.evictRate = options.evictRate || 1; this.discarded = options.discarded; this.expireTime = options.expireTime; this.fail = options.fail; var self = this; Object.keys(promises || {}).forEach(function(key){ self.set(key, promises[key], options); }); }; /** * Sets a promise in the cache with key and options that override the default versions, * can trigger eviction if capacity is exceeded. * @param {string} key to access the cached promise * @param {Promise} promise Promise to save in the cache * @param {Object} options { This options override the defaults available in the constructor * discarded function(key, promise): optional, function to be called * when the element is removed from the cache * expireTime int: optional, number of seconds before the promise is * removed from the cache * fail function(dfr: Deferred, key, promise): optional, if present * or the default exists a new promise will be created and set in * the cache, this promise will be succeed when the original is * resolved. * If the original promise is rejected the fail function will be * called and dfr can be used to resolve or reject the new promise * that all the users of the get method have. * This allow the setter to centralize error handling and * potentially provide transparent retries and recovery procedures * for the getters. * } */ PromiseCache.prototype.set = function (key, promise, options) { if (promise) options = options || {}; var self = this; var interceptor; if (!promise || !promise.then) { throw new Error('promise: ' + promise + ' is not a Promise'); } if (!this._promises[key]){ this.length += 1; if (this.capacity < this.length) { this.evict(this.evictRate); } } var fail = options.fail || this.fail; var promiseObj; if (fail) { var dfr = deferred(); interceptor = dfr.promise; promiseObj = { promise: interceptor }; this._promises[key] = promiseObj; promise.then(function (value) { dfr.resolve(value); }, function () { fail(dfr, key, promise); }); } else { promiseObj = { promise: promise }; this._promises[key] = promiseObj; promise.then(null, function () { if (self._promises[key] && self._promises[key] === promiseObj) { this.remove(key); } }); } this._promises[key].discarded = options.discarded || this.discarded || noop; var expireTime = options.expireTime !== undefined ? options.expireTime : this.expireTime; if (expireTime !== undefined) { setTimeout(function () { if (self._promises[key] && self._promises[key] === promiseObj) { self.remove(key); } }, expireTime); } this.eviction.set(key, promiseObj, promise, options); }; /** * Remove the key from the cache and calls the discarded callback if it exists, it is called * by the eviction algorithms when clearing the cache. * @param {string} key cache entry to remove * @return {Promise|undefined} Removed promise or undefined it it doesn't exist */ PromiseCache.prototype.remove = function (key) { var promise = this._promises[key]; if (promise !== undefined) { delete this._promises[key]; this.length -= 1; this.eviction.remove(key, promise); promise.discarded(key, promise.promise); return promise.promise; } }; /** * Retrieves the promise in the cache stored with the key * @param {string} key cache entry to retrieve * @return {Promise|undefined} Promise stored with the key or undefined if it doesn't exist */ PromiseCache.prototype.get = function (key) { if (this._promises[key]) { this.eviction.get(key, this._promises[key]); return this._promises[key].promise; } }; /** * Object containing all promises in the cache with their keys, the object is an independent * copy of the internal promises store. * @return {Object} promises */ PromiseCache.prototype.promises = function () { var cleanCopy = {}; var promises = this._promises; Object.keys(promises).forEach(function(key){ cleanCopy[key] = promises[key].promise; }); return cleanCopy; }; /** * Will remove nEvicted promises from the cache or all if larger than the number of promises * @param {int} nEvicted number of cache entries to clear */ PromiseCache.prototype.evict = function (nEvicted) { this.eviction.evict(nEvicted); }; return PromiseCache; }();