UNPKG

jsforce2

Version:

Salesforce API Library for JavaScript

253 lines (236 loc) 6.95 kB
/** * @file Manages asynchronous method response cache * @author Shinichi Tomita <shinichi.tomita@gmail.com> */ 'use strict'; var events = require('events'), inherits = require('inherits'), _ = require('lodash/core'); /** * Class for managing cache entry * * @private * @class * @constructor * @template T */ var CacheEntry = function() { this.fetching = false; }; inherits(CacheEntry, events.EventEmitter); /** * Get value in the cache entry * * @param {Callback.<T>} [callback] - Callback function callbacked the cache entry updated * @returns {T|undefined} */ CacheEntry.prototype.get = function(callback) { if (!callback) { return this._value; } else { this.once('value', callback); if (!_.isUndefined(this._value)) { this.emit('value', this._value); } } }; /** * Set value in the cache entry * * @param {T} [value] - A value for caching */ CacheEntry.prototype.set = function(value) { this._value = value; this.emit('value', this._value); }; /** * Clear cached value */ CacheEntry.prototype.clear = function() { this.fetching = false; delete this._value; }; /** * Caching manager for async methods * * @class * @constructor */ var Cache = function() { this._entries = {}; }; /** * retrive cache entry, or create if not exists. * * @param {String} [key] - Key of cache entry * @returns {CacheEntry} */ Cache.prototype.get = function(key) { if (key && this._entries[key]) { return this._entries[key]; } else { var entry = new CacheEntry(); this._entries[key] = entry; return entry; } }; /** * clear cache entries prefix matching given key * @param {String} [key] - Key prefix of cache entry to clear */ Cache.prototype.clear = function(key) { for (var k in this._entries) { if (!key || k.indexOf(key) === 0) { this._entries[k].clear(); } } }; /** * create and return cache key from namespace and serialized arguments. * @private */ function createCacheKey(namespace, args) { args = Array.prototype.slice.apply(args); return namespace + '(' + _.map(args, function(a){ return JSON.stringify(a); }).join(',') + ')'; } /** * Enable caching for async call fn to intercept the response and store it to cache. * The original async calll fn is always invoked. * * @protected * @param {Function} fn - Function to covert cacheable * @param {Object} [scope] - Scope of function call * @param {Object} [options] - Options * @return {Function} - Cached version of function */ Cache.prototype.makeResponseCacheable = function(fn, scope, options) { var cache = this; options = options || {}; return function() { var args = Array.prototype.slice.apply(arguments); var callback = args.pop(); if (!_.isFunction(callback)) { args.push(callback); callback = null; } var keys = _.isString(options.key) ? options.key : _.isFunction(options.key) ? options.key.apply(scope, args) : createCacheKey(options.namespace, args); if (!Array.isArray(keys)) { keys = [ keys ]; } var entries = []; keys.forEach(function (key) { var entry = cache.get(key); entry.fetching = true; entries.push(entry); }) if (callback) { args.push(function(err, result) { if (Array.isArray(result) && result.length == entries.length) { entries.forEach(function (entry, index) { entry.set({ error: err, result: result[index] }); }) } else { entries.forEach(function (entry) { entry.set({ error: err, result: result }); }); } callback(err, result); }); } var ret, error; try { ret = fn.apply(scope || this, args); } catch(e) { error = e; } if (ret && _.isFunction(ret.then)) { // if the returned value is promise if (!callback) { return ret.then(function(result) { if (Array.isArray(result) && result.length == entries.length) { entries.forEach(function (entry, index) { entry.set({ error: undefined, result: result[index] }); }) } else { entries.forEach(function (entry) { entry.set({ error: undefined, result: result }); }); } return result; }, function(err) { if (Array.isArray(err) && err.length == entries.length) { entries.forEach(function (entry, index) { entry.set({ error: err[index], result: undefined }); }) } else { entries.forEach(function (entry) { entry.set({ error: err, result: undefined }); }); } throw err; }); } else { return ret; } } else { if (Array.isArray(ret) && ret.length == entries.length) { entries.forEach(function (entry, index) { entry.set({ error: error, result: ret[index] }); }) } else { entries.forEach(function (entry) { entry.set({ error: error, result: ret }); }); } if (error) { throw error; } return ret; } }; }; /** * Enable caching for async call fn to lookup the response cache first, then invoke original if no cached value. * * @protected * @param {Function} fn - Function to covert cacheable * @param {Object} [scope] - Scope of function call * @param {Object} [options] - Options * @return {Function} - Cached version of function */ Cache.prototype.makeCacheable = function(fn, scope, options) { var cache = this; options = options || {}; var $fn = function() { var args = Array.prototype.slice.apply(arguments); var callback = args.pop(); if (!_.isFunction(callback)) { args.push(callback); } var key = _.isString(options.key) ? options.key : _.isFunction(options.key) ? options.key.apply(scope, args) : createCacheKey(options.namespace, args); var entry = cache.get(key); if (!_.isFunction(callback)) { // if callback is not given in last arg, return cached result (immediate). var value = entry.get(); if (!value) { throw new Error('Function call result is not cached yet.'); } if (value.error) { throw value.error; } return value.result; } entry.get(function(value) { callback(value.error, value.result); }); if (!entry.fetching) { // only when no other client is calling function entry.fetching = true; args.push(function(err, result) { entry.set({ error: err, result: result }); }); fn.apply(scope || this, args); } }; $fn.clear = function() { var key = _.isString(options.key) ? options.key : _.isFunction(options.key) ? options.key.apply(scope, arguments) : createCacheKey(options.namespace, arguments); cache.clear(key); }; return $fn; }; module.exports = Cache;