cache-service
Version:
A tiered caching solution for node.
334 lines (311 loc) • 9.71 kB
JavaScript
var CacheCollection = require('./cacheCollection');
/**
* cacheService constructor
* @constructor
* @param cacheServiceConfig: {
* nameSpace: {string | ''},
* verbose: {boolean | false},
* writeToVolatileCaches: {boolean | true}
* }
* @param cacheModules: [
* {cache module object}
* ]
*/
function cacheService(cacheServiceConfig, cacheModules) {
var self = this;
/**
******************************************* PUBLIC FUNCTIONS *******************************************
*/
/**
* Get the value associated with a given key
* @param {string} key
* @param {function} cb
*/
self.get = function(key, cb){
if(arguments.length < 2){
throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.get() requires 2 arguments.');
}
log(false, '.get() called:', {key: key});
var curCache;
var curCacheIndex = 0;
var callback = function(err, result){
var status = checkCacheResponse(key, err, result, curCache.type, curCacheIndex - 1);
if(status.status === 'continue'){
if(status.toIndex){
curCacheIndex = status.toIndex;
}
getNextCache();
return;
}
if(status.status === 'break'){
cb(err, status.result);
if(self.writeToVolatileCaches){
writeToVolatileCaches(curCacheIndex - 1, key, result);
}
}
else {
log(false, '.get() key not found:', {key: key});
cb(null, null);
}
};
function getNextCache(){
if(curCacheIndex < self.caches.length){
curCache = self.caches[curCacheIndex++];
curCache.get(key, callback);
}
}
getNextCache();
};
/**
* Get multiple values given multiple keys
* @param {array} keys
* @param {function} cb
*/
self.mget = function(keys, cb){
if(arguments.length < 2){
throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.mget() requires 2 arguments.');
}
log(false, '.mget() called:', {keys: keys});
var maxKeysFound = 0;
var returnError = null;
var returnResponse = null;
var keepGoing = true;
var i = 0;
var successIndex = 0;
var finished = false;
var finish = function(index){
if(!finished){
finished = true;
keepGoing = false;
if(self.writeToVolatileCaches){
writeToVolatileCaches(index, returnResponse);
}
cb(returnError, returnResponse);
}
};
var callback = function(err, response, index){
var objectSize = 0;
for (var key in response){
if(response.hasOwnProperty(key)){
++objectSize;
}
}
if(objectSize === keys.length){
returnResponse = response;
finish(index);
}
else if(objectSize > maxKeysFound){
maxKeysFound = objectSize;
returnResponse = response;
successIndex = index;
}
if(index + 1 === self.caches.length){
returnResponse = response;
finish(successIndex);
}
};
while(keepGoing && i < self.caches.length){
var cache = self.caches[i];
cache.mget(keys, callback, i);
i++;
}
};
/**
* Associate a key and value and optionally set an expiration
* @param {string} key
* @param {string | object} value
* @param {integer} expiration
* @param {function} refresh
* @param {function} cb
*/
self.set = function(){
if(arguments.length < 2){
throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.set() requires a minimum of 2 arguments.');
}
var key = arguments[0];
var value = arguments[1];
var expiration = arguments[2] || null;
var refresh = arguments[3] || null;
var cb = arguments[4] || noop;
if(arguments.length === 4 && typeof arguments[3] === 'function'){
cb = arguments[3];
if(typeof arguments[2] === 'function'){
refresh = arguments[2];
expiration = null;
}
else{
expiration = arguments[2];
refresh = null;
}
}
else if(arguments.length === 3 && typeof arguments[2] === 'function'){
cb = arguments[2];
expiration = null;
}
log(false, '.set() called:', {key: key, value: value});
for(var i = 0; i < self.cachesLength; i++){
var cache = self.caches[i];
var ref = (i === self.caches.length - 1) ? refresh : null;
var func = (i === 0) ? cb : noop;
cache.set(key, value, expiration, ref, func);
}
};
/**
* Associate multiple keys with multiple values and optionally set expirations per function and/or key
* @param {object} arguments[0]
* @param {integer} arguments[1]
* @param {function} arguments[2]
*/
self.mset = function(){
if(arguments.length < 1){
throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.mset() requires a minimum of 1 argument.');
}
var obj = arguments[0];
var expiration = arguments[1] || null;
var cb = arguments[2] || null;
log(false, '.mset() called:', {data: obj});
for(var i = 0; i < self.cachesLength; i++){
var cache = self.caches[i];
expiration = expiration || cache.expiration;
if(i === self.caches.length - 1){
cache.mset(obj, expiration, cb);
}
else{
cache.mset(obj, expiration);
}
}
};
/**
* Delete a list of keys and their values
* @param {array} keys
* @param {function} cb
*/
self.del = function(keys, cb){
if(arguments.length < 1){
throw new Exception('INCORRECT_ARGUMENT_EXCEPTION', '.del() requires a minimum of 1 argument.');
}
log(false, '.del() called:', {keys: keys});
for(var i = 0; i < self.cachesLength; i++){
var cache = self.caches[i];
if(i === self.caches.length - 1){
cache.del(keys, cb);
}
else{
cache.del(keys);
}
}
};
/**
* Flush all keys and values from all configured caches in caches
* @param {function} cb
*/
self.flush = function(cb){
log(false, '.flush() called');
for(var i = 0; i < self.cachesLength; i++){
var cache = self.caches[i];
if(i === self.caches.length - 1){
cache.flush(cb);
}
else{
cache.flush();
}
}
};
/**
******************************************* PRIVATE FUNCTIONS *******************************************
*/
/**
* Initialize cacheService given the provided constructor params
*/
function init(){
if(!cacheServiceConfig) {
cacheServiceConfig = {};
}
self.nameSpace = cacheServiceConfig.nameSpace || '';
self.verbose = (typeof cacheServiceConfig.verbose === 'boolean') ? cacheServiceConfig.verbose : false;
self.writeToVolatileCaches = (typeof cacheServiceConfig.writeToVolatileCaches === 'boolean') ? cacheServiceConfig.writeToVolatileCaches : true;
self.caches = new CacheCollection({nameSpace: self.nameSpace, verbose: self.verbose}, cacheModules).caches;
self.cachesLength = self.caches.length;
}
/**
* Decides what action cacheService should take given the response from a configured cache
* @param {string} key
* @param {null | object} err
* @param {null | object | string} result
* @param {string} type
* @param {integer} cacheIndex
*/
function checkCacheResponse(key, err, result, type, cacheIndex){
if(err){
log(true, 'Error when getting key ' + key + ' from cache of type ' + type + ':', err);
if(cacheIndex < self.caches.length - 1){
return {status:'continue'};
}
}
//THIS ALLOWS false AS A VALID CACHE VALUE, BUT DO I WANT null TO BE VALID AS WELL?
if(result !== null && typeof result !== 'undefined'){
log(false, 'Key found:', {key: key, value: result});
return {status: 'break', result: result};
}
var curCache = self.caches[cacheIndex];
for(var i = cacheIndex + 1; i < self.cachesLength; i++) {
var nextCache = self.caches[i];
if(nextCache.checkOnPreviousEmpty || nextCache.expiration > curCache.expiration){
return {status:'continue', toIndex: i};
}
}
return {status: 'else'};
}
/**
* Writes data to caches that appear before the current cache in caches
* @param {integer} currentCacheIndex
* @param {string} key
* @param {null | object | string} value
*/
function writeToVolatileCaches(currentCacheIndex, key, value){
if(currentCacheIndex > 0){
var curExpiration = self.caches[currentCacheIndex].expiration;
for(var tempIndex = currentCacheIndex; tempIndex > -1; tempIndex--){
var preExpiration = self.caches[tempIndex].expiration;
if(preExpiration <= curExpiration){
var preCache = self.caches[currentCacheIndex];
if(value){
preCache.set(key, value); /*This means that a more volatile cache can have a key longer than a less volatile cache. Should I adjust this?*/
}
else if(typeof key === 'object'){
preCache.mset(key);
}
}
}
}
}
/**
* Instantates an exception to be thrown
* @param {string} name
* @param {string} message
* @return {exception}
*/
function Exception(name, message) {
this.name = name;
this.message = message;
}
/**
* Logging utility function
* @param {boolean} isError
* @param {string} message
* @param {object} data
*/
function log(isError, message, data){
var indentifier = 'cacheService: ';
if(self.verbose || isError){
if(data) {
console.log(indentifier + message, data);
} else {
console.log(indentifier + message);
}
}
}
function noop(){}
init();
}
module.exports = cacheService;