UNPKG

cache-service-cache-module

Version:
312 lines (294 loc) 9.86 kB
/** * cacheModule constructor * @param config: { * type: {string | 'cache-module'} * verbose: {boolean | false}, * defaultExpiration: {integer | 900}, * readOnly: {boolean | false}, * checkOnPreviousEmpty: {boolean | true}, * backgroundRefreshIntervalCheck: {boolean | true}, * backgroundRefreshInterval: {integer | 60000}, * backgroundRefreshMinTtl: {integer | 70000}, * storage: {string | null}, * storageMock: {object | null} * } */ function cacheModule(config){ var self = this; config = config || {}; self.type = config.type || 'cache-module'; self.verbose = config.verbose || false; self.defaultExpiration = config.defaultExpiration || 900; self.readOnly = config.readOnly || false; self.checkOnPreviousEmpty = (typeof config.checkOnPreviousEmpty === 'boolean') ? config.checkOnPreviousEmpty : true; self.backgroundRefreshIntervalCheck = (typeof config.backgroundRefreshIntervalCheck === 'boolean') ? config.backgroundRefreshIntervalCheck : true; self.backgroundRefreshInterval = config.backgroundRefreshInterval || 60000; self.backgroundRefreshMinTtl = config.backgroundRefreshMinTtl || 70000; var store = null; var storageMock = config.storageMock || false; var backgroundRefreshEnabled = false; var browser = (typeof window !== 'undefined'); var cache = { db: {}, expirations: {}, refreshKeys: {} }; var storageKey; var interval; setupBrowserStorage(); log(false, 'Cache-module client created with the following defaults:', {type: self.type, defaultExpiration: self.defaultExpiration, verbose: self.verbose, readOnly: self.readOnly}); /** * Get the value associated with a given key * @param {string} key * @param {function} cb */ self.get = function(key, cb){ throwErrorIf((arguments.length < 2), 'ARGUMENT_EXCEPTION: .get() requires 2 arguments.'); log(false, 'get() called:', {key: key}); try { var now = Date.now(); var expiration = cache.expirations[key]; if(expiration > now){ var output = cache.db[key]; if(typeof output === 'object'){ output = cloneObject(output); } cb(null, output); } else{ expire(key); cb(null, null); } } catch (err) { cb({name: 'GetException', message: err}, null); } } /** * Get multiple values given multiple keys * @param {array} keys * @param {function} cb * @param {integer} index */ self.mget = function(keys, cb, index){ throwErrorIf((arguments.length < 2), 'ARGUMENT_EXCEPTION: .mget() requires 2 arguments.'); log(false, '.mget() called:', {keys: keys}); var values = {}; for(var i = 0; i < keys.length; i++){ var key = keys[i]; self.get(key, function(err, response){ if(response !== null){ values[key] = response; } }); } cb(null, values, index); } /** * Associate a key and value and optionally set an expiration * @param {string} key * @param {string | object} value * @param {integer} expiration * @param {function} refresh * @param {function} cb */ self.set = function(){ throwErrorIf((arguments.length < 2), 'ARGUMENT_EXCEPTION: .set() requires at least 2 arguments.'); var key = arguments[0]; var value = arguments[1]; var expiration = arguments[2] || null; var refresh = (arguments.length == 5) ? arguments[3] : null; var cb = (arguments.length == 5) ? arguments[4] : arguments[3]; log(false, '.set() called:', {key: key, value: value}); if(!self.readOnly){ try { expiration = (expiration) ? (expiration * 1000) : (self.defaultExpiration * 1000); var exp = expiration + Date.now(); cache.expirations[key] = exp; cache.db[key] = value; if(cb) cb(); if(refresh){ cache.refreshKeys[key] = {expiration: exp, lifeSpan: expiration, refresh: refresh}; backgroundRefreshInit(); } overwriteBrowserStorage(); } catch (err) { log(true, '.set() failed for cache of type ' + self.type, {name: 'CacheModuleSetException', message: err}); } } } /** * Associate multiple keys with multiple values and optionally set expirations per function and/or key * @param {object} obj * @param {integer} expiration * @param {function} cb */ self.mset = function(obj, expiration, cb){ throwErrorIf((arguments.length < 1), 'ARGUMENT_EXCEPTION: .mset() requires at least 1 argument.'); log(false, '.mset() called:', {data: obj}); for(var key in obj){ if(obj.hasOwnProperty(key)){ var tempExpiration = expiration || self.defaultExpiration; var value = obj[key]; if(typeof value === 'object' && value.cacheValue){ tempExpiration = value.expiration || tempExpiration; value = value.cacheValue; } self.set(key, value, tempExpiration); } } if(cb) cb(); } /** * Delete the provided keys and their associated values * @param {array} keys * @param {function} cb */ self.del = function(keys, cb){ throwErrorIf((arguments.length < 1), 'ARGUMENT_EXCEPTION: .del() requires at least 1 argument.'); log(false, '.del() called:', {keys: keys}); if(typeof keys === 'object'){ for(var i = 0; i < keys.length; i++){ var key = keys[i]; delete cache.db[key]; delete cache.expirations[key]; delete cache.refreshKeys[key]; } if(cb) cb(null, keys.length); } else{ delete cache.db[keys]; delete cache.expirations[keys]; delete cache.refreshKeys[keys]; if(cb) cb(null, 1); } overwriteBrowserStorage(); } /** * Flush all keys and values * @param {function} cb */ self.flush = function(cb){ log(false, '.flush() called'); cache.db = {}; cache.expirations = {}; cache.refreshKeys = {}; if(cb) cb(); overwriteBrowserStorage(); if(interval) clearInterval(interval); backgroundRefreshEnabled = false; } /** * Enable browser storage if desired and available */ function setupBrowserStorage(){ if(browser || storageMock){ if(storageMock){ store = storageMock; storageKey = 'cache-module-storage-mock'; } else{ var storageType = (config.storage === 'local' || config.storage === 'session') ? config.storage : null; store = (storageType && typeof Storage !== void(0)) ? window[storageType + 'Storage'] : false; storageKey = (storageType) ? self.type + '-' + storageType + '-storage' : null; } if(store){ var db = store.getItem(storageKey); try { cache = JSON.parse(db) || cache; } catch (err) { /* Do nothing */ } } // If storageType is set but store is not, the desired storage mechanism was not available else if(storageType){ log(true, 'Browser storage is not supported by this browser. Defaulting to an in-memory cache.'); } } } /** * Overwrite namespaced browser storage with current cache */ function overwriteBrowserStorage(){ if((browser && store) || storageMock){ var db = cache; try { db = JSON.stringify(db); store.setItem(storageKey, db); } catch (err) { /* Do nothing */ } } } /** * Throw a given error if error is true * @param {boolean} error * @param {string} message */ function throwErrorIf(error, message){ if(error) throw new Error(message); } /** * Delete a given key from cache.db and cache.expirations but not from cache.refreshKeys * @param {string} key */ function expire(key){ delete cache.db[key]; delete cache.expirations[key]; overwriteBrowserStorage(); } /** * Initialize background refresh */ function backgroundRefreshInit(){ if(!backgroundRefreshEnabled){ backgroundRefreshEnabled = true; if(self.backgroundRefreshIntervalCheck){ if(self.backgroundRefreshInterval > self.backgroundRefreshMinTtl){ throw new Error('BACKGROUND_REFRESH_INTERVAL_EXCEPTION: backgroundRefreshInterval cannot be greater than backgroundRefreshMinTtl.'); } } interval = setInterval(backgroundRefresh, self.backgroundRefreshInterval); } } /** * Handle the refresh callback from the consumer, save the data to redis. * * @param {string} key The key used to save. * @param {Object} data refresh keys data. * @param {Error|null} err consumer callback failure. * @param {*} response The consumer response. */ function handleRefreshResponse (key, data, err, response) { if(!err) { this.set(key, response, (data.lifeSpan / 1000), data.refresh, function(){}); } } /** * Refreshes all keys that were set with a refresh function */ function backgroundRefresh() { var keys = Object.keys(cache.refreshKeys); keys.forEach(function(key) { var data = cache.refreshKeys[key]; if(data.expiration - Date.now() < this.backgroundRefreshMinTtl){ data.refresh(key, handleRefreshResponse.bind(this, key, data)); } }, self); } /** * Return a clone of an object * @param {object} obj */ function cloneObject(obj){ return JSON.parse(JSON.stringify(obj)); } /** * Error logging logic * @param {boolean} isError * @param {string} message * @param {object} data */ function log(isError, message, data){ if(self.verbose || isError){ if(data) console.log(self.type + ': ' + message, data); else console.log(self.type + message); } } } module.exports = cacheModule;