/**
* @file Manages asynchronous method response cache
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
*/
'use strict';
var events = require('events'),
inherits = require('inherits'),
_ = require('lodash/core');
/**
* Class for managing cache entry
*
* @private
* @class
* @constructor
* @template T
*/
var CacheEntry = function() {
this.fetching = false;
};
inherits(CacheEntry, events.EventEmitter);
/**
* Get value in the cache entry
*
* @param {Callback.<T>} [callback] - Callback function callbacked the cache entry updated
* @returns {T|undefined}
*/
CacheEntry.prototype.get = function(callback) {
if (!callback) {
return this._value;
} else {
this.once('value', callback);
if (!_.isUndefined(this._value)) {
this.emit('value', this._value);
}
}
};
/**
* Set value in the cache entry
*
* @param {T} [value] - A value for caching
*/
CacheEntry.prototype.set = function(value) {
this._value = value;
this.emit('value', this._value);
};
/**
* Clear cached value
*/
CacheEntry.prototype.clear = function() {
this.fetching = false;
delete this._value;
};
/**
* Caching manager for async methods
*
* @class
* @constructor
*/
var Cache = function() {
this._entries = {};
};
/**
* retrive cache entry, or create if not exists.
*
* @param {String} [key] - Key of cache entry
* @returns {CacheEntry}
*/
Cache.prototype.get = function(key) {
if (key && this._entries[key]) {
return this._entries[key];
} else {
var entry = new CacheEntry();
this._entries[key] = entry;
return entry;
}
};
/**
* clear cache entries prefix matching given key
* @param {String} [key] - Key prefix of cache entry to clear
*/
Cache.prototype.clear = function(key) {
for (var k in this._entries) {
if (!key || k.indexOf(key) === 0) {
this._entries[k].clear();
}
}
};
/**
* create and return cache key from namespace and serialized arguments.
* @private
*/
function createCacheKey(namespace, args) {
args = Array.prototype.slice.apply(args);
return namespace + '(' + _.map(args, function(a){ return JSON.stringify(a); }).join(',') + ')';
}
/**
* Enable caching for async call fn to intercept the response and store it to cache.
* The original async calll fn is always invoked.
*
* @protected
* @param {Function} fn - Function to covert cacheable
* @param {Object} [scope] - Scope of function call
* @param {Object} [options] - Options
* @return {Function} - Cached version of function
*/
Cache.prototype.makeResponseCacheable = function(fn, scope, options) {
var cache = this;
options = options || {};
return function() {
var args = Array.prototype.slice.apply(arguments);
var callback = args.pop();
if (!_.isFunction(callback)) {
args.push(callback);
callback = null;
}
var key = _.isString(options.key) ? options.key :
_.isFunction(options.key) ? options.key.apply(scope, args) :
createCacheKey(options.namespace, args);
var entry = cache.get(key);
entry.fetching = true;
if (callback) {
args.push(function(err, result) {
entry.set({ error: err, result: result });
callback(err, result);
});
}
var ret, error;
try {
ret = fn.apply(scope || this, args);
} catch(e) {
error = e;
}
if (ret && _.isFunction(ret.then)) { // if the returned value is promise
if (!callback) {
return ret.then(function(result) {
entry.set({ error: undefined, result: result });
return result;
}, function(err) {
entry.set({ error: err, result: undefined });
throw err;
});
} else {
return ret;
}
} else {
entry.set({ error: error, result: ret });
if (error) { throw error; }
return ret;
}
};
};
/**
* Enable caching for async call fn to lookup the response cache first, then invoke original if no cached value.
*
* @protected
* @param {Function} fn - Function to covert cacheable
* @param {Object} [scope] - Scope of function call
* @param {Object} [options] - Options
* @return {Function} - Cached version of function
*/
Cache.prototype.makeCacheable = function(fn, scope, options) {
var cache = this;
options = options || {};
var $fn = function() {
var args = Array.prototype.slice.apply(arguments);
var callback = args.pop();
if (!_.isFunction(callback)) {
args.push(callback);
}
var key = _.isString(options.key) ? options.key :
_.isFunction(options.key) ? options.key.apply(scope, args) :
createCacheKey(options.namespace, args);
var entry = cache.get(key);
if (!_.isFunction(callback)) { // if callback is not given in last arg, return cached result (immediate).
var value = entry.get();
if (!value) { throw new Error('Function call result is not cached yet.'); }
if (value.error) { throw value.error; }
return value.result;
}
entry.get(function(value) {
callback(value.error, value.result);
});
if (!entry.fetching) { // only when no other client is calling function
entry.fetching = true;
args.push(function(err, result) {
entry.set({ error: err, result: result });
});
fn.apply(scope || this, args);
}
};
$fn.clear = function() {
var key = _.isString(options.key) ? options.key :
_.isFunction(options.key) ? options.key.apply(scope, arguments) :
createCacheKey(options.namespace, arguments);
cache.clear(key);
};
return $fn;
};
module.exports = Cache;