UNPKG

magazine

Version:

A collective least-recently used cache.

253 lines (219 loc) 6.38 kB
var ok = require('assert').ok, slice = [].slice function detach (cartridge) { var magazine = cartridge._magazine, cache = magazine._cache magazine.heft -= cartridge.heft cache.heft -= cartridge.heft delete cache._cache[cartridge._compoundKey] magazine.count-- cache.count-- cartridge._detached = true unlink(cartridge) } function unlink (cartridge, prefix) { if (!prefix) { unlink(cartridge, 'cache') unlink(cartridge, 'magazine') } else { var prev = '_' + prefix + 'Previous' var next = '_' + prefix + 'Next' cartridge[prev][next] = cartridge[next] cartridge[next][prev] = cartridge[prev] } } function link (cartridge, previous, prefix) { var prev = '_' + prefix + 'Previous' var next = '_' + prefix + 'Next' cartridge[next] = previous[next] cartridge[prev] = previous cartridge[next][prev] = cartridge previous[next] = cartridge } function Cache (options) { options = options || {} ok(!options.clock) this._Date = options.Date || Date var head = {} head._cachePrevious = head._cacheNext = head this._cache = {} this._head = head this._nextKey = 0 this.count = 0 this.holds = 0 this.heft = 0 } Cache.prototype.createMagazine = function () { var key = ':' + (++this._nextKey) + ':' return new Magazine(this, key) } Cache.prototype.purge = function () { return purge.call(this, '_cachePrevious', slice.call(arguments)) } Cache.prototype.expire = function (expired) { expire(this, expired) } function Magazine (cache, key) { var head = { key: null, when: null, _terminal: true } head._magazinePrevious = head._magazineNext = head this._cache = cache this._key = key this._head = head this.count = 0 this.holds = 0 this.heft = 0 } Magazine.prototype.hold = function (key, initializer) { // TODO Remove once you're certain you're not using function initializers. ok(typeof initializer != 'function') var compoundKey = this._key + key var cartridge = this._cache._cache[compoundKey] if (!cartridge) { cartridge = this._cache._cache[compoundKey] = new Cartridge(this, key, initializer, compoundKey) this.count++ this._cache.count++ } else { unlink(cartridge) } link(cartridge, this._cache._head, 'cache') link(cartridge, this._head, 'magazine') cartridge.holds++ this.holds++ this._cache.holds++ cartridge.when = this._cache._Date.now() return cartridge } function expire (collection, expired) { var purge = collection.purge() while (purge.cartridge && purge.cartridge.when <= expired) { purge.cartridge.remove() purge.next() } purge.release() } // TODO Legacy, do not document, use expire or iterator interface. function purge (next, vargs) { var downTo, condition, gather, stop, head, iterator, cache, magazine if (vargs.length == 0) { return new Purge(this, next) } downTo = vargs.shift() if (typeof vargs[0] == 'function') { condition = vargs.shift() } if (Array.isArray(vargs[0])) { gather = vargs.shift() } condition || (condition = function () { return true }) stop = downTo head = this._head iterator = head if (typeof downTo == 'number') { downTo = Math.max(downTo, -1) stop = function () { return this.heft <= downTo }.bind(this) } while (iterator[next] !== head && !stop(iterator[next])) { var cartridge = iterator[next] if (!cartridge.holds && condition(cartridge)) { magazine = cartridge._magazine, cache = magazine._cache if (gather) { gather.push(cartridge.value) } detach(cartridge) } else { iterator = cartridge } } } Magazine.prototype.purge = function () { return purge.call(this, '_magazinePrevious', slice.call(arguments)) } Magazine.prototype.expire = function (expired) { expire(this, expired) } function Cartridge (magazine, key, value, compoundKey) { this.key = key this.value = value this._magazine = magazine this._compoundKey = compoundKey this._terminal = false this.heft = 0 this.holds = 0 } Cartridge.prototype.adjustHeft = function (heft) { this.heft += heft this._magazine.heft += heft this._magazine._cache.heft += heft return this } Cartridge.prototype.release = function () { if (!this.holds) { throw new Error('attempt to release a cartridge not held') } if (--this.holds == 0) { this._magazine.holds-- this._magazine._cache.holds-- } } Cartridge.prototype.remove = function () { if (this.holds != 1) { throw new Error('attempt to remove cartridge held by others') } detach(this) } function Purge (object, next) { this._object = object this._next = next this.cartridge = this._object._head this.next() } Purge.prototype.next = function () { do { this.cartridge = this.cartridge[this._next] === this._object._head ? null : this.cartridge[this._next] } while (this.cartridge && this.cartridge.holds) if (this.cartridge) { this.cartridge.holds = 1 } } Purge.prototype.release = function () { if (this.cartridge) { this.cartridge.release() } } var NULL = {} Magazine.prototype.get = function (key, value) { var cartridge, got if (arguments.length == 2) { cartridge = this.hold(key, value) got = cartridge.value cartridge.release() } else { cartridge = this.hold(key, NULL) if (cartridge.value === NULL) { cartridge.remove() } else { got = cartridge.value cartridge.release() } } return got } Magazine.prototype.put = function (key, value) { var cartridge = this.hold(key, NULL), got if (cartridge.value !== NULL) { got = cartridge.value } cartridge.value = value cartridge.release() return got } Magazine.prototype.remove = function (key) { var cartridge = this.hold(key, NULL), got if (cartridge.value !== NULL) { got = cartridge.value } cartridge.remove() return got } module.exports = Cache