Skip to content

Commit

Permalink
Merge pull request #2 from jpodwys/browser-storage
Browse files Browse the repository at this point in the history
Browser storage
  • Loading branch information
Joe Podwys committed Dec 21, 2015
2 parents 7575dc8 + f2180bf commit fd5026d
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 71 deletions.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
```

Expand All @@ -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.
Expand Down
145 changes: 81 additions & 64 deletions cacheModule.js
Original file line number Diff line number Diff line change
@@ -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){
Expand All @@ -58,7 +54,6 @@ function cacheModule(config){
expire(key);
cb(null, null);
}

} catch (err) {
cb({name: 'GetException', message: err}, null);
}
Expand All @@ -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++){
Expand All @@ -96,31 +89,28 @@ 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;
cache.db[key] = value;
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});
}
}

Expand All @@ -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)){
Expand All @@ -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++){
Expand All @@ -174,6 +160,7 @@ function cacheModule(config){
delete cache.refreshKeys[keys];
if(cb) cb(null, 1);
}
overwriteBrowserStorage();
}

/**
Expand All @@ -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
Expand All @@ -199,6 +231,7 @@ function cacheModule(config){
function expire(key){
delete cache.db[key];
delete cache.expirations[key];
overwriteBrowserStorage();
}

/**
Expand All @@ -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);
}
}

Expand All @@ -228,40 +259,26 @@ 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(){});
}
});
}
}
}
}

/**
* 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
* @param {string} message
* @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;
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -27,6 +28,12 @@
"cache-service",
"cache",
"node",
"cache-module"
"cache-module",
"localStorage",
"sessionStorage",
"browser",
"node",
"superagent-cache",
"superagent"
]
}
Loading

0 comments on commit fd5026d

Please sign in to comment.