Skip to content

Instantly share code, notes, and snippets.

@psandeepunni
Last active March 18, 2018 07:50
Show Gist options
  • Save psandeepunni/bc1e102e6e71149582f3 to your computer and use it in GitHub Desktop.
Save psandeepunni/bc1e102e6e71149582f3 to your computer and use it in GitHub Desktop.
Mongoose Query Cache (in-memory)
'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