diff --git a/README.md b/README.md index 2135f5a..c2a367d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # cache-service-cache-module -* A bare-bones cache plugin for [cache-service](https://github.com/jpodwys/cache-service) -* AND a standalone in-memory cache +* A light-weight cache plugin for [superagent-cache](https://github.com/jpodwys/superagent-cache) and [cache-service](https://github.com/jpodwys/cache-service) +* AND a standalone in-memory cache that's optionally backed by `localStorage` and `sessionStorage` #### Features +* Optionally backed by localStorage and sessionStorage * Background refresh * No external dependencies * Robust API @@ -18,7 +19,7 @@ Require and instantiate ```javascript var cModule = require('cache-service-cache-module'); -var cacheModuleConfig = {defaultExpiration: 60}; +var cacheModuleConfig = {storage: 'session', defaultExpiration: 60}; var cacheModule = new cModule(cacheModuleConfig); ``` @@ -38,6 +39,13 @@ An arbitrary identifier you can assign so you know which cache is responsible fo * type: string * default: 'cache-module' +## storage + +Indicates whether cacheModule's in-memory cache should be backed by `localStorage` or `sessionStorage`. The available options are 'local' and 'session'. If not set, or if running in node, it will default to an im-memory cache. When a browser storage is activated, cacheModule will still write to and read from an in-memory cache in the interest of speed, but at initialization it will load it's in-memory cache from browser storage and write all changes back to browser storage. + +* type: string +* default: '' + ## defaultExpiration The expiration to include when executing cache set commands. Can be overridden via `.set()`'s optional `expiraiton` param. diff --git a/cacheModule.js b/cacheModule.js index 981e6e5..c762b44 100644 --- a/cacheModule.js +++ b/cacheModule.js @@ -1,54 +1,50 @@ /** * cacheModule constructor - * @constructor * @param config: { - * type: {string | 'cache-module'} + * type: {string | 'cache-module'} * verbose: {boolean | false}, - * expiration: {integer | 900}, + * defaultExpiration: {integer | 900}, * readOnly: {boolean | false}, - * checkOnPreviousEmpty {boolean | true}, - * backgroundRefreshIntervalCheck {boolean | true}, - * backgroundRefreshInterval {integer | 60000}, - * backgroundRefreshMinTtl {integer | 70000} + * 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.verbose = config.verbose || false; self.type = config.type || 'cache-module'; + self.verbose = config.verbose || false; self.defaultExpiration = config.defaultExpiration || 900; - self.readOnly = (typeof config.readOnly === 'boolean') ? config.readOnly : false; + 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 backgroundRefreshEnabled = false; - - log(false, 'Cache-module client created with the following defaults:', {expiration: this.expiration, verbose: this.verbose, readOnly: this.readOnly}); - - /** - ******************************************* PUBLIC FUNCTIONS ******************************************* - */ + 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 - * @param {string} cleanKey */ - self.get = function(key, cb, cleanKey){ - if(arguments.length < 2){ - throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.get() requires 2 arguments.'); - } + self.get = function(key, cb){ + throwErrorIf((arguments.length < 2), 'ARGUMENT_EXCEPTION: .get() requires 2 arguments.'); log(false, 'get() called:', {key: key}); try { - var cacheKey = (cleanKey) ? cleanKey : key; var now = Date.now(); var expiration = cache.expirations[key]; if(expiration > now){ @@ -58,7 +54,6 @@ function cacheModule(config){ expire(key); cb(null, null); } - } catch (err) { cb({name: 'GetException', message: err}, null); } @@ -71,9 +66,7 @@ function cacheModule(config){ * @param {integer} index */ self.mget = function(keys, cb, index){ - if(arguments.length < 2){ - throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.mget() requires 2 arguments.'); - } + 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++){ @@ -96,17 +89,15 @@ function cacheModule(config){ * @param {function} cb */ self.set = function(){ - if(arguments.length < 2){ - throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.set() requires a minimum of 2 arguments.'); - } + 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}); - try { - if(!self.readOnly){ + if(!self.readOnly){ + try { expiration = (expiration) ? (expiration * 1000) : (self.defaultExpiration * 1000); var exp = expiration + Date.now(); cache.expirations[key] = exp; @@ -114,13 +105,12 @@ function cacheModule(config){ if(cb) cb(); if(refresh){ cache.refreshKeys[key] = {expiration: exp, lifeSpan: expiration, refresh: refresh}; - if(!backgroundRefreshEnabled){ - backgroundRefreshInit(); - } + backgroundRefreshInit(); } + overwriteBrowserStorage(); + } catch (err) { + log(true, '.set() failed for cache of type ' + self.type, {name: 'CacheModuleSetException', message: err}); } - } catch (err) { - log(true, '.set() failed for cache of type ' + self.type, {name: 'CacheModuleSetException', message: err}); } } @@ -131,9 +121,7 @@ function cacheModule(config){ * @param {function} cb */ self.mset = function(obj, expiration, cb){ - if(arguments.length < 1){ - throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.mset() requires a minimum of 1 argument.'); - } + throwErrorIf((arguments.length < 1), 'ARGUMENT_EXCEPTION: .mset() requires at least 1 argument.'); log(false, '.mset() called:', {data: obj}); for(key in obj){ if(obj.hasOwnProperty(key)){ @@ -155,9 +143,7 @@ function cacheModule(config){ * @param {function} cb */ self.del = function(keys, cb){ - if(arguments.length < 1){ - throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.del() requires a minimum of 1 argument.'); - } + 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++){ @@ -174,6 +160,7 @@ function cacheModule(config){ delete cache.refreshKeys[keys]; if(cb) cb(null, 1); } + overwriteBrowserStorage(); } /** @@ -186,11 +173,56 @@ function cacheModule(config){ cache.expirations = {}; cache.refreshKeys = {}; if(cb) cb(); + overwriteBrowserStorage(); + } + + /** + * 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') ? 'local' : 'session'; + store = (typeof Storage !== void(0)) ? window[storageType + 'Storage'] : false; + storageKey = 'cache-module-' + storageType + '-storage'; + } + if(store){ + var db = store.getItem(storageKey); + try { + cache = JSON.parse(db) || cache; + } catch (err) { /* Do nothing */ } + } + else{ + 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); + } catch (err) { /* Do nothing */ } + store.setItem(storageKey, db); + } } /** - ******************************************* PRIVATE FUNCTIONS ******************************************* + * 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 @@ -199,6 +231,7 @@ function cacheModule(config){ function expire(key){ delete cache.db[key]; delete cache.expirations[key]; + overwriteBrowserStorage(); } /** @@ -209,12 +242,10 @@ function cacheModule(config){ backgroundRefreshEnabled = true; if(self.backgroundRefreshIntervalCheck){ if(self.backgroundRefreshInterval > self.backgroundRefreshMinTtl){ - throw new exception('BACKGROUND_REFRESH_INTERVAL_EXCEPTION', 'backgroundRefreshInterval cannot be greater than backgroundRefreshMinTtl.'); + throw new Error('BACKGROUND_REFRESH_INTERVAL_EXCEPTION: backgroundRefreshInterval cannot be greater than backgroundRefreshMinTtl.'); } } - setInterval(function(){ - backgroundRefresh(); - }, self.backgroundRefreshInterval); + setInterval(backgroundRefresh, self.backgroundRefreshInterval); } } @@ -228,7 +259,7 @@ function cacheModule(config){ if(data.expiration - Date.now() < self.backgroundRefreshMinTtl){ data.refresh(key, function (err, response){ if(!err){ - self.set(key, response, (data.lifeSpan / 1000), data.refresh, noop); + self.set(key, response, (data.lifeSpan / 1000), data.refresh, function(){}); } }); } @@ -236,17 +267,6 @@ function cacheModule(config){ } } - /** - * Instantates an exception to be thrown - * @param {string} name - * @param {string} message - * @return {exception} - */ - function exception(name, message){ - this.name = name; - this.message = message; - } - /** * Error logging logic * @param {boolean} isError @@ -254,14 +274,11 @@ function cacheModule(config){ * @param {object} data */ function log(isError, message, data){ - var indentifier = 'cacheModule: '; if(self.verbose || isError){ - if(data) console.log(indentifier + message, data); - else console.log(indentifier + message); + if(data) console.log(self.type + ': ' + message, data); + else console.log(self.type + message); } } - - function noop(){} } module.exports = cacheModule; diff --git a/package.json b/package.json index 12fedaa..e56c669 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "cache-service-cache-module", - "version": "1.1.1", + "version": "1.2.0", "description": "A cache plugin for cache-service.", "main": "cacheModule.js", "devDependencies": { "mocha": "2.2.4", - "expect": "1.6.0" + "expect": "1.6.0", + "mock-localstorage": "0.1.3" }, "scripts": { "test": "bin/tests" @@ -27,6 +28,12 @@ "cache-service", "cache", "node", - "cache-module" + "cache-module", + "localStorage", + "sessionStorage", + "browser", + "node", + "superagent-cache", + "superagent" ] } diff --git a/test/server/cache-module.js b/test/server/cache-module.js index d28f241..69c0388 100644 --- a/test/server/cache-module.js +++ b/test/server/cache-module.js @@ -1,11 +1,15 @@ var expect = require('expect'); +var mockStorage = require('mock-localstorage'); +var storageMock = new mockStorage(); var cModule = require('../../cacheModule'); var cacheModule = new cModule({ - backgroundRefreshInterval: 500 + backgroundRefreshInterval: 500, + storageMock: storageMock }); var key = 'key'; var value = 'value'; +var mockName = 'cache-module-storage-mock'; beforeEach(function(){ cacheModule.flush(); @@ -22,6 +26,8 @@ describe('cacheModule Tests', function () { cacheModule.set(key, value); cacheModule.get(key, function (err, result) { expect(result).toBe(value); + var data = storageMock.getItem(mockName); + expect(data.indexOf('key')).toBeGreaterThan(-1); done(); }); }); @@ -30,6 +36,8 @@ describe('cacheModule Tests', function () { cacheModule.del(key); cacheModule.get(key, function (err, result) { expect(result).toBe(null); + var data = storageMock.getItem(mockName); + expect(data.indexOf('key')).toBe(-1); done(); }); }); @@ -44,6 +52,10 @@ describe('cacheModule Tests', function () { expect(response).toBe(null); cacheModule.get(key, function (err, response){ expect(response).toBe(null); + var data = storageMock.getItem(mockName); + expect(data.indexOf(key)).toBe(-1); + expect(data.indexOf('key2')).toBe(-1); + expect(data.indexOf('key3')).toBe(-1); done(); }); }); @@ -68,6 +80,11 @@ describe('cacheModule Tests', function () { expect(response.key2).toBe('value2'); expect(response.key3).toBe('value3'); expect(response.key4).toBe(undefined); + var data = storageMock.getItem(mockName); + expect(data.indexOf(key)).toBeGreaterThan(-1); + expect(data.indexOf('key2')).toBeGreaterThan(-1); + expect(data.indexOf('key3')).toBeGreaterThan(-1); + expect(data.indexOf('key4')).toBe(-1); done(); }); });