Last active
March 18, 2018 07:50
-
-
Save psandeepunni/bc1e102e6e71149582f3 to your computer and use it in GitHub Desktop.
Mongoose Query Cache (in-memory)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
/** | |
* This mongoose query caching is a shameless copy of mongoose-memcached (https://github.com/benjibc/mongoose-memcached) module | |
* But instead of relying upon using memcached db, this uses an in memory cache provided by memory-cache (https://github.com/ptarjan/node-cache) | |
* Thank you Paul Tarjan (ptarjan) and Yufei (Benny) Chen (benjibc). You've made our lives easier | |
*/ | |
/* | |
*Module dependencies. | |
*/ | |
var inMemoryCache = require('memory-cache'), | |
NodeStream = require('stream'), | |
helpers = require('mongoose/lib/queryhelpers'), | |
Promise = require('mpromise'); | |
/** | |
* generate unique hash key from a string | |
*/ | |
String.prototype.hashCode = function(){ | |
if (Array.prototype.reduce){ | |
return this.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0); | |
} | |
var hash = 0; | |
if (this.length === 0) return hash; | |
for (var i = 0; i < this.length; i++) { | |
var character = this.charCodeAt(i); | |
hash = ((hash<<5)-hash)+character; | |
hash = hash & hash; // Convert to 32bit integer | |
} | |
return hash.toString(); | |
} | |
/** | |
* sets the function to generate the key | |
* before you call this, make sure the query is a find query | |
* @param {Query} query | |
*/ | |
function genKeyFromQuery(query) { | |
var obj = {}; | |
obj.model = query.model.modelName; | |
obj.options = query.options; | |
obj.cond = query._conditions; | |
obj.fields = query._fields; | |
obj.update = query._update; | |
obj.path = query._path; | |
obj.distinct = query._distinct; | |
return JSON.stringify(obj).hashCode(); | |
} | |
/** | |
* Expose module. | |
* @param {Object} Mongoose | |
* @param {Object} options for the server such as cache and ttl | |
*/ | |
module.exports = function mongooseCached(mongoose, options) { | |
options = options || {}; | |
// default time to live in seconds | |
var TTL = options.ttl || 60, | |
CACHED = 'cache' in options ? options.cache : false, | |
Query = mongoose.Query, | |
Stream = mongoose.Stream, | |
exec = Query.prototype.exec, | |
stream = Query.prototype.stream, | |
genKey = options.genKey || genKeyFromQuery; | |
/** | |
* Cache enabler and disabler | |
* @param {Boolean} cache | |
* @param {Number} ttl | |
* @param {String} key identifier used for cache storage | |
* @return {Query} this | |
* @api public | |
*/ | |
Query.prototype.cache = function (cache, ttl, key) { | |
// if you just call this function with no parameter, we are going to | |
// assume the user want it cached, with the default ttl | |
if (!arguments.length) { | |
this._cache = true; | |
} else if ("boolean" === typeof cache) { | |
this._cache = cache; | |
} | |
if ('number' === typeof ttl) { | |
this._ttl = ttl; | |
} else { | |
this._ttl = TTL; | |
} | |
if('string' === typeof key) { | |
this._key = key; | |
} else { | |
this._key = undefined; | |
} | |
return this; | |
}; | |
/** | |
* execution for the function | |
* @param {Function} cb callback for the query | |
* @return {Query} this | |
* @api public | |
*/ | |
Query.prototype.exec = function (cb) { | |
var self = this; | |
// initialize the value of _cache if .cache() was never called | |
if(typeof self._cache === 'undefined') { | |
self._cache = CACHED; | |
} | |
if(typeof self._ttl === 'undefined') { | |
self._ttl = TTL; | |
} | |
// only cache find operations | |
if (!self._cache || !(self.op === 'find' || self.op === 'findOne')) { | |
self._dataCached = false; | |
return exec.call(self, cb); | |
} | |
var key = self._key || genKey(self); | |
// check in cache. If not, then execute the query | |
var data = JSON.parse(inMemoryCache.get(key)); | |
if (!data) { | |
// if it does not exist, set the data after execution finished | |
exec.call(self, function(e, data) { | |
if(e) { | |
return cb(e); | |
} | |
// if it is not in cache, set the key and return | |
inMemoryCache.put(key, JSON.stringify(data), self._ttl); | |
self._dataCached = false; | |
return cb(null, data); | |
}); | |
} else { | |
var pop, opts = self._mongooseOptions; | |
if (opts.populate) { pop = helpers.preparePopulationOptionsMQ(self, opts); } | |
if (Array.isArray(data)) { | |
var arr = []; | |
var createAndEmit = function (promise, doc) { | |
var instance = helpers.createModel(self.model, doc, self._fields); | |
instance.init(doc, function (err) { | |
if (err) { return promise.reject(err); } | |
promise.fulfill(instance); | |
}); | |
}; | |
// instanciation promise generator | |
var instanciate = function (doc) { | |
// create promise | |
var promise = new Promise(); | |
promise.onFulfill(function (arg) { | |
arr.push(arg); | |
}); | |
// Check population | |
if (!pop) { | |
if (opts.lean) { | |
promise.fulfill(doc); | |
} else { | |
createAndEmit(promise, doc); | |
} | |
return promise; | |
} | |
// Populate document | |
self.model.populate(doc, pop, function (err, doc) { | |
if (err) { return promise.reject(err); } | |
return true === opts.lean | |
? promise.fulfill(doc) | |
: createAndEmit(promise, doc); | |
}); | |
return promise; | |
}; | |
// chaining instanciation promises | |
var initialPromise, returnPromise = initialPromise = new Promise(), i, len; | |
for (i = 0, len = data.length; i < len; i += 1) { | |
returnPromise = returnPromise.chain(instanciate(data[i])); | |
} | |
// on chain resolve | |
returnPromise.onResolve(function (err) { | |
if (err) { | |
self._dataCached = false; | |
return cb(err); | |
} | |
self._dataCached = true; | |
cb(null, arr); | |
}); | |
// start chain | |
initialPromise.fulfill(); | |
} else { | |
var pop, opts = self._mongooseOptions; | |
if (opts.populate) { pop = helpers.preparePopulationOptionsMQ(self, opts); } | |
var createAndEmit = function (doc) { | |
var instance = helpers.createModel(self.model, doc, self._fields); | |
instance.init(doc, function (err) { | |
if (err) { | |
self._dataCached = false; | |
return cb(err); | |
} | |
self._dataCached = true; | |
cb(null, instance); | |
}); | |
}; | |
// Check population | |
if (!pop) { | |
if (opts.lean) { | |
cb(null, data); | |
} else { | |
createAndEmit(data); | |
} | |
return this; | |
} | |
// Populate document | |
self.model.populate(data, pop, function (err, doc) { | |
if (err) { return cb(err); } | |
return true === opts.lean | |
? cb(null, doc) | |
: createAndEmit(doc); | |
}); | |
} | |
return this; | |
} | |
return this; | |
}; | |
/** | |
* execution for the stream function | |
* @param {Options} options | |
* @return {Stream} | |
* @api public | |
*/ | |
Query.prototype.stream = function (options) { | |
var self = this, outStream = new NodeStream(); | |
// initialize the value of _cache if .cache() was never called | |
if(typeof self._cache === 'undefined') { | |
self._cache = CACHED; | |
} | |
if(typeof self._ttl === 'undefined') { | |
self._ttl = TTL; | |
} | |
// only cache find operations | |
if (!self._cache || self.op !== 'find') { | |
self._dataCached = false; | |
return stream.call(self, options); | |
} | |
var key = self._key || genKey(self); | |
// check in memory. If not, then execute the query | |
var data = JSON.parse(inMemoryCache.get(key)); | |
var pop, | |
opts = self._mongooseOptions, | |
transform = options && 'function' == typeof options.transform | |
? options.transform | |
: function(k){ return k }; | |
function emit (doc) { | |
outStream.emit('data', doc); | |
} | |
function createAndEmit (doc) { | |
var instance = helpers.createModel(self.model, doc, self._fields); | |
instance.init(doc, function (err) { | |
if (err) return self.destroy(err); | |
emit(instance); | |
}); | |
} | |
function processItem (doc) { | |
if (self._destroyed) return; | |
if (!pop) { | |
return true === opts.lean | |
? emit(transform(doc)) | |
: createAndEmit(doc); | |
} | |
self.model.populate(doc, pop, function (err, doc) { | |
if (err) { return self.destroy(err); } | |
return true === opts.lean | |
? emit(transform(doc)) | |
: createAndEmit(doc); | |
}); | |
} | |
function emitData(items) { | |
if (opts.populate) { pop = helpers.preparePopulationOptionsMQ(self, opts); } | |
setTimeout(function () { | |
items.forEach(processItem); | |
outStream.emit('end'); | |
outStream.emit('close'); | |
}, 3); | |
return outStream; | |
} | |
if (data) { | |
self._dataCached = true; | |
return emitData(data); | |
} else { | |
data = []; | |
// if it does not exist, set the data after execution finished | |
stream.call(self, options) | |
.on('data', function (item) { | |
data.push(item); | |
}) | |
.on('error', function (e) { | |
if(e) { | |
return setTimeout(function () { | |
outStream.emit('error', e); | |
}, 3); | |
} | |
}) | |
.on('end', function() { | |
// if it is not in memcached, set the key and return | |
inMemoryCache.put(key, JSON.stringify(data), self._ttl); | |
self._dataCached = false; | |
setTimeout(function () { | |
data.forEach(emit); | |
outStream.emit('end'); | |
outStream.emit('close'); | |
}, 3); | |
}); | |
} | |
return outStream; | |
}; | |
/** | |
* Check if results from this query is from cache. | |
* | |
* @type {Boolean} | |
* @api public | |
*/ | |
Object.defineProperty(Query.prototype, 'isFromCache', { | |
get: function () { | |
return !(typeof this._dataCached === 'undefined' || | |
this._dataCached === false); | |
} | |
}); | |
/** | |
* Check if this query has caching enabled. | |
* | |
* @type {Boolean} | |
* @api public | |
*/ | |
Object.defineProperty(Query.prototype, 'isCacheEnabled', { | |
get: function () { | |
return this._cache; | |
} | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment