polyclay-redis
Version:
redis persistence adapter for polyclay, the schema-enforcing document mapper
332 lines (267 loc) • 7.28 kB
JavaScript
var
_ = require('lodash'),
events = require('events'),
redis = require('redis'),
util = require('util')
;
function RedisAdapter()
{
events.EventEmitter.call(this);
}
util.inherits(RedisAdapter, events.EventEmitter);
RedisAdapter.prototype.redis = null;
RedisAdapter.prototype.dbname = null;
RedisAdapter.prototype.constructor = null;
RedisAdapter.prototype.ephemeral = false;
RedisAdapter.prototype.options = null;
RedisAdapter.prototype.attempts = 0;
RedisAdapter.prototype.configure = function(opts, modelfunc)
{
this.options = opts;
this.dbname = opts.dbname || modelfunc.prototype.plural;
this.constructor = modelfunc;
this.ephemeral = opts.ephemeral;
this.connect();
};
RedisAdapter.prototype.connect = function()
{
this.connectTimeout = null;
this.redis = redis.createClient(this.options.port, this.options.host);
this.redis.on('error', this.handleError.bind(this));
this.redis.once('ready', this.handleReady.bind(this));
};
RedisAdapter.prototype.handleReady = function()
{
this.emit('log', 'redis @ ' + this.options.host + ':' + this.options.port + ' ready');
this.attempts = 0;
};
function exponentialBackoff(attempt)
{
return Math.min(Math.floor(Math.random() * Math.pow(2, attempt) + 10), 10000);
}
RedisAdapter.prototype.handleError = function(err)
{
if (this.connectTimeout)
return;
this.emit('log', 'error caught: ' + err);
this.attempts++;
this.redis.removeAllListeners('error');
this.connectTimeout = setTimeout(this.connect.bind(this), exponentialBackoff(this.attempts));
};
RedisAdapter.prototype.provision = function(callback)
{
// Nothing to do?
callback(null);
};
RedisAdapter.prototype.all = function(callback)
{
if (this.ephemeral)
return callback(null, []);
this.redis.smembers(this.idskey(), function(err, ids)
{
callback(err, ids);
});
};
RedisAdapter.prototype.hashKey = function(key)
{
return this.dbname + ':' + key;
};
RedisAdapter.prototype.attachmentKey = function(key)
{
return this.dbname + ':' + key + ':attaches';
};
RedisAdapter.prototype.idskey = function()
{
return this.dbname + ':ids';
};
RedisAdapter.prototype.save = function(object, json, callback)
{
if (!object.key || !object.key.length)
throw(new Error('cannot save a document without a key'));
var payload = RedisAdapter.flatten(json);
var okey = this.hashKey(object.key);
var chain = this.redis.multi();
chain.hmset(okey, payload.body);
if (!this.ephemeral)
chain.sadd(this.idskey(), object.key);
if (Object.keys(payload.attachments).length)
chain.hmset(this.attachmentKey(object.key), payload.attachments);
if (this.ephemeral)
{
if (_.isNumber(object.ttl) && object.ttl > 0)
chain.expire(okey, object.ttl);
else if (_.isNumber(object.expire_at) && object.expire_at > 0)
chain.expireat(okey, Math.floor(object.expire_at));
}
chain.exec(function(err, replies)
{
callback(err, replies[0]);
});
};
RedisAdapter.prototype.update = RedisAdapter.prototype.save;
RedisAdapter.prototype.get = function(key, callback)
{
var self = this;
if (Array.isArray(key))
return this.getBatch(key, callback);
var chain = this.redis.multi();
var hkey = this.hashKey(key);
chain.hgetall(hkey);
chain.ttl(hkey);
chain.exec(function(err, jsondocs)
{
if (err) return callback(err);
var item = jsondocs[0];
if (!item) return callback(null, null);
var object = self.inflate(item);
if (self.ephemeral)
{
var ttl = jsondocs[1];
object.ttl = ttl;
object.expire_at = Math.floor(Date.now() / 1000 + ttl);
}
callback(null, object);
});
};
RedisAdapter.prototype.getBatch = function(keylist, callback)
{
var self = this;
var chain = this.redis.multi();
_.each(keylist, function(item)
{
var hkey = self.hashKey(item);
chain.hgetall(hkey);
chain.ttl(hkey);
});
chain.exec(function(err, jsondocs)
{
if (err) return callback(err);
var results = [];
for (var i = 0, len = jsondocs.length; i < len--; i += 2)
{
var item = jsondocs[i];
if (!item) continue;
var object = self.inflate(item);
if (self.ephemeral)
{
var ttl = jsondocs[i + 1];
object.ttl = ttl;
object.expire_at = Math.floor(Date.now() / 1000 + ttl);
}
results.push(object);
}
callback(err, results);
});
};
RedisAdapter.prototype.merge = function(key, attributes, callback)
{
this.redis.hmset(this.hashKey(key), RedisAdapter.flatten(attributes).body, callback);
};
RedisAdapter.prototype.remove = function(object, callback)
{
var chain = this.redis.multi();
chain.del(this.hashKey(object.key));
chain.del(this.attachmentKey(object.key));
if (!this.ephemeral)
chain.srem(this.idskey(), object.key);
chain.exec(function(err, replies)
{
callback(err, replies[0]);
});
};
RedisAdapter.prototype.destroyMany = function(objects, callback)
{
var self = this;
var ids = _.map(objects, function(obj)
{
if (typeof obj === 'string')
return obj;
return obj.key;
});
var idkey = this.idskey();
var chain = this.redis.multi();
if (!this.ephemeral)
_.each(ids, function(id) { chain.srem(idkey, id); });
chain.del(_.map(ids, function(key) { return self.hashKey(key); }));
chain.del(_.map(ids, function(key) { return self.attachmentKey(key); }));
chain.exec(function(err, replies)
{
callback(err);
});
};
RedisAdapter.prototype.attachment = function(key, name, callback)
{
this.redis.hget(this.attachmentKey(key), name, function(err, payload)
{
if (err) return callback(err);
if (!payload) return callback(null, null);
var struct = JSON.parse(payload);
if (struct && struct.body && _.isObject(struct.body))
struct.body = new Buffer(struct.body);
callback(null, struct.body);
});
};
RedisAdapter.prototype.saveAttachment = function(object, attachment, callback)
{
this.redis.hset(this.attachmentKey(object.key), attachment.name, JSON.stringify(attachment), callback);
};
RedisAdapter.prototype.removeAttachment = function(object, name, callback)
{
this.redis.hdel(this.attachmentKey(object.key), name, callback);
};
RedisAdapter.prototype.inflate = function(payload)
{
if (payload === null)
return;
var object = new this.constructor();
var json = {};
json._attachments = {};
var matches;
var fields = Object.keys(payload).sort();
for (var i = 0; i < fields.length; i++)
{
var field = fields[i];
try
{
json[field] = JSON.parse(payload[field]);
}
catch (e)
{
json[field] = payload[field];
}
}
object.initFromStorage(json);
return object;
};
RedisAdapter.flatten = function(json)
{
var payload = { body: {} };
var i;
if (json._attachments)
{
payload.attachments = {};
var attaches = Object.keys(json._attachments);
for (i = 0; i < attaches.length; i++)
{
var attachment = json._attachments[attaches[i]];
var item =
{
body: attachment.body,
content_type: attachment.content_type,
length: attachment.length,
name: attaches[i]
};
payload.attachments[item.name] = JSON.stringify(item);
}
delete json._attachments;
}
var fields = Object.keys(json).sort();
for (i = 0; i < fields.length; i++)
{
var field = fields[i];
payload.body[field] = JSON.stringify(json[field]);
}
return payload;
};
//-----------------------------------------------------------------
module.exports = RedisAdapter;