Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 81 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
* A [node-cache](https://github.com/tcs-de/nodecache) plugin for [cache-service](https://github.com/jpodwys/cache-service)
* AND a standalone node-cache wrapper

#### Features

* Background refresh
* Robust API
* Built-in logging with a `verbose` flag.
* Compatible with `cache-service` and `superagent-cache`
* Public access to the underlying `node-cache` instance
* Excellent `.mset()` implementation which allows you to set expirations on a per key, per function call, and/or per `cache-service-cache-module` instance basis.

# Basic Usage

Require and instantiate
Expand All @@ -18,15 +27,10 @@ Cache!
nodeCache.set('key', 'value');
```

# Benefits of Using `cache-service-node-cache`

If you're using `cache-service-node-cache` with `cache-service`, the benefits are obvious. However, there are also a couple of reasons you might prefer it to using vanilla [node-cache](https://www.npmjs.com/package/node-cache):

* It adds an excellent `.mset()` implementation which allow you to set expirations on a per key, per function call, and/or per `cache-service-node-cache` instance basis (Vanilla node-cache does not offer `.mset()` at all).
* Built-in logging with a `verbose` flag.

# Cache Module Configuration Options

`cache-service-node-cache`'s constructor takes an optional config object with any number of the following properties:

## type

An arbitrary identifier you can assign so you know which cache is responsible for logs and errors.
Expand All @@ -42,6 +46,29 @@ The expiration to include when executing cache set commands. Can be overridden v
* default: 900
* measure: seconds

## backgroundRefreshInterval

How frequently should all background refresh-enabled keys be scanned to determine whether they should be refreshed. For a more thorough explanation on `background refresh`, see the [Using Background Refresh](#using-background-refresh) section.

* type: int
* default: 60000
* measure: milliseconds

## backgroundRefreshMinTtl

The maximum ttl a scanned background refresh-enabled key can have without triggering a refresh. This number should always be greater than `backgroundRefreshInterval`.

* type: int
* default: 70000
* measure: milliseconds

## backgroundRefreshIntervalCheck

Whether to throw an exception if `backgroundRefreshInterval` is greater than `backgroundRefreshMinTtl`. Setting this property to false is highly discouraged.

* type: boolean
* default: true

## verbose

> When used with `cache-service`, this property is overridden by `cache-service`'s `verbose` value.
Expand Down Expand Up @@ -73,16 +100,19 @@ Retrieve the values belonging to a series of keys. If a key is not found, it wil
* err: type: object
* response: type: object, example: {key: 'value', key2: 'value2'...}

## .set(key, value [, expiraiton, callback])
## .set(key, value, [expiraiton], [refresh(key, cb)], [callback])

> See the [Using Background Refresh](#using-background-refresh) section for more about the `refresh` and `callback` params.

Set a value by a given key.

* key: type: string
* callback: type: function
* expiration: type: int, measure: seconds
* refresh: type: function
* callback: type: function

## .mset(obj [, expiration, callback])
## .mset(obj, [expiration], [callback])

Set multiple values to multiple keys

Expand All @@ -94,7 +124,7 @@ This function exposes a heirarchy of expiration values as follows:
* If an object with both `cacheValue` and `expiration` as properties is not present, the `expiration` provided to the `.mset()` argument list will be used.
* If neither of the above is provided, each cache's `defaultExpiration` will be applied.

## .del(keys [, callback (err, count)])
## .del(keys, [callback (err, count)])

Delete a key or an array of keys and their associated values.

Expand All @@ -109,11 +139,48 @@ Flush all keys and values from.

* callback: type: function

# More Node-Cache Methods
## .db

This is the underlying [`node-cache` instance](https://github.com/tcs-de/nodecache). If needed, you can access `node-cache` functions I haven't abstracted.

# Using Background Refresh

With a typical cache setup, you're left to find the perfect compromise between having a long expiration so that users don't have to suffer through the worst case load time, and a short expiration so data doesn't get stale. `cache-service-cache-module` eliminates the need to worry about users suffering through the longest wait time by automatically refreshing keys for you. Here's how it works:

#### How do I turn it on?

By default, background refresh is off. It will turn itself on the first time you pass a `refresh` param to `.set()`.

#### Configure

There are three options you can manipulate. See the API section for more information about them.

* `backgroundRefreshInterval`
* `backgroundRefreshMinTtl`
* `backgroundRefreshIntervalCheck`

#### Use

Background refresh is exposed via the `.set()` command as follows:

```javascript
cacheModule.set('key', 'value', 300, refresh, cb);
```

If you want to pass `refresh`, you must also pass `cb` because if only four params are passed, `cache-service-node-cache` will assume the fourth param is `cb`.

#### The Refresh Param

###### refresh(key, cb(err, response))

* key: type: string: this is the key that is being refreshed
* cb: type: function: you must trigger this function to pass the data that should replace the current key's value

If you need access to one of node-cache's other functions, you can get at the underlying [`node-cache` instance](https://github.com/tcs-de/nodecache) by tapping into the `.db` property like so:
The `refresh` param MUST be a function that accepts `key` and a callback function that accepts `err` and `response` as follows:

```javascript
var underlyingNodeCacheInstance = nodeCacheModule.db;
underlyingNodeCacheInstance.SOME_OTHER_NODE_CACHE_FUNCTION();
var refresh = function(key, cb){
var response = goGetData();
cb(null, response);
}
```
123 changes: 105 additions & 18 deletions nodeCacheModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ var nodeCache = require('node-cache');
* nodeCacheModule constructor
* @constructor
* @param config: {
* type: {string | 'node-cache'}
* verbose: {boolean | false},
* expiration: {integer | 900},
* readOnly: {boolean | false},
* checkOnPreviousEmpty {boolean | true}
* type: {string | 'node-cache'}
* verbose: {boolean | false},
* expiration: {integer | 900},
* readOnly: {boolean | false},
* checkOnPreviousEmpty {boolean | true},
* backgroundRefreshIntervalCheck {boolean | true},
* backgroundRefreshInterval {integer | 60000},
* backgroundRefreshMinTtl {integer | 70000}
* }
*/
function nodeCacheModule(config){
Expand All @@ -18,6 +21,10 @@ function nodeCacheModule(config){
self.defaultExpiration = config.defaultExpiration || 900;
self.readOnly = (typeof config.readOnly === 'boolean') ? config.readOnly : false;
self.checkOnPreviousEmpty = (typeof config.checkOnPreviousEmpty === 'boolean') ? config.checkOnPreviousEmpty : true;
self.backgroundRefreshInterval = config.backgroundRefreshInterval || 60000;
self.backgroundRefreshMinTtl = config.backgroundRefreshMinTtl || 70000;
var refreshKeys = {};
var backgroundRefreshEnabled = false;

/**
******************************************* PUBLIC FUNCTIONS *******************************************
Expand All @@ -30,10 +37,12 @@ function nodeCacheModule(config){
* @param {string} cleanKey
*/
self.get = function(key, cb, cleanKey){
log(false, 'Attempting to get key:', {key: key});
if(arguments.length < 2){
throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.get() requires 2 arguments.');
}
log(false, 'get() called:', {key: key});
try {
var cacheKey = (cleanKey) ? cleanKey : key;
log(false, 'Attempting to get key:', {key: cacheKey});
self.db.get(cacheKey, function(err, result){
cb(err, result);
});
Expand All @@ -49,7 +58,10 @@ function nodeCacheModule(config){
* @param {integer} index
*/
self.mget = function(keys, cb, index){
log(false, 'Attempting to mget keys:', {keys: keys});
if(arguments.length < 2){
throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.mget() requires 2 arguments.');
}
log(false, '.mget() called:', {keys: keys});
self.db.mget(keys, function (err, response){
cb(err, response, index);
});
Expand All @@ -60,18 +72,32 @@ function nodeCacheModule(config){
* @param {string} key
* @param {string | object} value
* @param {integer} expiration
* @param {function} refresh
* @param {function} cb
*/
self.set = function(key, value, expiration, cb){
log(false, 'Attempting to set key:', {key: key, value: value});
self.set = function(){
if(arguments.length < 2){
throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.set() requires a minimum of 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];
cb = cb || noop;
log(false, '.set() called:', {key: key, value: value});
try {
if(!self.readOnly){
expiration = expiration || self.defaultExpiration;
cb = cb || noop;
var exp = (expiration * 1000) + Date.now();
self.db.set(key, value, expiration, cb);
if(refresh){
refreshKeys[key] = {expiration: exp, lifeSpan: expiration, refresh: refresh};
backgroundRefreshInit();
}
}
} catch (err) {
log(true, 'Set failed for cache of type ' + self.type, {name: 'NodeCacheSetException', message: err});
log(true, '.set() failed for cache of type ' + self.type, {name: 'NodeCacheSetException', message: err});
}
}

Expand All @@ -82,7 +108,10 @@ function nodeCacheModule(config){
* @param {function} cb
*/
self.mset = function(obj, expiration, cb){
log(false, 'Attempting to mset data:', {data: obj});
if(arguments.length < 1){
throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.mset() requires a minimum of 1 argument.');
}
log(false, '.mset() called:', {data: obj});
for(key in obj){
if(obj.hasOwnProperty(key)){
var tempExpiration = expiration || self.defaultExpiration;
Expand All @@ -103,15 +132,27 @@ function nodeCacheModule(config){
* @param {function} cb
*/
self.del = function(keys, cb){
log(false, 'Attempting to delete keys:', {keys: keys});
if(arguments.length < 1){
throw new exception('INCORRECT_ARGUMENT_EXCEPTION', '.del() requires a minimum of 1 argument.');
}
log(false, '.del() called:', {keys: keys});
try {
self.db.del(keys, function (err, count){
if(cb){
cb(err, count);
}
});
if(typeof keys === 'object'){
for(var i = 0; i < keys.length; i++){
var key = keys[i];
delete refreshKeys[key];
}
}
else{
delete refreshKeys[keys];
}
} catch (err) {
log(true, 'Delete failed for cache of type ' + this.type, err);
log(true, '.del() failed for cache of type ' + this.type, err);
}
}

Expand All @@ -120,12 +161,12 @@ function nodeCacheModule(config){
* @param {function} cb
*/
self.flush = function(cb){
log(false, 'Attempting to flush all data.');
log(false, '.flush() called');
try {
self.db.flushAll();
log(false, 'Flushing all data from cache of type ' + this.type);
refreshKeys = {};
} catch (err) {
log(true, 'Flush failed for cache of type ' + this.type, err);
log(true, '.flush() failed for cache of type ' + this.type, err);
}
if(cb) cb();
}
Expand All @@ -148,6 +189,52 @@ function nodeCacheModule(config){
self.type = config.type || 'node-cache';
}

/**
* Initialize background refresh
*/
function backgroundRefreshInit(){
if(!backgroundRefreshEnabled){
backgroundRefreshEnabled = true;
if(self.backgroundRefreshIntervalCheck){
if(self.backgroundRefreshInterval > self.backgroundRefreshMinTtl){
throw new exception('BACKGROUND_REFRESH_INTERVAL_EXCEPTION', 'backgroundRefreshInterval cannot be greater than backgroundRefreshMinTtl.');
}
}
setInterval(function(){
backgroundRefresh();
}, self.backgroundRefreshInterval);
}
}

/**
* Refreshes all keys that were set with a refresh function
*/
function backgroundRefresh(){
for(key in refreshKeys){
if(refreshKeys.hasOwnProperty(key)){
var data = refreshKeys[key];
if(data.expiration - Date.now() < self.backgroundRefreshMinTtl){
data.refresh(key, function (err, response){
if(!err){
self.set(key, response, data.lifeSpan, data.refresh, noop);
}
});
}
}
}
}

/**
* 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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cache-service-node-cache",
"version": "1.0.1",
"version": "1.1.0",
"description": "A node-cache plugin for cache-service.",
"main": "nodeCacheModule.js",
"dependencies": {
Expand Down
17 changes: 16 additions & 1 deletion test/server/node-cache-module.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
var expect = require('expect');
var ncModule = require('../../nodeCacheModule');
var nodeCache = new ncModule();
var nodeCache = new ncModule({
backgroundRefreshInterval: 500
});

var key = 'key';
var value = 'value';
Expand Down Expand Up @@ -64,4 +66,17 @@ describe('nodeCacheModule Tests', function () {
done();
});
});
it('Using background refresh should reset a nearly expired key', function (done) {
var refresh = function(key, cb){
cb(null, 1);
}
nodeCache.set(key, value, 1, refresh, function (err, result){
setTimeout(function(){
nodeCache.get(key, function (err, response){
expect(response).toBe(1);
done();
});
}, 1500);
});
});
});