mquery-cache
Version:
TTL based cache library for mquery supporting mongoose and native mongodb query result caching.
245 lines (205 loc) • 6.06 kB
JavaScript
'use strict';
/**
* Module dependencies.
*/
var debug = require('debug')('mquery-cache')
, EventEmitter = require('events').EventEmitter
, util = require('util')
, _ = require('underscore')
;
/**
* Default object hashing algorithm to return a unique
* key representation of an object.
*
* @param {Object} query
* @return {String} hash
* @api private
*/
function keyhasher(query){
debug('Beginning key hash');
var keyData = {
options: query._mongooseOptions,
collection: query._collection.collection.name,
conditions: query._conditions,
fields: query._fields,
path: query._path,
distinct: query._distinct
}
, key = JSON.stringify(keyData);
debug('Hashed key [ %s ] with data %j', key, keyData);
return key;
}
/**
* CachedQuery constructor.
*
* @param {String|Number} key
* @param {Object} query
* @param {Mixed} data
* @param {String|Number} ttl
* @return {CachedQuery} this
* @api private
*/
function CachedQuery(key,query,data,ttl){
//
this.query = query;
this.ttl = ttl;
this._data = data;
this.key = key;
this.refreshing = false;
}
util.inherits(CachedQuery, EventEmitter);
Object.defineProperty(CachedQuery.prototype, 'data', {
get: function(){
return this._data || null;
},
set: function(val){
var oldval = this._data;
this._data = val;
debug('Cache [ %s ] refreshed',this.key);
this.emit('refresh',val,oldval);
}
});
/**
* TTLCacheCollection constructor.
*
* @param {Array} values
* @return {TTLCacheCollection} this
* @api private
*/
function TTLCacheCollection(values){
this.values = values || {};
this._ttlIntervals = {};
}
util.inherits(TTLCacheCollection, EventEmitter);
TTLCacheCollection.prototype.add = function(cache){
debug('Adding cache [ %s ] to collection',cache.key);
var _this = this;
this.remove(cache.key,true);
this.values[ cache.key] = cache;
this._ttlIntervals[ cache.key] = setTimeout(function(){
debug('Cache [ %s ] expired',cache.key);
delete _this.values[ cache.key];
delete _this._ttlIntervals[ cache.key]
_this.emit('expire');
}, cache.ttl*1000);
this.emit('add',cache,this);
}
TTLCacheCollection.prototype.remove = function(key,silent){
if(this._ttlIntervals[ key]) {
debug('Removing cache [ %s ] from collection',key);
clearTimeout(this._ttlIntervals[ key]);
delete this._ttlIntervals[ key];
delete this.values[ key];
if(!silent) this.emit('remove');
}
}
TTLCacheCollection.prototype.get = function(key){
return this.values[ key];
}
/**
* Expose module.
*/
module.exports = function(Query, Promise, options){
var caches = new TTLCacheCollection
, opts = {
ttl: 10,
hasher: keyhasher
}
;
// Combine default and user defined options.
options = _.extend(opts, options || {});
// Hash function.
var hashCacheKey = options.hasher;
/**
* Cache setter. Calling this method on a query
* will trigger cache to be turned on with the
* supplied ttl.
*
* @param {Number|String} ttl
* @return {Query} this
* @api public
*/
Query.prototype.cache = function(ttl){
//
debug('Checking query operation [%s] for cacheability', this.op);
// Restrict caching to `find` operations only.
if(/^(find)(?![a-z]+(update))/ig.test(this.op)&&ttl) {
debug('Caching query %j', this._mongooseOptions);
// Capture current exec method to call later.
var exec = this.exec;
/**
* Overwrite default Query.exec and call it only
* when there is no cache to return.
*
* @param {String} op
* @param {Function} callback
* @return {Promise} promise
* @api public
*/
this.exec = function(op, callback){
debug('Executing cachable query');
// Generate query key and check for existing cache.
var key = hashCacheKey(this)
, cache = caches.get(key)
;
//
debug('Validating cache for key %s', key);
// Check arguments.
if( 'function' === typeof op){
callback = op;
op = null;
}else if( 'string' === typeof op){
this.op = op;
}
// if cache exists validate and return it.
if(cache&&cache.refreshing===false){
debug('Cached results found for key', key);
// Create promise to send data back through callback.
var promise = new Promise();
if(callback) promise.addBack(callback);
// Resolve promise with cached data.
return promise.resolve(null, cache.data);
}
// Otherwise query the database as normal.
else{
debug('No cache found for query %j', this.options);
// Create cache instance
cache = cache || new CachedQuery(key, this, null, ttl || options.ttl);
// Checking if another request is already updating cached query
if(cache.refreshing){
// Create promise to send data back through callback.
var promise = new Promise();
//
cache.once('refresh', function(val,oldval){
debug('Cache refreshed results');
promise.resolve(null, val);
});
}else{
//
cache.refreshing = true;
// Execute query
var promise = exec.apply( this, arguments);
// Get notified when promise has been
// resolved and cache results.
promise.addBack( function(err,results){
if(results) {
debug('Promise returned results');
//
cache.refreshing = false;
//
cache.data = results;
// Set cache in caches
caches.add(cache);
}
});
}
//
return promise;
}
}
}else if(!ttl){
caches.remove(hashCacheKey(this));
}
return this;
};
}