Skip to content
This repository has been archived by the owner on Feb 18, 2021. It is now read-only.

Commit

Permalink
The Initial CacheRedis
Browse files Browse the repository at this point in the history
  • Loading branch information
David Ellis committed Mar 22, 2014
1 parent 1a02e1a commit 635f91b
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ notifications:
- d.f.ellis@ieee.org
on_success: change
on_failure: change

services: redis
82 changes: 79 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,81 @@
function cacheRedis() {

var redisManager = require('redis-manager');
var EventEmitter = require('events').EventEmitter;
var util = require('util');

function CacheRedis(config) {
EventEmitter.call(this);
this.host = config.host || 'localhost';
this.port = config.port || 6379;
this.redisConfig = config.redisConfig || {};
this.namespace = config.namespace;
this.lazy = config.lazy || false;
this.redisClient = redisManager.getClient(this.port, this.host, this.redisConfig);
this.redisClient.on('error', this.emit.bind(this, 'error'));
this.cache = {};

if (!this.lazy) {
this.redisClient.hgetall(this.namespace, function(err, cache) {
if (err) this.emit('error', err);
if (cache) this.cache = cache;
}.bind(this));
}
}

module.exports = cacheRedis;
util.inherits(CacheRedis, EventEmitter);

CacheRedis.prototype.close = function() {
redisManager.freeClient(this.redisClient);
};

CacheRedis.prototype.set = function(key, val, callback) {
this.cache[key] = val;
this.redisClient.hset(this.namespace, key, val, callback);
};

CacheRedis.prototype.getLocal = function(key) {
return this.cache[key];
};

CacheRedis.prototype.get = function(key, callback) {
if (this.cache.hasOwnProperty(key)) return process.nextTick(callback.bind(this, null, this.cache[key]));
this.redisClient.hget(this.namespace, key, function(err, val) {
if (err) return callback(err);
this.cache[key] = val || undefined;
callback(null, this.cache[key]);
}.bind(this));
};

CacheRedis.prototype.keysLocal = function() {
return Object.keys(this.cache);
};

CacheRedis.prototype.keys = function(callback) {
this.redisClient.hkeys(this.namespace, callback);
};

CacheRedis.prototype.valuesLocal = function() {
return Object.keys(this.cache).map(function(key) { return this.cache[key]; }.bind(this));
};

CacheRedis.prototype.values = function(callback) {
this.redisClient.hvals(this.namespace, callback);
};

CacheRedis.prototype.has = function(key, callback) {
if (this.cache.hasOwnProperty(key)) {
process.nextTick(callback.bind(this, true));
} else {
this.redisClient.hexists(this.namespace, key, function(err, result) {
if (err) return callback(false);
callback(!!result); // Cast to boolean
});
}
};

CacheRedis.prototype.hasLocal = function(key) {
return this.cache.hasOwnProperty(key);
};

// TODO: Add other ES6 Map methods: items, forEach, iterator, delete, clear, toString, and the property size

module.exports = CacheRedis;
17 changes: 17 additions & 0 deletions lib/mutate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = function mutate(obj, key, val, scope) {
return function(test) {
var old = obj[key];
obj[key] = val;
var boundTestDone = test.end.bind(test);
test.end = function() {
obj[key] = old;
boundTestDone();
};
try {
scope.apply(this, arguments);
} catch(e) {
obj[key] = old;
throw e;
}
};
};
244 changes: 241 additions & 3 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,246 @@
var CacheRedis = require('..');
var redisManager = require('redis-manager');
var mutate = require('../lib/mutate');
var test = require('tape');

var cacheRedis = require('../index.js');
var redisClient = redisManager.getClient();

test('cacheRedis is a function', function (assert) {
assert.strictEqual(typeof cacheRedis, 'function');
test('clear', function(assert) {
redisClient.flushall(function() {
assert.end();
});
});

function createCache() {
return new CacheRedis({
namespace: 'cacheTest'
});
}

test('initialize', function(assert) {
assert.plan(5);
var cache = createCache();
assert.equal(cache.host, 'localhost', 'got default host');
assert.equal(cache.port, 6379, 'got default port');
assert.equal(cache.namespace, 'cacheTest', 'got passed in cache');
assert.equal(cache.lazy, false, 'greedy by default');
assert.ok(cache.redisClient.flushall instanceof Function, 'correctly constructed a redis object');
cache.close();
assert.end();
});

test('emits error if error during initialization', mutate(redisManager, 'getClient', function() {
return {
on: function() { },
hgetall: function(namespace, callback) {
console.log(arguments);
callback(new Error('initialization error'));
}
};
}, function(assert) {
assert.plan(1);
// Not possible to listen for events emitted during construction
assert.throws(function() {
createCache();
}, /initialization error/, 'got the mocked error');
assert.end();
}));

test('set', function(assert) {
assert.plan(3);
var cache = createCache();
assert.doesNotThrow(function() {
cache.set('foo', 'bar');
}, 'can set fire and forget');
cache.set('foo2', 'bar2', function() {
redisClient.hget('cacheTest', 'foo2', function(err, val) {
assert.ifError(err, 'does not fail to get from redis');
assert.equal(val, cache.cache.foo2, 'key written to redis and matches memory');
cache.close();
assert.end();
});
});
});

test('getLocal', function(assert) {
assert.plan(1);
var cache = createCache();
setTimeout(function() {
var val = cache.getLocal('foo');
assert.equal(val, 'bar', 'got the expected value even from a separate constructor obj');
cache.close();
assert.end();
}, 50);
});

test('getLocal non existent', function(assert) {
assert.plan(1);
var cache = createCache();
var val = cache.getLocal('nonexistent');
assert.equal(val, undefined, 'got the expected undefined value for a nonexistent key');
cache.close();
assert.end();
});

test('get', function(assert) {
assert.plan(2);
var cache = createCache();
cache.get('foo2', function(err, val) {
assert.ifError(err, 'should not get an error back');
assert.equal(val, 'bar2', 'got the expected value from a separate constructor obj');
cache.close();
assert.end();
});
});

test('get already cached value', function(assert) {
assert.plan(2);
var cache = createCache();
setTimeout(function() {
cache.get('foo2', function(err, val) {
assert.ifError(err, 'should not get an error back');
assert.equal(val, 'bar2', 'got the expected value from a separate constructor obj');
cache.close();
assert.end();
});
}, 50);
});

test('getLazy', function(assert) {
assert.plan(3);
var cache = new CacheRedis({
namespace: 'cacheTest',
lazy: true
});
assert.ok(!cache.cache.foo2, 'key not yet in memory');
cache.get('foo2', function(err, val) {
assert.ifError(err, 'should not get an error back');
assert.equal(val, 'bar2', 'got the expected value even though not originally in memory');
cache.close();
assert.end();
});
});

test('get non existent', function(assert) {
assert.plan(2);
var cache = createCache();
cache.get('nonexistent', function(err, result) {
assert.ifError(err, 'got no error');
assert.equal(result, undefined, 'got no result');
cache.close();
assert.end();
});
});

test('get redis failure', mutate(redisClient, 'hget', function(namespace, key, callback) {
callback(new Error('fake error'));
}, function (assert) {
assert.plan(2);
var cache = new CacheRedis({
namespace: 'cacheTest',
lazy: true
});
redisManager.freeClient(cache.redisClient);
cache.redisClient = redisClient;
cache.get('foo2', function(err) {
assert.ok(err instanceof Error, 'got an error back');
assert.equal(err.message, 'fake error', 'got the fake redisClient error back');
assert.end();
});
}));

test('keysLocal', function(assert) {
assert.plan(1);
var cache = createCache();
setTimeout(function() {
assert.deepEqual(cache.keysLocal().sort(), ['foo', 'foo2'], 'got both keys');
cache.close();
assert.end();
}, 50);
});

test('keys', function(assert) {
assert.plan(1);
var cache = createCache();
cache.keys(function(err, keys) {
assert.deepEqual(keys.sort(), ['foo', 'foo2'], 'got both keys');
cache.close();
assert.end();
});
});

test('valuesLocal', function(assert) {
assert.plan(1);
var cache = createCache();
setTimeout(function() {
assert.deepEqual(cache.valuesLocal().sort(), ['bar', 'bar2'], 'got both values');
cache.close();
assert.end();
}, 50);
});

test('values', function(assert) {
assert.plan(1);
var cache = createCache();
cache.values(function(err, values) {
assert.deepEqual(values.sort(), ['bar', 'bar2'], 'got both values');
cache.close();
assert.end();
});
});

test('has', function(assert) {
assert.plan(2);
var cache = createCache();
cache.has('foo', function(has) {
assert.equal(has, true, 'has that key');
cache.has('lolno', function(has) {
assert.equal(has, false, 'does not have that key');
cache.close();
assert.end();
});
});
});

test('has already cached', function(assert) {
assert.plan(1);
var cache = createCache();
setTimeout(function() {
cache.has('foo', function(has) {
assert.equal(has, true, 'has that key');
cache.close();
assert.end();
});
}, 50);
});

test('has returns false on error', mutate(redisClient, 'hexists', function(namespace, key, callback) {
callback(new Error('Rar! This is an error of some sort!'));
}, function(assert) {
assert.plan(1);
var cache = createCache();
redisManager.freeClient(cache.redisClient);
cache.redisClient = redisClient;
cache.has('foo', function(has) {
assert.equal(has, false, 'returns false if having issues with redis');
assert.end();
});
}));

test('hasLocal', function(assert) {
assert.plan(2);
var cache = createCache();
setTimeout(function() {
assert.equal(cache.hasLocal('foo'), true, 'has that key');
assert.equal(cache.hasLocal('lolno'), false, 'does not have that key');
cache.close();
assert.end();
}, 50);
});

test('clearAgain', function (assert) {
redisClient.flushall(function() {
redisManager.freeClient(redisClient);
assert.end();
});
});

0 comments on commit 635f91b

Please sign in to comment.