Source: cache.js

  1. /**
  2. * @file Manages asynchronous method response cache
  3. * @author Shinichi Tomita <shinichi.tomita@gmail.com>
  4. */
  5. 'use strict';
  6. var events = require('events'),
  7. inherits = require('inherits'),
  8. _ = require('lodash/core');
  9. /**
  10. * Class for managing cache entry
  11. *
  12. * @private
  13. * @class
  14. * @constructor
  15. * @template T
  16. */
  17. var CacheEntry = function() {
  18. this.fetching = false;
  19. };
  20. inherits(CacheEntry, events.EventEmitter);
  21. /**
  22. * Get value in the cache entry
  23. *
  24. * @param {Callback.<T>} [callback] - Callback function callbacked the cache entry updated
  25. * @returns {T|undefined}
  26. */
  27. CacheEntry.prototype.get = function(callback) {
  28. if (!callback) {
  29. return this._value;
  30. } else {
  31. this.once('value', callback);
  32. if (!_.isUndefined(this._value)) {
  33. this.emit('value', this._value);
  34. }
  35. }
  36. };
  37. /**
  38. * Set value in the cache entry
  39. *
  40. * @param {T} [value] - A value for caching
  41. */
  42. CacheEntry.prototype.set = function(value) {
  43. this._value = value;
  44. this.emit('value', this._value);
  45. };
  46. /**
  47. * Clear cached value
  48. */
  49. CacheEntry.prototype.clear = function() {
  50. this.fetching = false;
  51. delete this._value;
  52. };
  53. /**
  54. * Caching manager for async methods
  55. *
  56. * @class
  57. * @constructor
  58. */
  59. var Cache = function() {
  60. this._entries = {};
  61. };
  62. /**
  63. * retrive cache entry, or create if not exists.
  64. *
  65. * @param {String} [key] - Key of cache entry
  66. * @returns {CacheEntry}
  67. */
  68. Cache.prototype.get = function(key) {
  69. if (key && this._entries[key]) {
  70. return this._entries[key];
  71. } else {
  72. var entry = new CacheEntry();
  73. this._entries[key] = entry;
  74. return entry;
  75. }
  76. };
  77. /**
  78. * clear cache entries prefix matching given key
  79. * @param {String} [key] - Key prefix of cache entry to clear
  80. */
  81. Cache.prototype.clear = function(key) {
  82. for (var k in this._entries) {
  83. if (!key || k.indexOf(key) === 0) {
  84. this._entries[k].clear();
  85. }
  86. }
  87. };
  88. /**
  89. * create and return cache key from namespace and serialized arguments.
  90. * @private
  91. */
  92. function createCacheKey(namespace, args) {
  93. args = Array.prototype.slice.apply(args);
  94. return namespace + '(' + _.map(args, function(a){ return JSON.stringify(a); }).join(',') + ')';
  95. }
  96. /**
  97. * Enable caching for async call fn to intercept the response and store it to cache.
  98. * The original async calll fn is always invoked.
  99. *
  100. * @protected
  101. * @param {Function} fn - Function to covert cacheable
  102. * @param {Object} [scope] - Scope of function call
  103. * @param {Object} [options] - Options
  104. * @return {Function} - Cached version of function
  105. */
  106. Cache.prototype.makeResponseCacheable = function(fn, scope, options) {
  107. var cache = this;
  108. options = options || {};
  109. return function() {
  110. var args = Array.prototype.slice.apply(arguments);
  111. var callback = args.pop();
  112. if (!_.isFunction(callback)) {
  113. args.push(callback);
  114. callback = null;
  115. }
  116. var key = _.isString(options.key) ? options.key :
  117. _.isFunction(options.key) ? options.key.apply(scope, args) :
  118. createCacheKey(options.namespace, args);
  119. var entry = cache.get(key);
  120. entry.fetching = true;
  121. if (callback) {
  122. args.push(function(err, result) {
  123. entry.set({ error: err, result: result });
  124. callback(err, result);
  125. });
  126. }
  127. var ret, error;
  128. try {
  129. ret = fn.apply(scope || this, args);
  130. } catch(e) {
  131. error = e;
  132. }
  133. if (ret && _.isFunction(ret.then)) { // if the returned value is promise
  134. if (!callback) {
  135. return ret.then(function(result) {
  136. entry.set({ error: undefined, result: result });
  137. return result;
  138. }, function(err) {
  139. entry.set({ error: err, result: undefined });
  140. throw err;
  141. });
  142. } else {
  143. return ret;
  144. }
  145. } else {
  146. entry.set({ error: error, result: ret });
  147. if (error) { throw error; }
  148. return ret;
  149. }
  150. };
  151. };
  152. /**
  153. * Enable caching for async call fn to lookup the response cache first, then invoke original if no cached value.
  154. *
  155. * @protected
  156. * @param {Function} fn - Function to covert cacheable
  157. * @param {Object} [scope] - Scope of function call
  158. * @param {Object} [options] - Options
  159. * @return {Function} - Cached version of function
  160. */
  161. Cache.prototype.makeCacheable = function(fn, scope, options) {
  162. var cache = this;
  163. options = options || {};
  164. var $fn = function() {
  165. var args = Array.prototype.slice.apply(arguments);
  166. var callback = args.pop();
  167. if (!_.isFunction(callback)) {
  168. args.push(callback);
  169. }
  170. var key = _.isString(options.key) ? options.key :
  171. _.isFunction(options.key) ? options.key.apply(scope, args) :
  172. createCacheKey(options.namespace, args);
  173. var entry = cache.get(key);
  174. if (!_.isFunction(callback)) { // if callback is not given in last arg, return cached result (immediate).
  175. var value = entry.get();
  176. if (!value) { throw new Error('Function call result is not cached yet.'); }
  177. if (value.error) { throw value.error; }
  178. return value.result;
  179. }
  180. entry.get(function(value) {
  181. callback(value.error, value.result);
  182. });
  183. if (!entry.fetching) { // only when no other client is calling function
  184. entry.fetching = true;
  185. args.push(function(err, result) {
  186. entry.set({ error: err, result: result });
  187. });
  188. fn.apply(scope || this, args);
  189. }
  190. };
  191. $fn.clear = function() {
  192. var key = _.isString(options.key) ? options.key :
  193. _.isFunction(options.key) ? options.key.apply(scope, arguments) :
  194. createCacheKey(options.namespace, arguments);
  195. cache.clear(key);
  196. };
  197. return $fn;
  198. };
  199. module.exports = Cache;