fwk
Version:
Simple JS framework for node.js
333 lines (301 loc) • 9.76 kB
JavaScript
// Copyright Teleportd
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
var base = require('./base.js');
/**
* cache.js
*
* a cache implementation for javascript uses LRU as default algorithm to evict
* elements from the cache when it is full.
* The cache does not expose a set method for caching value, to cache a value use the
* get function wich take as a parameter a closure (options.getter) to fetch the value
*
* cache.get(key, { getter: getter,
* evict: evict,
* timeout: 2000 }, cb); // cb(err, value)
*
* These are the `getter` params:
* @param key to identify the value in the cache
* @param cb cb(error,value)
* Example:
* var getter = function(key, cb) {
* // retrieves the value as value
* cb(null, value);
* };
*
* The `evict` function takes the value as parameter. Only the evict passed with the
* getter that has been used to retrieve the value is used at eviction. All subsequent
* evict passed when retrieving a cached value are *not* called.
* These are the `evict` params:
* @param key the evicted key
* @param value the evicted value
*
* If a value is not in the cache and there is many conccurent get hits for it,
* the closure is fired only once. That allows avoinding reddundant calls.
*
* The structure of a cache element is the following:
* { date: 123456789, // last access date to this cached element
* [value: ...,] // the cached value
* timeout: 123456789, // date at which to invalidate this cache element
* queue: [] } // the queue of callbacks, empty if already cached
*
*
* @extends {}
*
* @param spec {[size] [, interval] [, evict]}
*/
var cache = function(spec, my) {
my = my || {};
var _super = {};
my.size = spec.size || 1000;
my.interval = spec.interval || 100;
my.cache = {};
my.evict = spec.evict || 'LRU';
my.modes = { LRU: 0,
MRU: 1,
RR: 2};
//public
var get; /* get(key [, options], cb); */
var invalidate; /* invalidate(key); */
var count; /* count(); */
//private
var runloop;
var evict = [];
var that = {};
/**
* List of available eviction algorithms. The algorithms used is driven
* by the current mode: 'LRU', 'MRU', or 'RR'
* @param count the current cache size
*/
//LRU (Least Recently Used) algo
evict[0] = function(count) {
while(count > my.size ) {
var min = Date.now();
var evct;
for(var s in my.cache) {
if(my.cache.hasOwnProperty(s)) {
if(typeof my.cache[s].value !== 'undefined') {
if(my.cache[s].date < min) {
min = my.cache[s].date;
evct = s;
}
}
}
}
count--;
if(typeof evct !== 'undefined') {
invalidate(evct);
}
}
};
//MRU (Most Recently Used)
evict[1] = function(count) {
while(count > my.size) {
var max = 0;
var evct;
for(var s in my.cache) {
if(my.cache.hasOwnProperty(s)) {
if(typeof mycache[s].value !== 'undefined') {
if(my.cache[s].date > max) {
max = my.cache[s].date;
evct = s;
}
}
}
}
count--;
if(typeof evct !== 'undefined')
invalidate(evct);
}
};
//RR (Random and Replacement)
evict[2] = function(count) {
while(count > my.size) {
var index = Math.floor(count * Math.random());
var evct;
for(var s in my.cache) {
if(my.cache.hasOwnProperty(s)) {
if(typeof mycache[s].value !== 'undefined') {
if(count == index)
evct = s;
}
}
}
count--;
if(typeof evct !== 'undefined')
invalidate(evct);
}
};
/**
* This is the main cache function that lets user retrieve cached
* values and set new value in the cache through the getter option.
* @param key to identify the cached value
* @param options {getter, evict, timeout}
* getter: closure for getting the value to be cached
* evict: called when the value is evicted
* (the one passed when getter is used)
* timeout: delay before invalidating the value
* @param cb(err, value) the async callback to retrieve the value
*/
get = function(key, options, cb) {
if(typeof key !== 'string') return;
if(typeof my.cache[key] !== 'undefined') {
if(typeof my.cache[key].value !== 'undefined') {
my.cache[key].date = Date.now();
// returns the value
cb(null, my.cache[key].value);
}
else {
my.cache[key].queue.push(cb);
}
// timeout comparison
if(typeof options.timeout === 'number') {
var t = Date.now() + options.timeout;
if(typeof my.cache[key].timeout === 'undefined' ||
my.cache[key].timeout > t) {
my.cache[key].timeout = t;
}
}
// we don't update evict here since the value is cached
// only the evict passed with the getter that has been
// used is called
}
else if(typeof options.getter === 'function') {
my.cache[key] = { queue: [cb] };
// timeout setting
if(typeof options.timeout === 'number') {
my.cache[key].timeout = Date.now() + options.timeout;
}
// evict setting
if(typeof options.evict === 'function') {
my.cache[key].evict = options.evict;
}
// getter asynchonous call
options.getter(key, function(err, val) {
if(!err && typeof val === 'undefined') {
err = new Error('undefined val returned by getter');
}
var queue = my.cache[key].queue;
if(!err) {
my.cache[key].date = Date.now();
my.cache[key].value = val;
delete my.cache[key].queue;
}
else {
delete my.cache[key];
}
queue.forEach(function(cb) {
cb(err, val);
});
});
}
else {
cb();
}
var count = that.count();
if(count > my.size) {
evict[my.modes[my.evict]](count);
}
};
/**
* Function for invalidating a value or more from the cache
* given a key or a regex matching all the keys whose value
* to be invalidated
* @param key {string or regex}
*/
invalidate = function(key) {
if(typeof key === "string") {
if(my.cache[key] && typeof my.cache[key].value !== 'undefined') {
var evct = my.cache[key].evict;
var value = my.cache[key].value;
delete my.cache[key];
if(typeof evct === 'function') {
evct(key, value);
}
}
return;
}
//regex on whole map
if(typeof key === "object" && key instanceof RegExp) {
base.forEach(my.cache, function(v, k) {
if(key.test(k) && typeof my.cache[k].value !== 'undefined') {
var evct = my.cache[k].evict;
var value = my.cache[k].value;
delete my.cache[k];
if(typeof evct === 'function') {
evct(k, value);
}
}
});
}
if(typeof key === "undefined") {
base.forEach(my.cache, function(v, k) {
if(typeof my.cache[k].value !== 'undefined') {
var evct = my.cache[k].evict;
var value = my.cache[k].value;
delete my.cache[k];
if(typeof evct === 'function') {
evct(k, value);
}
}
});
}
};
/**
* Calculates and returns the number of elements
* @return count the number of elements within the cache.
*/
count = function() {
var count = 0;
for(var s in my.cache) {
if(my.cache.hasOwnProperty(s)) {
count ++;
}
}
return count;
};
/**
* Private Function for keeping the cash invalidate expired element at
* my.interval rate.
*/
runloop = function() {
var now = Date.now();
var expired = [];
for(var s in my.cache) {
if(my.cache.hasOwnProperty(s)) {
if(typeof my.cache[s].timeout === "undefined" ||
typeof my.cache[s].value === "undefined")
continue;
if(now > my.cache[s].timeout) {
expired.push(s);
}
}
}
for(var i = 0; i < expired.length; i ++) {
invalidate(expired[i].key);
}
};
setInterval(runloop, my.interval);
base.method(that, 'get', get, _super);
base.method(that, 'invalidate', invalidate, _super);
base.method(that, 'count', count, _super);
return that;
}
exports.cache = cache;