/*jshint -W106 */ /** * Interception module for caching the results returned by the intercepted methods. * * **IMPORTANT:** this interceptor breaks the call chain and returns immediately if the requested item is cached, therefore subsequent interceptors and the method itself will not be invoked. * * The interceptor uses [node-cache-manager](https://github.com/BryanDonovan/node-cache-manager) library for caching. * Please refer to **node-cache-manager** [documentation](https://github.com/BryanDonovan/node-cache-manager) for more details on configuration and usage. * * For more details on using interceptors please see the {@tutorial interceptors} tutorial. * * @example * var entree = require("entree"), * cache = entree.resolveInterceptor("cache"), * stores = [ * { store: "memory", max: 1000, ttl: 10 }, * { store: require("./redis_store"), db: 0, ttl: 100 } * ]; * * entree.posts.use(cache.interception(stores, ["get"])); * * @module cache */ "use strict"; var cacheManager = require("cache-manager"), _ = require("lodash"); function getCursorKey(cursor) { return cursor.provider._uuid + JSON.stringify(cursor.query) + cursor.skipValue + cursor.limitValue + (cursor.mapping || "") + (cursor.criteria || ""); } /** * Configures and returns an interceptor function. * @param {object=} stores - defines the cache stores. By default only memory store is used with the following configuration: `{ store: "memory", max: 1000, ttl: 10 }`. * @param {(string|string[])=} actions - Specifies which actions (methods) should be cached. Could be single action or an array of actions. If omitted, all actions are cached. * Possible values are: `["insert", "upsert", "update", "get", "delete", "select"]` * @return {function} */ exports.interception = function (stores, actions) { var cache, _stores, i; if (!stores) { cache = cacheManager.caching({ store: "memory", max: 1000, ttl: 10 }); } else if (_.isArray(stores)) { if (stores.length === 1) { cache = cacheManager.caching(stores[0]); } else { _stores = []; _.each(stores, function (store) { _stores.push(cacheManager.caching(store)); }); cache = cacheManager.multi_caching(_stores); } } else { cache = cacheManager.caching(stores); } if (actions) { if (!_.isArray(actions)) { actions = [actions]; } for (i = 0; i < actions.length; i++) { if (actions[i].charAt(0) !== "_") { actions[i] = "_" + actions[i]; } } } function wrapCursor(cursor, done) { var key = getCursorKey(cursor); cache.get(key, function (err, entry) { if (err) { return done(err); } var current = 0, items = [], _nextObject = cursor._nextObject, _count = cursor.count; if (!entry) { entry = {}; } cursor._nextObject = function (callback) { if (entry.items) { callback(null, entry.items[current++]); } else { _nextObject.call(cursor, function (err, item) { if (!err) { if (item) { items.push(item); } else { entry.items = items; cache.set(key, entry); } } callback(err, item); }); } }; cursor.count = function (callback) { if (entry.count) { callback(null, entry.count); } else { _count.call(cursor, function (err, count) { if (!err) { entry.count = count; cache.set(key, entry); } callback(err, count); }); } }; done(null, cursor); }); } return function (action, context, item, next, out) { if (!out) { throw new Error("Cache interceptor is asynchronous and therefore it requires a callback function."); } var key; if (actions && actions.indexOf(action) === -1) { return next(item, out); } switch (action) { case "_insert": case "_upsert": case "_update": key = this._getId(item); cache.set(key, item); return next(item, out); case "_delete": key = this._getId(item); cache.del(key); return next(item, out); case "_get": key = this._getId(item); return cache.wrap(key, function (cb) { next(item, cb); }, out); case "_select": return next(item, function (err, res) { if (err) { return out(err); } wrapCursor(res, out); }); } }; };