lib/backend.js
"use strict";
// Error replies.
var ERROR = function (message) {
this.getError = function () { return message; };
this.toString = function () { return "<ERROR<" + message + ">>"; };
};
var BAD_TYPE = new ERROR('Operation against a key holding the wrong kind of value');
var BAD_KEY = new ERROR('no such key');
var BAD_INT = new ERROR('value is not an integer or out of range');
var BAD_FLOAT = new ERROR('value is not a valid float');
var BAD_ARGS = new ERROR('wrong number of arguments');
var BAD_SYNTAX = new ERROR('syntax error');
var BAD_INDEX = new ERROR('index out of range');
var BAD_SORT = new ERROR('One or more scores can\'t be converted into double');
var BAD_BIT1 = new ERROR('bit offset is not an integer or out of range');
var BAD_BIT2 = new ERROR('bit is not an integer or out of range');
var BAD_SETEX = new ERROR('invalid expire time in SETEX');
var BAD_ZUIS = new ERROR('at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE');
// Status replies.
var STATUS = function (message) {
this.getStatus = function () { return message; };
this.toString = function () { return "<STATUS<" + message + ">>"; };
};
var OK = new STATUS('OK');
var PONG = new STATUS('PONG');
var NONE = new STATUS('none');
// Redis types.
var VALID_TYPE = function () {};
var TYPE = function (type, makePrimitive) {
var Constr = function (value) {
if (!(this instanceof VALID_TYPE))
return new Constr(value);
if (!value)
value = makePrimitive();
this.value = value;
};
Constr.getStatus = function () { return type; };
Constr.prototype = new VALID_TYPE;
Constr.prototype.toString = function () { return "<TYPE<" + type + ">>"; };
Constr.prototype.TYPE = Constr;
return Constr;
};
var EMPTY_STR = { toString: function () { return ""; }, length: 0, copy: function () {} };
var STRING = TYPE("string", function () { return EMPTY_STR; });
var LIST = TYPE("list", function () { return []; });
var HASH = TYPE("hash", function () { return {}; });
var SET = TYPE("set", function () { return {}; });
var ZSET = TYPE("zset", function () { return {}; });
// Utils.
var arr = function (obj) {
var i, n = obj.length, out = [];
for (i = 0; i < n; i++)
out[i] = obj[i];
return out;
};
var range = function (min, max) {
var xlo, xhi;
if ((xlo = min.substr(0, 1) === '('))
min = str2float(min.substr(1));
else
min = str2float(min);
if (min instanceof ERROR)
return min;
if ((xhi = max.substr(0, 1) === '('))
max = str2float(max.substr(1));
else
max = str2float(max);
if (max instanceof ERROR)
return max;
return function (num) {
return !((xlo && num <= min) || (num < min) || (xhi && num >= max) || (num > max));
};
};
var slice = function (arr, start, stop, asCount) {
start = str2int(start);
stop = str2int(stop);
if (start instanceof ERROR) return start;
if (stop instanceof ERROR) return stop;
if (arr.slice) {
var n = arr.length;
if (asCount) {
if (start < 0) {
start = 0; // Redis is inconsistent about this, ZRANGEBYSCORE will return an empty multibulk on negative offset
stop = 0; // whilst SORT will return as if the offset was 0. Best to lint these away with client-side errors.
}
else if (stop < 0) stop = n;
else stop += start;
}
else {
if (start < 0) start = n + start;
if (stop < 0) stop = n + stop;
stop++;
}
if (start >= stop)
return [];
else
return arr.slice(start < 0 ? 0 : start, stop > n ? n : stop);
}
else
return arr;
};
var str2float = function (string) {
var value = Number(string);
if (typeof string !== 'string') throw new Error("WOOT! str2float: '" + string + "' not a string.");
if (string === '+inf') value = Number.POSITIVE_INFINITY;
else if (string === '-inf') value = Number.NEGATIVE_INFINITY;
else if (!string || (!value && value !== 0)) return BAD_FLOAT;
return value;
};
var str2int = function (string) {
var value = str2float(string);
if (value instanceof ERROR || value % 1 !== 0) return BAD_INT;
return value;
};
var pattern = function (string) {
string = string.replace(/([+{($^|.\\])/g, '\\' + '$1');
string = string.replace(/(^|[^\\])([*?])/g, '$1.$2');
string = '^' + string + '$';
var pattern = new RegExp(string);
return pattern.test.bind(pattern);
};
var populationCount = function (data) {
var count;
for (count = 0; data > 0; count++) {
data &= data - 1;
}
return count;
};
// Keyspace and pubsub.
exports.Backend = function () {
var state
, dbs = {}
, delrev = {}
, rev = 0
, subs = []
, call = []
, tick = false
, nextTick = function () {
var c, func;
tick = false;
c = call.splice(0, call.length);
while ((func = c.shift())) func();
};
// Select.
// Selected keyspace is NOT relevant to pubsub.
this.selectDB = function (id) {
if (typeof id !== "number" || id % 1 !== 0)
throw new Error("Invalid database id: " + id);
// Select or instantiate.
var db = dbs[id] || (dbs[id] = {});
state = db;
};
// Connections start in database 0.
this.selectDB(0);
// Typed getKey.
this.getKey = function (Type, key, make) {
var entry = state[key];
if (Type && !Type.getStatus)
throw new Error("WOOT! Type param for getKey is not a valid Type.");
if (key === undefined)
throw new Error("WOOT! key param for getKey is undefined.");
if (entry) {
if (entry.expire < Date.now()) {
delete state[key];
delrev[key] = ++rev;
entry = null;
}
else if (!(entry.value instanceof VALID_TYPE))
throw new Error("WOOT! keyspace entry value is not a valid Type.");
}
if (Type) {
if (entry && !(entry.value instanceof Type))
return BAD_TYPE;
if (!entry && make)
return new Type;
}
return (entry && entry.value) || null;
};
this.setKey = function (key, value) {
if (value) {
if (!(value instanceof VALID_TYPE))
throw new Error("WOOT! Value doesn't have a valid type.");
rev++;
state[key] = { value: value };
state[key].rev = rev;
delete delrev[key];
this.pub(this.UPDATE, key);
return 1;
}
else if (state[key]) {
rev++;
delrev[key] = rev;
delete state[key];
return 1;
}
return 0;
};
this.upsetKey = function (key, value) {
if (!value)
throw new Error("WOOT! Update key with a falsy value.");
if (!(value instanceof VALID_TYPE))
throw new Error("WOOT! Value doesn't have a valid type.");
if (state[key] && state[key].expire >= Date.now()) {
if (state[key].value !== value)
throw new Error("WOOT! Chaning value containers during upsetKey.");
rev++;
state[key].value = value;
state[key].rev = rev;
this.pub(this.UPDATE, key);
}
else
this.setKey(key, value);
};
this.getExpire = function (key) {
var entry = state[key];
if (!entry || entry.expire < Date.now()) {
delete state[key];
return null;
}
return entry.expire;
};
this.setExpire = function (key, expire) {
var entry = state[key];
if (!entry || entry.expire < Date.now()) {
delete state[key];
return 0;
}
else if (expire) {
entry.expire = expire;
return 1;
}
else if (entry.expire) {
delete entry.expire;
return 1;
}
return 0;
};
this.getKeys = function () {
var keys = []
, key;
for (key in state)
if (this.getKey(null, key))
keys.push(key);
return keys;
};
this.renameKey = function (keyA, keyB) {
if (!this.getKey(null, keyA))
return false;
rev++;
state[keyB] = state[keyA];
state[keyB].rev = rev++;
delete state[keyA];
this.pub(this.UPDATE, keyB);
return true;
};
// Keyspace change event.
this.UPDATE = new STATUS("Key value updated.");
// For implementing watch and stuff.
this.getRevision = function (key) {
this.getKey(null, key);
return (state[key] && state[key].rev) || delrev[key] || 0;
};
// Publish / subscribe backend.
this.pub = function (channel, message) {
if (!channel && channel !== '') throw new Error("WOOT! Publishing to a falsy, non-string channel : [" + channel + '] ' + message);
if (!message && message !== '') throw new Error("WOOT! Publishing a falsy, non-string message : [" + channel + '] ' + message);
var i, n = subs.length, sub, x = 0;
for (i = 0; i < n; i++) {
sub = subs[i];
if (sub.channel === channel || (sub.pattern !== null && sub.channel(channel))) {
if (sub.pattern !== null)
call.push(sub.client.pushMessage.bind(sub.client, 'pmessage', sub.pattern, channel, message));
else
call.push(sub.client.pushMessage.bind(sub.client, 'message', channel, message));
x++;
if (!tick) {
tick = true;
process.nextTick(nextTick);
}
}
}
return x;
};
// p - true/false
// channel - string
// client { push ( pattern, channel, message ) }
this.sub = function (p, channel, client) {
if (!channel && channel !== '') throw new Error("WOOT! Subscribing to a falsy, non-string channel : [" + channel + ']');
if (!client || !client.pushMessage) throw new Error("WOOT! Subscribing an invalid client : " + client);
if (typeof channel === 'function') throw new Error("WOOT! Subscribing to a function : " + channel);
var i, n = subs.length, sub, found = false;
for (i = 0; i < n; i++) {
sub = subs[i];
if (sub.client === client && ((p && sub.pattern === channel) || (!p && sub.channel === channel))) {
found = true;
break;
}
}
var x = this.numSubs(client);
if (!found) {
x++;
subs.push({ pattern: p ? channel : null, channel: p ? pattern(channel) : channel, client: client });
process.nextTick(client.pushMessage.bind(client, p ? 'psubscribe' : 'subscribe', channel, x));
}
return x;
};
this.unsub = function (p, channel, client) {
if (!channel && channel !== '' && channel !== null) throw new Error("WOOT! Unsubscribing from a falsy, non-string, non-null channel : [" + channel + ']');
if (!client || !client.pushMessage) throw new Error("WOOT! Unsubscribing an invalid client : " + client);
var x = this.numSubs(client);
var i, n = subs.length, sub;
for (i = 0; i < n; i++) {
sub = subs[i];
if (sub.client !== client)
continue;
if ((p && sub.pattern !== null && (channel === null || sub.pattern === channel)) || (!p && sub.pattern === null && (channel === null || sub.channel === channel))) {
x--;
subs.splice(i, 1);
process.nextTick(client.pushMessage.bind(client, p ? 'punsubscribe' : 'unsubscribe', p ? sub.pattern : sub.channel, x));
i--; n--;
}
}
return x;
};
this.numSubs = function (client) {
var i, n = subs.length, x = 0;
for (i = 0; i < n; i++)
if (subs[i].client === client)
x++;
return x;
};
};
// Redis commands.
exports.Backend.prototype = {
// Keys.
DEL: function () {
var i, n = arguments.length, x = 0;
if (!n) return BAD_ARGS;
for (i = 0; i < n; i++)
if (this.setKey(arguments[i], null)) x++;
return x;
}
, EXISTS: function (key) {
return this.getKey(null, key)? 1 : 0;
}
, PEXPIREAT: function (key, time) {
time = str2int(time);
if (time instanceof ERROR) return time;
return this.setExpire(key, time);
}
, EXPIREAT: function (key, time) {
time = str2int(time);
if (time instanceof ERROR) return time;
return this.setExpire(key, time * 1000);
}
, PEXPIRE: function (key, time) {
time = str2int(time);
if (time instanceof ERROR) return time;
return this.setExpire(key, time + Date.now());
}
, EXPIRE: function (key, time) {
time = str2int(time);
if (time instanceof ERROR) return time;
return this.setExpire(key, time * 1000 + Date.now());
}
, PERSIST: function (key) {
return this.PEXPIREAT(key, "0");
}
, PTTL: function (key) {
var ttl = this.getExpire(key);
if (ttl) return ttl - Date.now();
else return - 1;
}
, RANDOMKEY: function () {
var keys = this.getKeys(), n = keys && keys.length;
if (n) return keys[Math.floor(Math.random()* n)];
else return null;
}
, RENAME: function (key, newkey) {
return this.renameKey(key, newkey)? OK : BAD_KEY;
}
, RENAMENX: function (key, newkey) {
if (!this.EXISTS(key)) return BAD_KEY;
if (this.EXISTS(newkey)) return 0;
if (!this.renameKey(key, newkey)) throw new Error("WOOT! Couldn't rename.");
return 1;
}
, TTL: function (key) {
var ttl = this.getExpire(key);
if (ttl) return Math.ceil((ttl - Date.now())/ 1000);
else return - 1;
}
, TYPE: function (key) {
var K = this.getKey(null, key);
return K ? K.TYPE : NONE;
}
, KEYS: function (pat) {
var keys = this.getKeys().filter(pattern(pat));
keys.sort();
return keys;
}
, SCAN: function () {
return this._scan(this.getKeys(), 1, arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]);
}
, SSCAN: function () {
return this._scan(this.SMEMBERS(arguments[0]), 1, arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
}
, HSCAN: function () {
return this._scan(this.HGETALL(arguments[0]), 2, arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
}
, ZSCAN: function () {
return this._scan(this.ZRANGE(arguments[0], '0', '-1', 'WITHSCORES'), 2, arguments[1], arguments[2], arguments[3], arguments[4], arguments[5]);
}
, _scan: function (allKeys, size, cursor, opt1, opt1val, opt2, opt2val) {
cursor = str2int(cursor);
if (cursor instanceof ERROR)
return cursor;
var count = 10;
var matchPattern = null;
opt1 = (opt1 || '').toUpperCase();
opt2 = (opt2 || '').toUpperCase();
if (opt1 === 'MATCH') {
matchPattern = pattern(opt1val);
if (opt2 === 'COUNT')
count = str2int(opt2val);
else if (opt2)
return BAD_SYNTAX;
}
else if (opt1 === 'COUNT') {
if (opt2)
return BAD_SYNTAX;
else
count = str2int(opt1val);
}
else if (opt1)
return BAD_SYNTAX;
if (count instanceof ERROR)
return count;
var nextCursor = cursor + count;
var keys = allKeys.slice(cursor, nextCursor);
// Apply MATCH filtering _after_ getting number of keys
if (matchPattern) {
var i = 0;
while (i < keys.length)
if (!matchPattern(keys[i]))
keys.splice(i, size);
else
i += size;
}
// Return 0 when iteration is complete.
if (nextCursor >= allKeys.length)
nextCursor = 0;
return [nextCursor, keys];
}
// String setters.
, SET: function () {
if (arguments.length < 2) return BAD_ARGS;
var argc = 0;
var key = arguments[argc++];
var value = arguments[argc++];
var buf = new Buffer(Buffer.byteLength(value));
buf.write(value);
var optNX = false;
var optXX = false;
var optEX = 0;
var optPX = 0;
// process.stdout.write('arguments is ' + JSON.stringify(arguments));
while (argc < arguments.length) {
switch (arguments[argc++].toUpperCase()) {
case 'NX': {
optNX = true;
break;
}
case 'XX': {
optXX = true;
break;
}
case 'EX': {
if (arguments.length === argc)
return BAD_ARGS;
optEX = arguments[argc++];
/*jshint -W018*/
if (!(str2int(optEX) > 0))
return BAD_INT;
break;
}
case 'PX': {
if (arguments.length === argc)
return BAD_ARGS;
optPX = arguments[argc++];
/*jshint -W018*/
if (!(str2int(optPX) > 0))
return BAD_INT;
break;
}
}
}
if (optNX) {
if (this.EXISTS(key)) return null;
}
if (optXX) {
if (!this.EXISTS(key)) return null;
}
this.setKey(key, new STRING(buf));
if (optEX) {
this.EXPIRE(key, optEX);
}
if (optPX) {
this.PEXPIRE(key, optPX);
}
return OK;
}
, sIncrBy: function (parse, key, incr) {
var K = this.getKey(STRING, key, true);
if (K instanceof ERROR) return K;
incr = parse(incr);
if (incr instanceof ERROR) return incr;
var value = parse(K.value.toString() || "0");
if (value instanceof ERROR) return value;
value = (value + incr).toString();
var buf = new Buffer(Buffer.byteLength(value));
buf.write(value);
K.value = value;
this.upsetKey(key, K);
return value;
}
, sFit: function (key, length) {
var K = this.getKey(STRING, key, true);
if (K instanceof ERROR) return ERROR;
if (K.value.length < length) {
var buf = new Buffer(length);
buf.fill(0);
K.value.copy(buf);
K.value = buf;
}
return K;
}
, SETBIT: function (key, offset, state) {
/*jshint -W018*/
offset = str2int(offset);
if (!(offset > - 1)) return BAD_BIT1;
state = str2int(state);
if (!(state === 0 || state === 1)) return BAD_BIT2;
var x = Math.floor(offset / 8);
var K = this.sFit(key, x + 1);
if (K instanceof ERROR) return K;
var mask = 1 << (7 - (offset % 8));
var current = K.value[x];
var old = current & mask ? 1 : 0;
if (state && !old)
K.value[x] = current | mask;
else if (!state && old)
K.value[x] = current & ~mask;
this.upsetKey(key, K);
return old;
}
, SETRANGE: function (key, offset, value) {
/*jshint -W018*/
offset = str2int(offset);
if (!(offset > - 1)) return BAD_BIT1;
var K = this.sFit(key, offset + Buffer.byteLength(value));
K.value.write(value, offset);
this.upsetKey(key, K);
return this.STRLEN(key);
}
// String getters.
, GET: function (key) {
var K = this.getKey(STRING, key);
if (K instanceof ERROR) return K;
return K ? K.value.toString() : null;
}
, STRLEN: function (key) {
var K = this.getKey(STRING, key);
if (K instanceof ERROR) return ERROR;
return K ? K.value.length : 0;
}
, GETBIT: function (key, offset) {
/*jshint -W018*/
var K = this.getKey(STRING, key);
if (K instanceof ERROR) return ERROR;
offset = str2int(offset);
if (!(offset > - 1)) return BAD_BIT1;
var x = Math.floor(offset / 8);
if (!K || K.length < x + 1) return 0;
var mask = 1 << (7 - (offset % 8));
return (K.value[x]& mask)? 1 : 0;
}
, GETRANGE: function (key, start, stop) {
var K = this.getKey(STRING, key);
if (K instanceof ERROR) return ERROR;
if (!K) return "";
var out = slice(K.value, start, stop);
if (out instanceof ERROR) return out;
return out.toString();
}
, BITCOUNT: function () {
var key = arguments[0];
var start = arguments[1] || 0;
var stop = arguments[2] || -1;
if (!key) return BAD_ARGS;
var K = this.GETRANGE(key, start.toString(), stop.toString());
if (K instanceof ERROR) return ERROR;
if (!K) return 0;
var bitCount = 0;
var buff = new Buffer(K);
for (var i = 0; i < buff.length; i++) {
bitCount += populationCount(buff[i].toString());
}
return bitCount;
}
// String ops.
, APPEND: function (key, value) {
var strlen = this.STRLEN(key);
if (strlen instanceof ERROR) return strlen;
return this.SETRANGE(key, strlen.toString(), value);
}
, DECR: function (key) {
return this.DECRBY(key, "1");
}
, DECRBY: function (key, decr) {
var value = str2int(decr);
if (value instanceof ERROR) return value;
return this.INCRBY(key, ( - value).toString());
}
, GETSET: function (key, value) {
var old = this.GET(key);
if (old instanceof ERROR) return old;
this.SET(key, value);
return old;
}
, INCR: function (key) {
return this.INCRBY(key, "1");
}
, INCRBY: function (key, incr) {
return this.sIncrBy(str2int, key, incr);
}
, INCRBYFLOAT: function (key, incr) {
return this.sIncrBy(str2float, key, incr);
}
, MGET: function () {
var out = [], i, n = arguments.length;
if (!n) return BAD_ARGS;
for (i = 0; i < n; i++) {
var value = this.GET(arguments[i]);
out[i] = value instanceof ERROR ? null : value;
}
return out;
}
, MSET: function () {
var key, value, i, n = arguments.length;
if (!n || n % 2) return BAD_ARGS;
for (i = 0; i < n; i += 2) {
key = arguments[i];
value = arguments[i + 1];
this.SET(key, value);
}
return OK;
}
, MSETNX: function () {
var i, n = arguments.length;
for (i = 0; i < n; i += 2)
if (this.EXISTS(arguments[i])) return 0;
this.MSET.apply(this, arguments);
return 1;
}
, PSETEX: function (key, timediff, value) {
/*jshint -W018*/
if (!(str2int(timediff) > 0))
return BAD_SETEX;
this.SET(key, value);
this.PEXPIRE(key, timediff);
return OK;
}
, SETEX: function (key, timediff, value) {
/*jshint -W018*/
if (!(str2int(timediff) > 0))
return BAD_SETEX;
this.SET(key, value);
this.EXPIRE(key, timediff);
return OK;
}
, SETNX: function (key, value) {
if (this.EXISTS(key)) return 0;
this.SET(key, value);
return 1;
}
// Lists, non-blocking.
, lStore: function (key, values) {
// Only used in SORT.
if (values.length)
return this.setKey(key, new LIST(values));
else
return this.setKey(key, null);
}
, LINDEX: function (key, index) {
var K = this.getKey(LIST, key);
if (K instanceof ERROR) return K;
index = str2int(index);
if (index instanceof ERROR)
return index;
return (K && K.value[index < 0 ? K.value.length + index : index]) || null;
}
, upsetList: function (key, K) {
if (K.value.length) this.upsetKey(key, K);
else this.setKey(key, null);
}
, LINSERT: function (key, relpos, pivot, value) {
var K = this.getKey(LIST, key), x;
if (K instanceof ERROR) return K;
relpos = relpos.toUpperCase();
if (relpos !== 'BEFORE' && relpos !== 'AFTER') return BAD_SYNTAX;
if (!K) return 0;
if ((x = K.value.indexOf(pivot)) < 0) return 0;
K.value.splice(relpos === 'AFTER'? x + 1 : x, 0, value);
this.upsetList(key, K);
return 1;
}
, LLEN: function (key) {
var K = this.getKey(LIST, key);
if (K instanceof ERROR) return K;
return (K && K.value && K.value.length) || 0;
}
, lPopMany: function (left, keys) {
var K = [], value, i, n = keys.length;
if (!n) return BAD_ARGS;
for (i = 0; i < n; i++) {
K[i] = this.getKey(LIST, keys[i]);
if (K[i]instanceof ERROR) return K[i];
}
for (i = 0; i < n; i++)
if (K[i] && K[i].value && K[i].value.length) {
value = left ? K[i].value.shift() : K[i].value.pop();
this.upsetList(keys[i], K[i]);
return [keys[i], value];
}
return null;
}
, lPop: function (left, key) {
var out = this.lPopMany(left, [key]);
return out && out.length ? out[1] : out;
}
, LPOP: function (key) {
return this.lPop(true, key);
}
, RPOP: function (key) {
return this.lPop(false, key);
}
, lPush: function (left, make, args) {
var i, n = args.length, key = args[0];
var K = this.getKey(LIST, key, make);
if (K instanceof ERROR) return K;
if (n < 2) return BAD_ARGS;
if (!K) return 0;
if (left) for (i = 1; i < n; i++)
K.value.unshift(args[i]);
else
K.value.push.apply(K.value, args.slice(1));
this.upsetList(key, K);
return K.value.length;
}
, LPUSH: function () {
return this.lPush(true, true, arr(arguments));
}
, LPUSHX: function () {
return this.lPush(true, false, arr(arguments));
}
, RPUSH: function () {
return this.lPush(false, true, arr(arguments));
}
, RPUSHX: function () {
return this.lPush(false, false, arr(arguments));
}
, RPOPLPUSH: function (source, destination) {
var dest = this.getKey(LIST, destination);
if (dest && dest instanceof ERROR) return dest;
var value = this.RPOP(source);
if (value === null || value instanceof ERROR) return value;
var len = this.LPUSH(destination, value);
if (!len || len instanceof ERROR) throw new Error("WOOT! LPUSH failed in RPOPLPUSH.");
return value;
}
, LRANGE: function (key, start, stop) {
var K = this.getKey(LIST, key);
if (K instanceof ERROR) return K;
if (!K) return [];
return slice(K.value, start, stop);
}
, LREM: function (key, count, value) {
var K = this.getKey(LIST, key);
if (K instanceof ERROR) return K;
count = str2int(count);
if (count instanceof ERROR) return count;
if (!K) return 0;
var i, n = K.value.length, x = 0;
if (count < 0) {
count *= - 1;
for (i = n - 1; i >= 0; i--)
if (K.value[i] === value && (!count || x < count)) {
K.value.splice(i, 1);
x++;
}
}
else for (i = 0; i < n; i++)
if (K.value[i] === value && (!count || x < count)) {
K.value.splice(i, 1);
i--; n--; x++;
}
if (x > 0) this.upsetList(key, K);
return x;
}
, LSET: function (key, index, value) {
var K = this.getKey(LIST, key);
if (!K) return BAD_KEY;
if (K instanceof ERROR) return K;
index = str2int(index);
if (index instanceof ERROR) return index;
if (index < 0 || index > K.value.length) return BAD_INDEX;
K.value[index] = value;
this.upsetList(key, K);
return OK;
}
, LTRIM: function (key, start, stop) {
var range = this.LRANGE(key, start, stop);
if (!range.join)
return range;
var K = this.getKey(LIST, key);
if (K) {
K.value = range;
this.upsetList(key, K);
}
return OK;
}
// Blocking list commands.
// The blocking part happens at the connection level,
// where in case the response is null the connection subscribes to the keyspace change event for the key and waits to retry.
// So this only validates the parameter.
, bArgs: function (args) {
args = arr(args);
var timeout = str2int(args.pop() || "FAIL");
if (timeout instanceof ERROR) return timeout;
if (timeout < 0) return BAD_INT;
return args;
}
, BLPOP: function () {
var a = this.bArgs(arguments);
if (a instanceof ERROR) return a;
return this.lPopMany(true, a);
}
, BRPOP: function () {
var a = this.bArgs(arguments);
if (a instanceof ERROR) return a;
return this.lPopMany(false, a);
}
, BRPOPLPUSH: function () {
var a = this.bArgs(arguments);
if (a instanceof ERROR) return a;
return this.RPOPLPUSH.apply(this, a);
}
// Hashes.
, structPut: function (type, validate, revArgs, args) {
var key = args[0], i, n = args.length, x = 0;
if (n < 3 || ((n - 1) % 2)) return BAD_ARGS;
var K = this.getKey(type, key, true);
if (K instanceof ERROR) return K;
for (i = 1; i < n; i += 2) {
var member = args[revArgs ? i + 1 : i]
, value = validate(args[revArgs ? i : i + 1]);
if (value instanceof ERROR) return value;
if (!(member in K.value)) x++;
K.value[member] = value;
}
if (x) this.upsetKey(key, K);
return x;
}
, structDel: function (type, args) {
var key = args[0]
, i, n = args.length
, x;
if (n < 2) return BAD_ARGS;
var K = this.getKey(type, key);
if (K instanceof ERROR) return K;
if (!K) return 0;
x = 0;
for (i = 1; i < n; i++) {
if (args[i]in K.value) x++;
delete K.value[args[i]];
}
// Remove the set if empty, upset otherwise.
var member;
for (member in K.value) {
if (x) this.upsetKey(key, K);
return x;
}
this.setKey(key, null);
return x;
}
, structGet: function (type, key, member) {
var K = this.getKey(type, key);
if (K instanceof ERROR) return K;
if (!K || !(member in K.value)) return null;
return K.value[member];
}
, HDEL: function () {
return this.structDel(HASH, arguments);
}
, HEXISTS: function (key, field) {
var fields = this.HKEYS(key);
if (fields.indexOf) return fields.indexOf(field) >= 0 ? 1 : 0;
return fields;
}
, HGET: function (key, field) {
return this.structGet(HASH, key, field);
}
, HGETALL: function (key) {
var fields = this.HKEYS(key);
var i, n = fields.length, out = [];
for (i = 0; i < n; i++)
out.push(fields[i], this.HGET(key, fields[i]));
return out;
}
, hIncrBy: function (parse, key, field, incr) {
var K = this.getKey(HASH, key, true);
if (K instanceof ERROR) return K;
incr = parse(incr);
if (incr instanceof ERROR) return incr;
var value = parse(K.value[field] || "0");
if (value instanceof ERROR) return value;
K.value[field] = (value + incr).toString();
this.upsetKey(key, K);
return K.value[field];
}
, HINCRBY: function (key, field, incr) {
return this.hIncrBy(str2int, key, field, incr);
}
, HINCRBYFLOAT: function (key, field, incr) {
return this.hIncrBy(str2float, key, field, incr);
}
, HKEYS: function (key) {
var K = this.getKey(HASH, key);
if (K instanceof ERROR) return K;
var fields = [], field;
if (K) for (field in K.value)
fields.push(field);
fields.sort();
return fields;
}
, HLEN: function (key) {
var fields = this.HKEYS(key);
if (fields.indexOf) return fields.length;
return fields;
}
, HMGET: function () {
var K = this.getKey(HASH, arguments[0]);
if (K instanceof ERROR) return K;
var i, n = arguments.length, values = [];
if (n < 2) return BAD_ARGS;
for (i = 1; i < n; i++)
values.push(K && arguments[i]in K.value ? K.value[arguments[i]] : null);
return values;
}
, HMSET: function () {
var x = this.structPut(HASH, String, false, arguments);
return x instanceof ERROR ? x : OK;
}
, HSET: function () {
return this.structPut(HASH, String, false, arguments);
}
, HSETNX: function (key, field, value) {
var exists = this.HEXISTS(key, field);
if (exists instanceof ERROR) return exists;
if (exists) return 0;
return this.HSET(key, field, value);
}
, HVALS: function (key) {
var out = this.HKEYS(key), self = this;
if (out instanceof ERROR) return out;
if (out.map)
out = out.map(function (field) {
return self.HGET(key, field);
});
out.sort();
return out;
}
// Sets.
, SADD: function () {
var key = arguments[0]
, i, n = arguments.length
, x = 0;
if (n < 2) return BAD_ARGS;
var K = this.getKey(SET, key, true);
if (K instanceof ERROR) return K;
for (i = 1; i < n; i++)
if (!K.value[arguments[i]]) {
K.value[arguments[i]] = true;
x++;
}
if (x) this.upsetKey(key, K);
return x;
}
, SCARD: function (key) {
var members = this.SMEMBERS(key);
return members.join ? members.length : members;
}
, SISMEMBER: function (key, member) {
var members = this.SMEMBERS(key);
return members.indexOf ? members.indexOf(member) >= 0 ? 1 : 0: members;
}
, SMEMBERS: function (key) {
return this.SUNION(key);
}
, SPOP: function (key) {
var member = this.SRANDMEMBER(key);
if (typeof member === 'string')
this.SREM(key, member);
return member;
}
, SRANDMEMBER: function (key) {
var members = this.SMEMBERS(key)
, n = members.length, member;
if (!n)
return n === 0 ? null : members;
member = members[Math.floor(Math.random()* n)];
return member;
}
, SREM: function () {
return this.structDel(SET, arguments);
}
// Set multikey ops.
// Set members come out sorted lexicographically to facilitate testing.
, SMOVE: function (source, destination, member) {
var removed = this.SREM(source, member);
if (removed === 1)
return this.SADD(destination, member);
else
return removed;
}
, SUNION: function () {
var i, n = arguments.length, out = [];
if (!n) return BAD_ARGS;
for (i = 0; i < n; i++) {
var K = this.getKey(SET, arguments[i]);
if (K instanceof ERROR) return K;
var member;
if (K) for (member in K.value)
if (out.indexOf(member) < 0)
out.push(member);
}
out.sort();
return out;
}
, sCombine: function (diff, args) {
var i, n = args.length;
if (!n)
return BAD_ARGS;
var out = this.SUNION(args[0]);
if (out instanceof ERROR) return out;
for (i = 1; i < n; i++) {
var K = this.getKey(SET, args[i]);
if (K instanceof ERROR) return K;
var j, m = out.length;
for (j = 0; j < m; j++)
if ((diff && (K && K.value[out[j]])) || (!diff && !(K && K.value[out[j]]))) {
out.splice(j, 1);
j--;
m--;
}
}
out.sort();
return out;
}
, SDIFF: function () {
return this.sCombine(true, arr(arguments));
}
, SINTER: function () {
return this.sCombine(false, arr(arguments));
}
, sStore: function (key, members) {
var K, i, n = members.length;
if (n) K = new SET({});
for (i = 0; i < n; i++)
K.value[members[i]] = true;
return this.setKey(key, K || null);
}
, sStoreOp: function (op, args) {
if (!args.length)
return BAD_ARGS;
var key = args.shift()
, members = op.apply(this, args);
if (members.join) {
this.sStore(key, members);
return members.length;
}
return members;
}
, SDIFFSTORE: function () {
return this.sStoreOp(this.SDIFF, arr(arguments));
}
, SINTERSTORE: function () {
return this.sStoreOp(this.SINTER, arr(arguments));
}
, SUNIONSTORE: function () {
return this.sStoreOp(this.SUNION, arr(arguments));
}
// Sorted sets.
, ZADD: function () {
return this.structPut(ZSET, str2float, true, arguments);
}
, ZCARD: function (key) {
return this.ZCOUNT(key, '-inf', '+inf');
}
, ZCOUNT: function (key, min, max) {
var members = this.ZRANGEBYSCORE(key, min, max);
return members.join ? members.length : members;
}
, ZINCRBY: function (key, incr, member) {
var K = this.getKey(ZSET, key, true);
if (K instanceof ERROR) return K;
var value = str2float(incr);
if (value instanceof ERROR) return value;
value += Number(K.value[member] || 0);
K.value[member] = value;
this.upsetKey(key, K);
return value;
}
// Sort set queries.
, zSort: function (rev, key, min, max) {
var K = this.getKey(ZSET, key);
if (K instanceof ERROR) return K;
if (!K) return [];
var rng = range(min, max), member, out = [];
if (rng instanceof ERROR) return rng;
for (member in K.value)
if (rng(K.value[member]))
out.push({ member: member, score: K.value[member] });
// First by score,
// then in lexicographic order.
if (rev)
out.sort(function (b, a) {
return (a.score - b.score) || (a.member < b.member ? - 1 : 1);
});
else
out.sort(function (a, b) {
return (a.score - b.score) || (a.member < b.member ? - 1 : 1);
});
return out;
}
, zUnwrap: function (range, scores) {
var i, n = range.length, out = n ?[] : range;
if (n)
for (i = 0; i < n; i++) {
out.push(range[i].member);
if (scores)
out.push(range[i].score);
}
return out;
}
, zGetRange: function (rev, args) {
var key = args[0], start = args[1], stop = args[2], scores = args[3];
if (args.length < 3 || args.length > 4)
return BAD_ARGS;
if (scores && scores.toUpperCase() !== 'WITHSCORES')
return BAD_SYNTAX;
var range = this.zSort(rev, key, '-inf', '+inf');
return this.zUnwrap(slice(range, start, stop), scores);
}
, zGetRangeByScore: function (rev, args) {
var key = args[0], min = args[rev ? 2 : 1], max = args[rev ? 1 : 2]
, scores, limit, offset, count;
if (args.length < 3)
return BAD_ARGS;
else if (args.length === 4)
scores = args[3];
else if (args.length === 6) {
limit = args[3];
offset = args[4];
count = args[5];
}
else if (args.length === 7) {
scores = args[3];
limit = args[4];
offset = args[5];
count = args[6];
}
if (scores && scores.toUpperCase() !== 'WITHSCORES')
return BAD_SYNTAX;
if (limit && limit.toUpperCase() !== 'LIMIT')
return BAD_SYNTAX;
var range = this.zSort(rev, key, min, max);
if (limit)
range = slice(range, offset, count, true);
return this.zUnwrap(range, scores);
}
, ZRANGE: function () {
return this.zGetRange(false, arr(arguments));
}
, ZREVRANGE: function () {
return this.zGetRange(true, arr(arguments));
}
, ZRANGEBYSCORE: function () {
return this.zGetRangeByScore(false, arr(arguments));
}
, ZREVRANGEBYSCORE: function () {
return this.zGetRangeByScore(true, arr(arguments));
}
, ZRANK: function (key, member) {
var out = this.zSort(false, key, '-inf', '+inf')
, i, n = out.length;
for (i = 0; i < n; i++)
if (out[i].member === member)
return i;
return n || n === 0 ? null : out;
}
, ZREVRANK: function (key, member) {
var out = this.zSort(false, key, '-inf', '+inf')
, i, n = out.length;
for (i = n - 1; i >= 0; i--)
if (out[i].member === member)
return n - i - 1;
return n || n === 0 ? null : out;
}
, ZSCORE: function (key, member) {
return this.structGet(ZSET, key, member);
}
, ZREM: function () {
return this.structDel(ZSET, arguments);
}
, ZREMRANGEBYRANK: function (key, start, stop) {
var members = this.ZRANGE(key, start, stop), n = members.length;
if (n)
n = this.ZREM.apply(this, [key].concat(members));
return n || n === 0 ? n : members;
}
, ZREMRANGEBYSCORE: function (key, min, max) {
var members = this.ZRANGEBYSCORE(key, min, max), n = members.length;
if (n)
n = this.ZREM.apply(this, [key].concat(members));
return n || n === 0 ? n : members;
}
// Sorted set multikey ops.
, getSetOrZsetKey: function (key) {
var K;
var type = this.TYPE(key);
if (type === ZSET || type === SET) {
K = this.getKey(type, key);
}
return K;
}
, zOpStore: function (union, key, keys, weights, aggregate) {
var K = this.getSetOrZsetKey(keys[0]);
if (K instanceof ERROR) return K;
var out = {}, member, x = 0, weight = (weights === null ? 1 : weights[0]);
if (K) for (member in K.value) {
out[member] = K.value[member]* weight;
x++;
}
var i, n = keys.length;
for (i = 1; i < n; i++) {
K = this.getSetOrZsetKey(keys[i]);
if (K instanceof ERROR) return K;
weight = (weights !== null ? weights[i] : 1);
if (!union) {
if (!K) {
out = {};
x = 0;
}
else for (member in out) if (!(member in K.value)) {
delete out[member];
x--;
}
}
if (K) for (member in K.value)
if (union || member in out) {
if (!(member in out)) {
x++;
out[member] = K.value[member]* weight;
}
else
out[member] = aggregate(K.value[member]* weight, out[member]);
}
}
if (x) this.setKey(key, new ZSET(out));
return x;
}
, zsum: function (a, b) { return a + b; }
, zmin: function (a, b) { return a < b ? a : b; }
, zmax: function (a, b) { return a > b ? a : b; }
, zParseOpStore: function (union, args) {
var key = args[0], N = str2int(args[1]);
if (N instanceof ERROR) return N;
if (N < 1) return BAD_ZUIS;
if (args.length < N + 2) return BAD_ARGS;
var keys = args.splice(2, N), weigh = (args[2] || '').toUpperCase() === 'WEIGHTS', weights;
if (weigh) {
if (args.length < N + 3) return BAD_ARGS;
weights = args.splice(3, N);
if (weights.map(str2float).some(function (w) { return w instanceof ERROR; })) return BAD_FLOAT;
args.splice(2, 1);
}
var aggregate = (args[2] || '').toUpperCase() === 'AGGREGATE' ? (args[3] || '').toLowerCase() : null;
if (aggregate) {
if (aggregate !== 'sum' && aggregate !== 'min' && aggregate !== 'max') return BAD_SYNTAX;
aggregate = this['z' + aggregate];
if (typeof aggregate !== 'function')
throw new Error("WOOT! Can't find the aggregate function for " + args[3]);
args.splice(2, 2);
}
if (args.length !== 2)
return BAD_ARGS;
return this.zOpStore(union, key, keys, weights || null, aggregate || this.zsum);
}
, ZINTERSTORE: function () {
return this.zParseOpStore(false, arr(arguments));
}
, ZUNIONSTORE: function () {
return this.zParseOpStore(true, arr(arguments));
}
// Sort.
, sortSelect: function (pat, kkey) {
var select = /^((?:.)*?)(?:->(.*))?$/.exec(pat)
, key = select[1].replace(/\*/, kkey), // no g flag, so only first occurence is replaced
field = select[2];
if (typeof field === 'string')
return this.HGET(key, field);
else
return this.GET(key);
}
, SORT: function () {
var self = this, args = arr(arguments), n = args.length;
if (!n) return new BAD_ARGS;
// Parse.
// SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]
var key = args.shift()
, by, limit, offset, count, get, pat, desc, alpha, store
, seenAscDesc = false;
if (/^by$/i.test(args[0])) {
by = args[1];
if (typeof by !== 'string') return BAD_SYNTAX;
args.splice(0, 2);
}
if (/^limit$/i.test(args[0])) {
limit = true;
if (args.length < 3) return BAD_ARGS;
offset = args[1]; // integer validation happens in slice()
count = args[2];
args.splice(0, 3);
}
if (/^asc|desc$/i.test(args[0])) {
desc = /^desc$/i.test(args[0]);
args.splice(0, 1);
seenAscDesc = true;
}
while (/^get$/i.test(args[0])) {
pat = args[1];
if (typeof pat !== 'string') return BAD_SYNTAX;
if (!get) get = [];
get.push(pat);
args.splice(0, 2);
}
if (!seenAscDesc)
if (/^asc|desc$/i.test(args[0])) {
desc = /^desc$/i.test(args[0]);
args.splice(0, 1);
seenAscDesc = true;
}
if (/^alpha$/i.test(args[0])) {
alpha = true;
args.splice(0, 1);
}
if (/^store$/i.test(args[0])) {
store = args[1];
if (typeof store !== 'string') return BAD_SYNTAX;
args.splice(0, 2);
}
// Redis appears to accept params in any order,
// needs some tests before allowing this here.
if (args.length) return BAD_SYNTAX;
// Collect data.
var type = this.TYPE(key), data, scoreFail = false;
if (type === NONE)
data = [];
else if (type === LIST)
data = this.LRANGE(key, '0', '-1');
else if (type === SET)
data = this.SMEMBERS(key);
else if (type === ZSET)
data = this.ZRANGE(key, '0', '-1');
else
return BAD_TYPE;
data = data.map(function (id) {
var entry = { id: id };
if (by) {
entry.by = self.sortSelect(by, id);
if (!alpha)
entry.num = str2float(entry.by || '0');
}
else if (!alpha)
entry.num = str2float(id);
else
entry.num = 0;
if (entry.num instanceof ERROR)
scoreFail = true;
if (get)
entry.get = get.map(function (get) {
if (get === '#') return id;
return self.sortSelect(get, id);
});
return entry;
});
if (scoreFail) return BAD_SORT;
// Sort.
data.sort(function (a, b) {
var d = a.num - b.num;
if (!d && by) d = a.by < b.by ? - 1 : a.by > b.by ? 1 : 0;
if (!d) d = a.id < b.id ? - 1 : a.id > b.id ? 1 : 0;
return desc ? - d : d;
});
// Limit.
if (parseInt(offset, 10) < 0)
offset = '0'; // SORT treats negative offset limit differently from other redis commands.
if (limit) data = slice(data, offset, count, true);
// Format.
var out = [], i;
n = data.length;
for (i = 0; i < n; i++) {
if (get) out.push.apply(out, data[i].get);
else out[i] = data[i].id;
}
// Store or return.
if (store) {
this.lStore(store, out);
return this.LLEN(store);
}
else
return out;
}
// Pubsub.
, PUBLISH: function (channel, message) {
return this.pub(channel, message);
}
// Connection.
// Quit and select could be implemented on the connection object.
, PING: function () {
if (arguments.length)
return BAD_ARGS;
return PONG;
}
, ECHO: function (message) {
return message;
}
// Server.
// FLUSHALL can be implemented on the connection object.
, DBSIZE: function () {
return this.getKeys().length;
}
, FLUSHDB: function () {
var keys = this.getKeys(), i, n = keys.length;
for (i = 0; i < n; i++)
this.setKey(keys[i], null);
return OK;
}
, TIME: function () {
var time = Date.now()
, sec = Math.round(time / 1000)
, msec = (time % 1000)* 1000 + Math.floor(Math.random()* 1000);
return [sec, msec];
}
// Helper commands.
, FAKE_DUMP: function (pattern) {
var keys = this.KEYS(pattern), i, n = keys.length, out = [], key, type;
for (i = 0; i < n; i++) {
key = keys[i];
type = this.TYPE(key);
out.push(key, this.TTL(key), type.getStatus());
if (type === STRING)
out.push(this.GET(key));
else if (type === LIST)
out.push(this.LRANGE(key, '0', '-1'));
else if (type === HASH)
out.push(this.HGETALL(key));
else if (type === SET)
out.push(this.SMEMBERS(key));
else if (type === ZSET)
out.push(this.ZRANGE(key, '0', '-1', 'withscores'));
else
throw new Error("WOOT! Key type is " + type);
}
return out;
}
};
// These don't have an effect on the dataset, so dummies are safe for tests.
exports.Backend.prototype.AUTH =
exports.Backend.prototype.BGREWRITEAOF =
exports.Backend.prototype.SAVE =
exports.Backend.prototype.BGSAVE = function () { return OK; };
// All of these are implemented at the connection level.
exports.Backend.prototype.QUIT =
exports.Backend.prototype.SUBSCRIBE =
exports.Backend.prototype.PSUBSCRIBE =
exports.Backend.prototype.UNSUBSCRIBE =
exports.Backend.prototype.PUNSUBSCRIBE =
exports.Backend.prototype.MULTI =
exports.Backend.prototype.EXEC =
exports.Backend.prototype.WATCH =
exports.Backend.prototype.UNWATCH =
exports.Backend.prototype.SELECT =
exports.Backend.prototype.DISCARD = function () { throw new Error("WOOT! This command shouldn't have reached the backend."); };