hdachev/fakeredis

View on GitHub
lib/backend.js

Summary

Maintainability
F
2 wks
Test Coverage
"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."); };