UNPKG

redis-js

Version:

An in-memory redis-compatible implementation written in pure javascript. Part of the [Rarefied Redis Project](http://wilkenstein.github.io/rarefied-redis/).

1,527 lines (1,441 loc) 124 kB
/* jshint unused:true, undef:true, strict:true, plusplus:true */ /* global setTimeout:false, module:false, exports:true, clearTimeout:false, require:false, process:false */ (function () { // Baseline setup // -------------- "use strict"; // Establish the root object, `window` in the browser, or `exports` on the server. var root = this; // Create the mock object. var redismock = {}; function exists(v) { return typeof v !== 'undefined' && v !== null; } // Export the mock object for **node.js/io.js**, with // backwards-compatibility for the old `require()` API. If we're in // the browser, add `redismock` as a global object. if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = redismock; } exports.redismock = redismock; } else { root.redismock = redismock; } // We use setImmediate for callback-style returns, // but it is non-standard. To standardize, convert // setImmediate to its equivalent (mostly) counterpart -- // setTimeout(f, 0) -- if it is not available. if (typeof setImmediate === 'undefined' || typeof setImmediate !== 'function') { var setImmediate = function (f) { setTimeout(f, 0); }; } redismock.Array = Array; function SortedSet() { this.scores = []; this.set = {}; this.invset = {}; this.indices = {}; this.lengths = {}; this.card = 0; return this; } SortedSet.prototype.add = function (score, member) { var ret = 1; if (exists(this.invset[member])) { this.rem(member); ret = 0; } if (!exists(this.set[score])) { this.scores.push(score); this.set[score] = []; this.lengths[score] = 0; } this.set[score].push(member); this.invset[member] = score; this.indices[member] = this.set[score].length - 1; this.lengths[score] += 1; this.card += 1; return ret; }; SortedSet.prototype.rem = function (member) { var score; if (!exists(this.invset[member])) { return 0; } score = this.invset[member]; this.set[score].splice(this.set[score].indexOf(member), 1); this.lengths[score] -= 1; if (this.lengths[score] === 0) { this.set[score] = undefined; this.scores.splice(this.scores.indexOf(score), 1); } this.invset[member] = undefined; this.indices[member] = undefined; this.card -= 1; return 1; }; SortedSet.prototype.sortScores = function () { this.scores.sort(function (a, b) { return parseInt(a, 10) - parseInt(b, 10); }); return this; }; SortedSet.prototype.sortScoresRev = function () { this.scores.sort(function (a, b) { return parseInt(b, 10) - parseInt(a, 10); }); return this; }; redismock.SortedSet = function () { return new SortedSet(); }; // The database. var cache = {}; var timeouts = {}; var subscribers = []; var watchers = {}; var sets = 'sets-' + Math.random(); var zsets = 'zsets-' + Math.random(); var hashes = 'hashes-' + Math.random(); //var keys = []; cache[sets] = {}; cache[zsets] = {}; cache[hashes] = {}; // Utils // ----- // From https://gist.github.com/Breton/2699916 String.prototype.escape = function () { var escapable = /[.\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; var meta; /* jshint ignore:start */ meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '\.': '\\.', '"': '\\"', '\\': '\\\\' }; /* jshint ignore:end */ function escapechar(a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); } return this.replace(escapable, escapechar); }; //Translate a shell PATTERN to a regular expression. function translate(pat) { //There is no way to quote meta-characters. var i=0, j, n = pat.length || 0, res, c, stuff; res = '^'; while (i < n) { c = pat[i]; i = i + 1; if (c === '*') { res = res + '.*'; } else if (c === '?') { res = res + '.'; } else if (c === '[') { j = i; if (j < n && pat[j] === '!') { j = j + 1; } if (j < n && pat[j] === ']') { j = j + 1; } while (j < n && pat[j] !== ']') { j = j + 1; } if (j >= n) { res = res + '\\['; } else { stuff = pat.slice(i, j).replace('\\', '\\\\'); i = j + 1; if (stuff[0] === '!') { stuff = '^' + stuff.slice(1); } else if (stuff[0] === '^') { stuff = '\\' + stuff; } res = res + '[' + stuff + ']'; } } else { res = res + (c).escape(); } } return res + '$'; } var cb = function (callback, context) { return function () { var args = arguments; if (callback && typeof callback === "function") { setImmediate(function () { callback.apply(context, args); }); } if (args[0] instanceof Error) { return args[0]; } return args[1]; }; }; var gather = function () { return function () { var idx, len = arguments.length; var callback; var list = []; for (idx = len - 1; idx >= 0; idx -= 1) { if (!callback && typeof arguments[idx] === "function") { callback = arguments[idx]; } else if (exists(arguments[idx])) { list.unshift(arguments[idx]); } } return { callback: callback, list: list }; }; }; var wrongType = function (callback) { return cb(callback)(new Error('WRONGTYPE Operation against a key holding the wrong kind of value')); }; redismock.ifType = function (key, type, callback) { var that = this; return { thenex: function (f) { this._ifex = f; return this; }, thennx: function (f) { this._ifnx = f; return this; }, then: function (f) { this._then = f; return this; }, end: function () { var ret; if (that.exists(key)) { if (that.type(key) !== type) { return wrongType(callback); } if (typeof this._ifex === 'function') { ret = this._ifex.call(that); if (ret && ret instanceof Error) { return cb(callback)(ret); } } } else { if (typeof this._ifnx === 'function') { ret = this._ifnx.call(that); if (ret && ret instanceof Error) { return cb(callback)(ret); } } } if (typeof this._then === 'function') { ret = this._then.call(that); if (ret && ret instanceof Error) { return cb(callback)(ret); } } return cb(callback)(null, ret); } }; }; // Keys Commands // ------------- // Delete a key redismock.del = function (key, callback) { var that = this; var count = 0; var g = gather(this.del).apply(this, arguments); callback = g.callback; var deleter = function (k) { if (that.exists(k)) { if (k in cache) { cache[k] = undefined; } else if (k in cache[sets]) { cache[sets][k] = undefined; } else if (k in cache[zsets]) { cache[zsets][k] = undefined; } else if (k in cache[hashes]) { cache[hashes][k] = undefined; } return 1; } return 0; }; var toDelete = g.list; if (typeof g.list[0] === 'object') { // Allow an array to be passed in. toDelete = g.list[0]; } toDelete.forEach(function (k) { count += deleter(k); }); return cb(callback)(null, count); }; // Return a serialized version of the value stored at the specified key. redismock.dump = function (key, callback) { return cb(callback)(new Error("UNIMPLEMENTED")); }; // Determine if a key exists. redismock.exists = function (key, callback) { return cb(callback)(null, (exists(cache[key]) || exists(cache[sets][key]) || exists(cache[zsets][key]) || exists(cache[hashes][key])) ? 1 : 0); }; // Set a key's time to live in seconds. redismock.expire = function (key, seconds, callback) { return this.pexpire(key, seconds*1000, callback); }; // Set the expiration for a key as a UNIX timestamp. redismock.expireat = function (key, timestamp, callback) { var now = new Date(); return this.pexpire(key, timestamp*1000 - now.getTime(), callback); }; // Find all the keys matching a given pattern. redismock.keys = function (pattern, callback) { var keys = [], key; var regex = new RegExp(translate(pattern)); for (key in cache) { if (key === sets || key === zsets || key === hashes) { continue; } if (cache.hasOwnProperty(key)) { if (key.match(regex) !== null) { keys.push(key); } } } for (key in cache[sets]) { if (cache[sets].hasOwnProperty(key)) { if (key.match(regex) !== null) { keys.push(key); } } } for (key in cache[zsets]) { if (cache[zsets].hasOwnProperty(key)) { if (key.match(regex) !== null) { keys.push(key); } } } for (key in cache[hashes]) { if (cache[hashes].hasOwnProperty(key)) { if (key.match(regex) !== null) { keys.push(key); } } } return cb(callback)(null, keys); }; // Atomically transfer a key from a Redis instance to another one. redismock.migrate = function (host, port, key, destination_db, timeout, callback) { return cb(callback)(new Error("UNIMPLEMENTED")); }; // Move a key to another database. redismock.move = function (key, db, callback) { return cb(callback)(new Error("UNIMPLEMENTED")); }; // Inspect the internals of Redis objects. redismock.object = function (subcommand, callback) { return cb(callback)(new Error("UNIMPLEMENTED")); }; // Remove the expiration from a key. redismock.persist = function (key, callback) { if (this.exists(key) && timeouts[key]) { clearTimeout(timeouts[key].timeout); return cb(callback)(null, 1); } return cb(callback)(null, 0); }; // Set a key's time to live in milliseconds. redismock.pexpire = function (key, milliseconds, callback) { var that = this; if (this.exists(key)) { if (timeouts[key]) { clearTimeout(timeouts[key].timeout); } if (milliseconds <= 0) { this.del(key); } else { timeouts[key] = {}; timeouts[key].start = new Date(); timeouts[key].end = (new Date(timeouts[key].start.getTime() + milliseconds)); timeouts[key].timeout = setTimeout(function () { timeouts[key] = undefined; that.del(key); }, milliseconds); } return cb(callback)(null, 1); } return cb(callback)(null, 0); }; // Set the expiration for a key as a UNIX timestamp specified in milliseconds. redismock.pexpireat = function (key, timestamp, callback) { var now = new Date(); return this.pexpire(key, timestamp - now.getTime(), callback); }; // Get the time to live for a key in milliseconds. redismock.pttl = function (key, callback) { var now = new Date(); if (timeouts[key]) { return cb(callback)(null, timeouts[key].end.getTime() - now.getTime()); } return cb(callback)(null, this.exists(key) ? -1 : -2); }; // Return a random key from the keyspace. redismock.randomkey = function (callback) { var rando = null; function loop_through(count, looper) { var key; for (key in looper) { if (key === sets) { count = loop_through(count, cache[sets]); continue; } else if (key === zsets) { count = loop_through(count, cache[zsets]); continue; } else if (key === hashes) { count = loop_through(count, cache[hashes]); continue; } else if (Math.random() < 1/count) { rando = key; } count += 1; } return count; } loop_through(1, cache); return cb(callback)(null, rando); }; // Rename a key. redismock.rename = function (key, newkey, callback) { var type; if (!this.exists(key)) { return cb(callback)(new Error('ERR no such key')); } if (key === newkey) { return cb(callback)(null, 'OK'); } type = this.type(key); if (type === 'string' || type === 'list') { cache[newkey] = cache[key]; } else if (type === 'set') { cache[sets][newkey] = cache[sets][key]; } else if (type === 'zset') { cache[zsets][newkey] = cache[zsets][key]; } else if (type === 'hash') { cache[hashes][newkey] = cache[hashes][key]; } // else can't occur yet... this.del(key); return cb(callback)(null, 'OK'); }; // Rename a key, only if the new key does not exist. redismock.renamenx = function (key, newkey, callback) { var r; if (this.exists(newkey)) { return cb(callback)(null, 0); } r = this.rename(key, newkey); if (r instanceof Error) { return cb(callback)(r); } return cb(callback)(null, 1); }; // Create a key using the provided serialized value, previously obtained using DUMP. redismock.restore = function (key, ttl, serialized_value, callback) { return cb(callback)(new Error('UNIMPLEMENTED')); }; // Sort the elements in a list, set, or sorted set. redismock.sort = function (key, callback) { return cb(callback)(new Error('UNIMPLEMENTED')); }; // Get the time to live for a key. redismock.ttl = function (key, callback) { var now = new Date(); if (timeouts[key]) { return cb(callback)(null, (timeouts[key].end.getTime() - now.getTime())/1000); } return cb(callback)(null, this.exists(key) ? -1 : -2); }; // Determine the type stored at key. redismock.type = function (key, callback) { if (this.exists(key)) { var type = typeof cache[key]; if (type === 'object') { if (cache[key] instanceof Array) { type = 'list'; } } else if (type === 'undefined') { if (key in cache[sets] && cache[sets][key]) { type = 'set'; } else if (key in cache[zsets] && cache[zsets][key]) { type = 'zset'; } else if (key in cache[hashes] && cache[hashes][key]) { type = 'hash'; } } return cb(callback)(null, type); } return cb(callback)(null, 'none'); }; // Incrementally iterate the keyspace. redismock.scan = function (cursor, callback) { var g = gather(this.scan).apply(null, arguments); var idx = 0; var key; var reply = []; var count, match; g .list .forEach(function (option, index) { if (option === 'count') { count = g.list[index + 1]; } if (option === 'match') { match = new RegExp(translate(g.list[index + 1])); } }); if (typeof count === 'undefined' || isNaN(parseInt(count, 10))) { count = 10; } callback = g.callback; for (key in cache) { if (cache.hasOwnProperty(key)) { if (key === sets || key === zsets || key === hashes) { continue; } idx += 1; if (idx > cursor) { if (!exists(match) || key.match(match)) { reply.push(key); if (reply.length >= count) { return cb(callback)(null, [idx, reply]); } } } } } for (key in cache[sets]) { if (cache[sets].hasOwnProperty(key)) { idx += 1; if (idx > cursor) { if (!exists(match) || key.match(match)) { reply.push(key); if (reply.length >= count) { return cb(callback)(null, [idx, reply]); } } } } } for (key in cache[zsets]) { if (cache[zsets].hasOwnProperty(key)) { idx += 1; if (idx > cursor) { if (!exists(match) || key.match(match)) { reply.push(key); if (reply.length >= count) { return cb(callback)(null, [idx, reply]); } } } } } for (key in cache[hashes]) { if (cache[hashes].hasOwnProperty(key)) { idx += 1; if (idx > cursor) { if (!exists(match) || key.match(match)) { reply.push(key); if (reply.length >= count) { return cb(callback)(null, [idx, reply]); } } } } } if (!reply.length) { idx = 0; } return cb(callback)(null, [idx, reply]); }; // String Commands // --------------- // Append a value to a key. redismock.append = function (key, value, callback) { return this .ifType(key, 'string', callback) .thenex(function () { cache[key] += value; return null; }) .thennx(function () { return this.set(key, value); }) .then(function () { return cache[key].length; }) .end(); }; /*var hamming_weight_map = { 0: 0, 1: 1, 2: 1, 3: 2, 4: 1, 5: 2, 6: 2, 7: 3, 8: 1, 9: 2, 10: 2, 11: 3, 12: 2, 13: 3, 14: 3, 15: 4 };*/ // Count set bits in a string. redismock.bitcount = function (key, callback) { var g = gather(this.bitcount).apply(this, arguments); var start, end; callback = g.callback; if (g.list.length === 3) { start = g.list[1]; end = g.list[2]; } return this .ifType(key, 'string', callback) .thenex(function () { var idx, n, count; if (!exists(start)) { start = 0; } if (!exists(end)) { end = cache[key].length - 1; } if (end >= cache[key].length) { end = cache[key].length - 1; } if (start < 0) { start = cache[key].length + start; } if (end < 0) { end = cache[key].length + end; } if (start > end) { return 0; } count = 0; // TODO: Slow bit-counting, do map to do fast bit counting. for (idx = start; idx <= end; idx += 1) { n = cache[key].charCodeAt(idx); while (n) { count += (n & 1); n >>= 1; } } return count; }) .thennx(function () { return 0; }) .end(); }; // Perform bitwise operations between strings. redismock.bitop = function (operation, destkey, key, callback) { var that = this; var g = gather(this.bitop).apply(this, arguments); var longest, strings, string, r; operation = typeof operation === 'string' ? operation.toLowerCase() : ''; if (operation !== 'and' && operation !== 'or' && operation !== 'xor' && operation !== 'not') { return cb(callback)(new Error('ERR syntax error')); } callback = g.callback; strings = g .list .slice(2) .map(function (k) { if (that.exists(k) && that.type(k) !== 'string') { return null; } return that.exists(k) ? cache[k] : ''; }); if (strings.some(function (str) { return str === null; })) { return wrongType(callback); } longest = strings.reduce(function (length, str) { return str.length > length ? str.length : length; }, 0); strings = strings.map(function (str) { while(str.length < longest) { str += '\0'; } return str; }); string = strings .reduce(function (cur, str, index) { var idx, n, s; s = ''; for (idx = 0; idx < longest; idx += 1) { if (operation === 'and') { n = cur.charCodeAt(idx) & str.charCodeAt(idx); } else if (operation === 'or') { n = cur.charCodeAt(idx) | str.charCodeAt(idx); } else if (operation === 'xor') { // a XOR a = 0, so we have to avoid XOR'ing the first string with itself. if (index > 0) { n = cur.charCodeAt(idx) ^ str.charCodeAt(idx); } else { n = cur.charCodeAt(idx); } } else if (operation === 'not') { n = ~cur.charCodeAt(idx); } s += String.fromCharCode(n); } return s; }, strings[0]); r = this.set(destkey, string); if (r instanceof Error) { return cb(callback)(r); } return cb(callback)(null, string.length); }; // Find first bit set or clear in a string. redismock.bitpos = function (key, bit, callback) { var g = gather(this.bitpos).apply(this, arguments); var start, end; callback = g.callback; if (g.list.length > 2) { start = g.list[2]; end = g.list[3]; } if (typeof start === 'undefined') { start = 0; } if (bit !== 0 && bit !== 1) { return cb(callback)(new Error('ERR The bit argument must be 1 or 0.')); } return this .ifType(key, 'string', callback) .thennx(function () { if (bit === 0) { return 0; } return -1; }) .thenex(function () { var idx, ch, cnt, noend = false; if (start < 0) { start = cache[key].length + start; } if (typeof end === 'undefined') { noend = true; end = cache[key].length - 1; } if (end < 0) { end = cache[key].length + end; } if (start > end) { return -1; } for (idx = start; idx <= end; idx += 1) { ch = cache[key].charCodeAt(idx); cnt = 0; while (cnt < 8) { if (bit === 0 && (ch & 0x80) !== 0x80) { return idx*8 + cnt; } if (bit === 1 && (ch & 0x80) === 0x80) { return idx*8 + cnt; } ch <<= 1; cnt += 1; } } if (bit === 1) { return -1; } if (bit === 0 && noend) { return idx*8; } return -1; }) .end(); }; redismock.decr = function (key, callback) { return this.decrby(key, 1, callback); }; redismock.decrby = function (key, decrement, callback) { return this .ifType(key, 'string', callback) .thennx(function () { this.set(key, '0'); }) .then(function () { var asInt = parseInt(this.get(key), 10); if (isNaN(asInt)) { return new Error("ERR value is not an integer or out of range"); } this.set(key, asInt - decrement); return (asInt - decrement); }) .end(); }; redismock.get = function (key, callback) { if (this.type(key) === 'string') { return cb(callback)(null, cache[key]); } return cb(callback)(null, null); }; // Returns the bit value at offset in the string value stored at key. redismock.getbit = function (key, offset, callback) { return this .ifType(key, 'string', callback) .thenex(function () { var n, pos; if (offset >= (cache[key].length*8)) { return 0; } n = cache[key].charCodeAt(Math.floor(offset/8)); pos = offset % 8; return (n >> pos) & 1; }) .thennx(function () { return 0; }) .end(); }; // Get a substring of the string stored at a key. redismock.getrange = function (key, start, end, callback) { return this .ifType(key, 'string', callback) .thenex(function () { var len = 0; if (end < 0) { end = cache[key].length + end; } if (start < 0) { len = end - (cache[key].length + start) + 1; } else { len = end - start + 1; } return cache[key].substr(start, len); }) .thennx(function () { return ""; }) .end(); }; // Set the string value of a key and return its old value. redismock.getset = function (key, value, callback) { var prev = this.get(key); this.set(key, value); return cb(callback)(null, prev); }; // Increment the integer value of a key by one. redismock.incr = function (key, callback) { return this.incrby(key, 1, callback); }; // Increment the integer value of a key by the given amount. redismock.incrby = function (key, increment, callback) { return this .ifType(key, 'string', callback) .thennx(function () { this.set(key, '0'); }) .then(function () { var asInt = parseInt(this.get(key), 10); if (isNaN(asInt)) { return new Error("ERR value is not an integer or out of range"); } this.set(key, asInt + increment); return (asInt + increment); }) .end(); }; // Increment the float value of a key by the given amount. redismock.incrbyfloat = function (key, increment, callback) { return this .ifType(key, 'string', callback) .thennx(function () { this.set(key, '0'); }) .then(function () { var asInt = parseFloat(this.get(key)); if (isNaN(asInt)) { return new Error("ERR value is not an integer or out of range"); } this.set(key, asInt + increment); return this.get(key); }) .end(); }; // Get the values of all the given keys. redismock.mget = function (key, callback) { var g = gather(this.mget).apply(this, arguments); callback = g.callback; var data = (typeof g.list[0] === 'object') ? g.list[0] : g.list; return cb(callback)(null, data.map(function (k) { return cache[k] || null; })); }; // Set multiple keys to multiple values. redismock.mset = function (key, value, callback) { var kvs = []; var that = this; var g = gather(this.mset).apply(this, arguments); callback = g.callback; g.list.forEach(function (opt, index) { if (index % 2 === 0) { kvs.push([opt, g.list[index + 1]]); } }); kvs.forEach(function (kv) { that.set(kv[0], kv[1]); }); return cb(callback)(null, 'OK'); }; // Set multiple keys to multiple values only if none of the keys exist. redismock.msetnx = function (key, value, callback) { var kvs = []; var that = this; var g = gather(this.msetnx).apply(this, arguments); callback = g.callback; g.list.forEach(function (opt, index) { if (index % 2 === 0) { kvs.push([opt, g.list[index + 1]]); } }); if (kvs.some(function (kv) { return that.exists(kv[0]); })) { return cb(callback)(null, 0); } kvs.forEach(function (kv) { that.set(kv[0], kv[1]); }); return cb(callback)(null, 1); }; // Set the value and expiration in milliseconds of a key. redismock.psetex = function (key, milliseconds, value, callback) { this.set(key, value); this.pexpire(key, milliseconds); return cb(callback)(null, 'OK'); }; // Set the string value of a key. redismock.set = function (key, value, callback) { var nx = false, xx = false, ex = -1, px = -1; var g = gather(this.set).apply(this, arguments); callback = g.callback; g .list .forEach(function (opt, index) { if (opt === 'nx' || opt === 'NX') { nx = true; } else if (opt === 'xx' || opt === 'XX') { xx = true; } else if (opt === 'ex' || opt === 'EX') { ex = g.list[index + 1]; } else if (opt === 'px' || opt === 'PX') { px = g.list[index + 1]; } }); if (nx) { if (this.exists(key)) { return cb(callback)(null, null); } } if (xx) { if (!this.exists(key)) { return cb(callback)(null, null); } } cache[key] = exists(value) ? value.toString() : ''; if (px !== -1) { redismock.pexpire(key, px); } if (ex !== -1) { redismock.expire(key, ex); } return cb(callback)(null, 'OK'); }; // Sets or clears the bit at offset in the string value stored at key. redismock.setbit = function (key, offset, value, callback) { return this .ifType(key, 'string', callback) .thennx(function () { cache[key] = ''; }) .then(function () { var byteIdx = Math.floor(offset/8); var bitIdx = offset % 8; var idx, bit, mask; var code; if (value !== 0 && value !== 1) { return new Error('ERR bit is not an integer or out of range'); } while (cache[key].length < byteIdx + 1) { cache[key] += '\0'; } code = cache[key].charCodeAt(byteIdx); idx = 0; mask = 0x80; while (idx < bitIdx) { mask >>= 1; idx += 1; } bit = (code & mask) === 0 ? 0 : 1; if (value === 0) { code = code & (~mask); } else { code = code | mask; } cache[key] = cache[key].substr(0, byteIdx) + String.fromCharCode(code) + cache[key].substr(byteIdx + 1); return bit; }) .end(); }; // Set the value and expiration of a key. redismock.setex = function (key, seconds, value, callback) { this.set(key, value); this.expire(key, seconds); return cb(callback)(null, 'OK'); }; // Set the value of the key only if the key does not exist. redismock.setnx = function (key, value, callback) { if (!this.exists(key)) { this.set(key, value); return cb(callback)(null, 1); } return cb(callback)(null, 0); }; // Overwrite part of the string at key starting at the specified offset. redismock.setrange = function (key, offset, value, callback) { return this .ifType(key, 'string', callback) .thennx(function () { cache[key] = ''; }) .then(function () { var idx, newValue; if (cache[key].length < offset + value.length - 1) { for (idx = cache[key].length; idx < offset + value.length; idx += 1) { cache[key] += '\0'; } } newValue = cache[key].substr(0, offset); for (idx = offset; idx < offset + value.length; idx += 1) { newValue += value[idx - offset]; } newValue += cache[key].substr(offset + value.length); cache[key] = newValue; return cache[key].length; }) .end(); }; // Get the length of the value stored in a key. redismock.strlen = function (key, callback) { if (!this.exists(key)) { return cb(callback)(null, 0); } return this .ifType(key, 'string', callback) .thenex(function () { return cache[key].length; }) .thennx(function () { return 0; }) .end(); }; // List Commands // ------------- redismock.blpop = function (key, timeout, callback) { var that = this; var g = gather(this.blpop).apply(null, arguments); var keys = g.list.slice(0, g.list.length - 1); timeout = g.list[g.list.length - 1]; callback = g.callback; var f, timedout = false; f = function () { if (timedout) { return cb(callback)(null, null); } if (keys.some(function (k) { var len = that.llen(k); if (len instanceof Error) { cb(callback)(len); return true; } if (len > 0) { that.lpop(k, callback); return true; } return false; })) { return; } setImmediate(f); }; if (timeout > 0) { setTimeout(function () { timedout = true; }, timeout*1000); } setImmediate(f); return this; }; redismock.brpop = function (key, timeout, callback) { var that = this; var g = gather(this.brpop).apply(null, arguments); var keys = g.list.slice(0, g.list.length - 1); var f, timedout = false; timeout = g.list[g.list.length - 1]; callback = g.callback; f = function () { if (timedout) { return cb(callback)(null, null); } if (keys.some(function (k) { var len = that.llen(k); if (len instanceof Error) { cb(callback)(len); return true; } if (len > 0) { that.rpop(k, callback); return true; } return false; })) { return; } setImmediate(f); }; if (timeout > 0) { setTimeout(function () { timedout = true; }, timeout*1000); } setImmediate(f); return this; }; redismock.brpoplpush = function (source, destination, timeout, callback) { var that = this; var f, timedout = false; f = function () { var len; if (timedout) { return cb(callback)(null, null); } len = that.llen(source); if (len instanceof Error) { return cb(callback)(len); } if (len > 0) { that.rpoplpush(source, destination, callback); return; } setImmediate(f); }; if (timeout > 0) { setTimeout(function () { timedout = true; }, timeout*1000); } setImmediate(f); return this; }; redismock.lindex = function (key, i, callback) { var elem = null; return this .ifType(key, 'list', callback) .thenex(function () { if (i >= 0 && i < cache[key].length) { elem = cache[key][i]; } else if (i < 0 && cache[key].length + 1 >= 0) { elem = cache[key][cache[key].length + i]; } }) .then(function () { return elem; }) .end(); }; redismock.linsert = function (key, beforeafter, pivot, value, callback) { return this .ifType(key, 'list', callback) .thenex(function () { var idx = cache[key].indexOf(pivot); if (idx !== -1) { if (beforeafter === 'before') { cache[key].splice(idx, 0, value); } else if (beforeafter === 'after') { cache[key].splice(idx + 1, 0, value); } return cache[key].length; } return -1; }) .thennx(function () { return 0; }) .end(); }; redismock.llen = function (key, callback) { return this .ifType(key, 'list', callback) .thenex(function () { return cache[key].length; }) .thennx(function () { return 0; }) .end(); }; redismock.lpop = function (key, callback) { return this .ifType(key, 'list', callback) .thenex(function () { var popped = cache[key].shift(); if (!cache[key].length) { this.del(key); } return popped; }) .thennx(function () { return null; }) .end(); }; redismock.lpush = function (key, element, callback) { var g = gather(this.lpush).apply(this, arguments); callback = g.callback; return this .ifType(key, 'list', callback) .thennx(function () { cache[key] = new redismock.Array(); }) .then(function () { g.list.slice(1).forEach(function (elem) { cache[key].unshift(elem); }); return cache[key].length; }) .end(); }; redismock.lpushx = function (key, element, callback) { return this .ifType(key, 'list', callback) .thenex(function () { cache[key].unshift(element); return cache[key].length; }) .thennx(function () { return 0; }) .end(); }; redismock.lrange = function (key, start, end, callback) { var l = []; return this .ifType(key, 'list', callback) .thenex(function () { if (start > cache[key].length - 1) { l = []; } else { if (start < 0) { start = cache[key].length + start; } if (end < 0) { end = cache[key].length + end; } if (start > end) { l = []; } else { l = cache[key].slice(start, end + 1); } } }) .then(function () { return l; }) .end(); }; redismock.lrem = function (key, count, element, callback) { var cnt = 0; return this .ifType(key, 'list', callback) .thenex(function () { var idx; while (true) { idx = cache[key].indexOf(element); if (idx === -1) { break; } cache[key].splice(idx, 1); cnt += 1; if (count > 0 && cnt === count) { break; } } if (!cache[key].length) { this.del(key); } }) .then(function () { return cnt; }) .end(); }; redismock.lset = function (key, index, element, callback) { return this .ifType(key, 'list', callback) .thenex(function () { if (index >= cache[key].length) { return new Error('ERR index out of range'); } cache[key][index] = element; return 'OK'; }) .thennx(function () { return new Error('ERR no such key'); }) .end(); }; redismock.ltrim = function (key, start, end, callback) { return this .ifType(key, 'list', callback) .thenex(function () { var tmpS, tmpE; if (start > cache[key].length - 1 || start > end) { cache[key] = new redismock.Array(); } else { if (start < 0 && end < 0) { tmpE = cache[key].length + end; tmpS = cache[key].length + start; if (tmpS < 0) { tmpS = 0; } start = tmpS; end = tmpE; } if (end > cache[key].length - 1) { end = cache[key].length - 1; } cache[key] = cache[key].slice(start, end + 1); } }) .then(function () { if (this.exists(key) && !cache[key].length) { this.del(key); } return 'OK'; }) .end(); }; redismock.rpop = function (key, callback) { return this .ifType(key, 'list', callback) .thenex(function () { var popped = cache[key].pop(); if (!cache[key].length) { this.del(key); } return popped; }) .thennx(function () { return null; }) .end(); }; redismock.rpoplpush = function (source, dest, callback) { var element = null, reply; if (this.exists(source) && this.type(source) !== 'list') { return wrongType(callback); } if (this.exists(dest) && this.type(dest) !== 'list') { return wrongType(callback); } element = this.rpop(source); if (element instanceof Error) { return cb(callback)(element); } if (element) { reply = this.lpush(dest, element); if (reply instanceof Error) { return cb(callback)(reply); } } return cb(callback)(null, element); }; redismock.rpush = function (key, element, callback) { var g = gather(this.rpush).apply(this, arguments); callback = g.callback; return this .ifType(key, 'list', callback) .thennx(function () { cache[key] = new redismock.Array(); }) .then(function () { cache[key] = cache[key].concat(g.list.slice(1)); return cache[key].length; }) .end(); }; redismock.rpushx = function (key, element, callback) { return this .ifType(key, 'list', callback) .thenex(function () { cache[key].push(element); return cache[key].length; }) .thennx(function () { return 0; }) .end(); }; // Set Commands // ------------ redismock.sadd = function (key, member, callback) { var g = gather(this.sadd).apply(this, arguments); var count = 0; callback = g.callback; return this .ifType(key, 'set', callback) .thennx(function () { cache[sets][key] = {}; }) .then(function () { g.list.slice(1).forEach(function (m) { m = m ? m.toString() : ''; if (m.length === 0) { return; } if (!(m in cache[sets][key])) { cache[sets][key][m] = ''; count += 1; } }); return count; }) .end(); }; redismock.scard = function (key, callback) { return this .ifType(key, 'set', callback) .thenex(function () { return Object.keys(cache[sets][key]).length; }) .thennx(function () { return 0; }) .end(); }; redismock.sdiff = function (key, callback) { var that = this; var g = gather(this.sdiff).apply(this, arguments); callback = g.callback; if (!g.list.every(function (k) { return that.type(k) === 'set' || that.type(k) === 'none'; })) { return wrongType(callback); } return this .ifType(key, 'set', callback) .thennx(function () { return []; }) .thene