Commits

Nikhil Marathe committed be7ced3

Updated to latest redis-node-client

  • Participants
  • Parent commits 01f6375

Comments (0)

Files changed (4)

File deps/redis-node-client/LICENSE

-Copyright (c) 2009 Fictorial LLC
+Copyright (c) 2009, 2010 Fictorial LLC
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal

File deps/redis-node-client/README.md

-# redis-node-client
+# Redis client for Node.js
 
-A Redis client implementation for Node.js which runs atop Google V8.
+## In a nutshell
 
-This project lets you access a Redis instance using server-side JavaScript.
+- Talk to Redis from Node.js 
+- Fully asynchronous; your code is called back when an operation completes
+- [Binary-safe](http://github.com/fictorial/redis-node-client/blob/master/test/test.js#L353-363); uses Node.js Buffer objects for request serialization and reply parsing
+- Client API directly follows Redis' [command specification](http://code.google.com/p/redis/wiki/CommandReference) 
+- *You have to understand how Redis works and the semantics of its command set to most effectively use this client*
+- Supports Redis' new exciting PUBSUB commands
 
-## Asynchronicity
+Recent changes completely break backwards compatibility.  Sorry, it was time.
 
-Node.js does not block on I/O operations.
+## Synopsis
 
-This means that while a typical Redis client might have code that accesses a
-Redis server in a blocking call, Node.js-based code cannot.
+When working from a git clone:
 
-Typical Redis client (e.g. Python):
-
-    foo = client.get('counter')
-
-This Node.js-based Redis client:
-
+    var client = require("./lib/redis-client").createClient(); 
     var sys = require("sys");
-    var redis = require("./redis");
-
-    var client = new redis.Client();
-    client.connect(learn_to_count);
-
-    function learn_to_count () {
-        client.incr('counter').addCallback(function (value) {
-            sys.puts("counter is now " + value);
+    client.stream.addListener("connect", function () {
+        client.info(function (err, info) {
+            if (err) throw new Error(err);
+            sys.puts("Redis Version is: " + info.redis_version);
             client.close();
         });
-    }
+    });
 
-Running this example, we'd see:
+When working with a Kiwi-based installation:
 
-    $ node counter-example.js
-    counter is now 1
-    $ node counter-example.js
-    counter is now 2
-    $ node counter-example.js
-    counter is now 3
+    // $ kiwi install redis-client
 
-That is, you must supply a callback function that is called when Redis returns,
-even if Redis queries are extremely fast.
+    var sys = require("sys"), 
+        kiwi = require("kiwi"),
+        client = kiwi.require("redis-client").createClient();
 
-A potential upside to this slightly awkward requirement is that you can enjoy
-the benefits of pipelining many Redis queries in a non-blocking way.  Redis
-returns replies for requests in the order received.
+    client.stream.addListener("connect", function () {
+        client.info(function (err, info) {
+            if (err) throw new Error(err);
+            sys.puts("Redis Version is: " + info.redis_version);
+            client.close();
+        });
+    });
 
-See the [test.js](http://github.com/fictorial/redis-node-client/raw/master/test.js) 
-file as a good example of this.
+- Refer to the many tests in `test/test.js` for many usage examples.
+- Refer to the `examples/` directory for focused examples.
 
-## Status
+## Installation
 
-* The full Redis 1.1 command specification is supported.
-* All tests pass using Redis HEAD at commit (09f6f7020952cd93e178da11e66e36f8a98398d1; Dec 1 2009) 
-* All tests pass using Node.js v0.1.20-3-g5b1a535 (Dec 1 2009)
-* See the TODO file for known issues.
+This version requires at least `Node.js v0.1.90` and Redis `1.3.8`.
 
-## Testing
+Tested with node `v0.1.91-20-g6e715b8`.
 
-To test:
+You have a number of choices:
 
-1. fire up redis-server on 127.0.0.1:6379 (the default)
-1. install node.js 
-1. run `node test.js`
+- git clone this repo or download a tarball and simply copy `lib/redis-client.js` into your project
+- use git submodule
+- use the [Kiwi](http://github.com/visionmedia/kiwi) package manager for Node.js
+- use the [NPM](http://github.com/isaacs/npm) package manager for Node.js
 
-## Author
+Please let me know if the package manager "seeds" and/or metadata have issues.
+Installation via Kiwi or NPM at this point isn't really possible since this repo
+depends on a unreleased version of Node.js.
 
-Brian Hammond, Fictorial (brian at fictorial dot com)
+## Running the tests
 
-## Copyright
+A good way to learn about this client is to read the test code.
 
-Copyright (C) 2009 Fictorial LLC
+To run the tests, install and run redis on the localhost on port 6379 (defaults).
+Then run `node test/test.js [-v|-q]` where `-v` is for "verbose" and `-q` is for "quiet".
 
-## License
+    $ node test/test.js
+    ..................................................................
+    ...........................++++++++++++++++++++++++++++++++++++
 
-See LICENSE (it's MIT; go nuts).
+    [INFO] All tests have passed.
+
+If you see something like "PSUBSCRIBE: unknown command" then it is time to upgrade
+your Redis installation.
+
+## Documentation
+
+There is a method per Redis command.  E.g. `SETNX` becomes `client.setnx`.
+
+For example, the Redis command [INCRBY](http://code.google.com/p/redis/wiki/IncrCommand)
+is specified as `INCRBY key integer`.  Also, the INCRBY spec says that the reply will
+be "... the new value of key after the increment or decrement."
+
+This translates to the following client code which increments key 'foo' by 42.  If
+the value at key 'foo' was 0 or non-existent, 'newValue' will take value 42 when
+the callback function is called.
+
+    client.incrby('foo', 42, function (err, newValue) {
+        // ...
+    });
+
+This can get [a little wacky](http://github.com/fictorial/redis-node-client/blob/master/test/test.js#L1093-1097). 
+I'm open to suggestions for improvement here.
+
+Note: for PUBSUB, you should use `subscribeTo` and `unsubscribeFrom` instead of the generated
+methods for Redis' `SUBSCRIBE` and `UNSUBSCRIBE` commands.  See [this](http://github.com/fictorial/redis-node-client/blob/master/lib/redis-client.js#L682-694)
+and [this](http://github.com/fictorial/redis-node-client/blob/master/examples/subscriber.js#L14).
+
+## Notes
+
+All commands/requests use the Redis *multi-bulk request* format which will be
+the only accepted request protocol come Redis 2.0.
+
+## Metadata
+
+- *Author*: Brian Hammond (brian at fictorial dot com) with various patches 
+  from nice people everywhere.
+- *Copyright*: Copyright (C) 2010 Fictorial LLC.
+- *License*: MIT
+

File deps/redis-node-client/lib/redis-client.js

+/*
+    Redis client module for Node.js
+
+    Copyright (C) 2010 Fictorial LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining
+    a copy of this software and associated documentation files (the
+    "Software"), to deal in the Software without restriction, including without
+    limitation the rights to use, copy, modify, merge, publish, distribute,
+    sublicense, and/or sell copies of the Software, and to permit persons to
+    whom the Software is furnished to do so, subject to the following
+    conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+*/
+
+// To add support for new commands, edit the array called "commands" at the
+// bottom of this file.
+
+// Set this to true to aid in debugging wire protocol input/output,
+// parsing methods, etc.
+
+exports.debugMode = false;
+
+var net = require("net"),
+    sys = require("sys"),
+    Buffer = require('buffer').Buffer,
+
+    CRLF = "\r\n",
+    CRLF_LEN = 2,
+    MAX_RECONNECTION_ATTEMPTS = 10,
+
+    PLUS      = exports.PLUS      = 0x2B, // +
+    MINUS     = exports.MINUS     = 0x2D, // -
+    DOLLAR    = exports.DOLLAR    = 0x24, // $
+    STAR      = exports.STAR      = 0x2A, // *
+    COLON     = exports.COLON     = 0x3A, // :
+    CR        = exports.CR        = 0x0D, // \r
+    LF        = exports.LF        = 0x0A, // \n
+                                
+    NONE      = exports.NONE      = "NONE",
+    BULK      = exports.BULK      = "BULK",     
+    MULTIBULK = exports.MULTIBULK = "MULTIBULK",
+    INLINE    = exports.INLINE    = "INLINE",   
+    INTEGER   = exports.INTEGER   = "INTEGER",  
+    ERROR     = exports.ERROR     = "ERROR";    
+
+function debugFilter(buffer, len) {
+    // Redis is binary-safe but assume for debug display that 
+    // the encoding of textual data is UTF-8.
+
+    var filtered = buffer.utf8Slice(0, len);
+
+    filtered = filtered.replace(/\r\n/g, '<CRLF>');
+    filtered = filtered.replace(/\r/g, '<CR>');
+    filtered = filtered.replace(/\n/g, '<LF>');
+
+    return filtered;
+}
+
+// fnmatch mirrors (mostly) the functionality of fnmatch(3) at least
+// in the same way as Redis.  
+
+var qmarkRE = /\?/g;
+var starRE  = /\*/g;
+var dotRE   = /\./g;
+
+function fnmatch (pattern, test) {
+    var newPattern = pattern.replace(dotRE, '(\\.)')
+                            .replace(qmarkRE, '(.)')
+                            .replace(starRE, '(.*?)');
+    return (new RegExp(newPattern)).test(test);
+}
+
+// A fully interruptable, binary-safe Redis reply parser.
+// 'callback' is called with each reply parsed in 'feed'.
+// 'thisArg' is the "thisArg" for the callback "call".
+
+function ReplyParser(callback, thisArg) {
+    this.onReply = callback;
+    this.thisArg = thisArg;
+    this.clearState();
+    this.clearMultiBulkState();
+}
+
+exports.ReplyParser = ReplyParser;
+
+ReplyParser.prototype.clearState = function () {
+    this.type = NONE;
+    this.bulkLengthExpected = null;
+    this.valueBufferLen = 0;
+    this.skip = 0;
+    this.valueBuffer = new Buffer(4096);
+};
+
+ReplyParser.prototype.clearMultiBulkState = function () {
+    this.multibulkReplies = null; 
+    this.multibulkRepliesExpected = null;
+};
+
+ReplyParser.prototype.feed = function (inbound) {
+    for (var i=0; i < inbound.length; ++i) {
+        if (this.skip > 0) {
+            this.skip--;
+            continue;
+        }
+
+        var typeBefore = this.type;
+
+        if (this.type === NONE) {
+            switch (inbound[i]) {
+                case DOLLAR: this.type = BULK;      break;
+                case STAR:   this.type = MULTIBULK; break;
+                case COLON:  this.type = INTEGER;   break;
+                case PLUS:   this.type = INLINE;    break;
+                case MINUS:  this.type = ERROR;     break;
+            }
+        }
+
+        // Just a state transition on '*', '+', etc.?  
+
+        if (typeBefore != this.type)
+            continue;
+
+        // If the reply is a part of a multi-bulk reply.  Save it.  If we have
+        // received all the expected replies of a multi-bulk reply, then
+        // callback.  If the reply is not part of a multi-bulk. Call back
+        // immediately.
+
+        var self = this;
+
+        var maybeCallbackWithReply = function (reply) {
+            if (self.multibulkReplies != null) {
+                self.multibulkReplies.push(reply);
+                if (--self.multibulkRepliesExpected == 0) {
+                    self.onReply.call(self.thisArg, { 
+                        type:  MULTIBULK, 
+                        value: self.multibulkReplies 
+                    });
+                    self.clearMultiBulkState();
+                }
+            } else {
+                self.onReply.call(self.thisArg, reply);
+            }
+            self.clearState();
+            self.skip = 1; // Skip LF
+        };
+
+        switch (inbound[i]) {
+        case CR:
+            switch (this.type) {
+                case INLINE:
+                case ERROR:
+                    // CR denotes end of the inline/error value.  
+                    // +OK\r\n
+                    //    ^
+
+                    var inlineBuf = new Buffer(this.valueBufferLen);
+                    this.valueBuffer.copy(inlineBuf, 0, 0, this.valueBufferLen);
+                    maybeCallbackWithReply({ type:this.type, value:inlineBuf });
+                    break;
+
+                case INTEGER:
+                    // CR denotes the end of the integer value.  
+                    // :42\r\n
+                    //    ^
+
+                    var n = parseInt(this.valueBuffer.asciiSlice(0, this.valueBufferLen), 10);
+                    maybeCallbackWithReply({ type:INTEGER, value:n });
+                    break;
+
+                case BULK:
+                    if (this.bulkLengthExpected == null) {
+                        // CR denotes end of first line of a bulk reply,
+                        // which is the length of the bulk reply value.
+                        // $5\r\nhello\r\n
+                        //   ^
+
+                        var bulkLengthExpected = 
+                            parseInt(this.valueBuffer.asciiSlice(0, this.valueBufferLen), 10);
+
+                        if (bulkLengthExpected <= 0) {
+                            maybeCallbackWithReply({ type:BULK, value:null });
+                        } else {
+                            this.clearState();
+
+                            this.bulkLengthExpected = bulkLengthExpected;
+                            this.type = BULK;
+                            this.skip = 1;  // skip LF
+                        }
+                    } else if (this.valueBufferLen == this.bulkLengthExpected) {
+                        // CR denotes end of the bulk reply value.
+                        // $5\r\nhello\r\n
+                        //            ^
+
+                        var bulkBuf = new Buffer(this.valueBufferLen);
+                        this.valueBuffer.copy(bulkBuf, 0, 0, this.valueBufferLen);
+                        maybeCallbackWithReply({ type:BULK, value:bulkBuf });
+                    } else {
+                        // CR is just an embedded CR and has nothing to do
+                        // with the reply specification.
+                        // $11\r\nhello\rworld\r\n
+                        //             ^
+                        
+                        this.valueBuffer[this.valueBufferLen++] = inbound[i];
+                    }
+                    break;
+
+                case MULTIBULK:
+                    // Parse the count which is the number of expected replies
+                    // in the multi-bulk reply.
+                    // *2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
+                    //   ^
+
+                    var multibulkRepliesExpected = 
+                        parseInt(this.valueBuffer.asciiSlice(0, this.valueBufferLen), 10);
+
+                    if (multibulkRepliesExpected <= 0) {
+                        maybeCallbackWithReply({ type:MULTIBULK, value:null });
+                    } else {
+                        this.clearState();
+                        this.skip = 1;    // skip LF
+                        this.multibulkReplies = [];
+                        this.multibulkRepliesExpected = multibulkRepliesExpected;
+                    }
+                    break;
+            }
+            break;
+
+        default:
+            this.valueBuffer[this.valueBufferLen++] = inbound[i];
+            break;
+        }
+
+        // If the current value buffer is too big, create a new buffer, copy in
+        // the old buffer, and replace the old buffer with the new buffer.
+ 
+        if (this.valueBufferLen === this.valueBuffer.length) {
+            var newBuffer = new Buffer(this.valueBuffer.length * 2);
+            this.valueBuffer.copy(newBuffer, 0, 0);
+            this.valueBuffer = newBuffer;
+        }
+    }
+};
+
+function Client(stream) {
+    this.originalCommands = [];
+    this.channelCallbacks = {};
+    this.requestBuffer = new Buffer(512);
+    this.replyParser = new ReplyParser(this.onReply_, this);
+    var client = this;
+    this.stream = stream;
+
+    this.stream.addListener("connect", function () {
+        if (exports.debugMode)
+            sys.debug("[CONNECTED]");
+
+        this.setNoDelay();
+        this.setTimeout(0);
+
+        client.noReconnect = false;
+        client.reconnectionAttempts = 0;
+    });
+
+    this.stream.addListener("data", function (buffer) {
+        if (exports.debugMode)
+            sys.debug("[RECV] " + debugFilter(buffer, buffer.length));
+
+        client.replyParser.feed(buffer);
+    });
+
+    this.stream.addListener("end", function () {
+        this.end();
+    });
+
+    stream.addListener("close", function (inError) {
+        if (exports.debugMode)
+            sys.debug("[DISCONNECTED]");
+
+        if (!client.noReconnect &&
+            client.reconnectionAttempts++ < MAX_RECONNECTION_ATTEMPTS) {
+            this.setTimeout(30);
+            this.connect(this.port, this.host);
+        }
+    });
+}
+
+exports.Client = Client;
+
+exports.createClient = function (port, host) {
+    var port = port || 6379;
+    var host = host || '127.0.0.1';
+
+    var client = new Client(new net.createConnection(port, host));
+
+    client.port = port;
+    client.host = host;
+
+    return client;
+};
+
+Client.prototype.close = function () {
+    this.noReconnect = true;
+    this.stream.end();
+};
+
+Client.prototype.onReply_ = function (reply) {
+    if (this.handlePublishedMessage_(reply)) 
+        return;
+
+    var originalCommand = this.originalCommands.shift();
+    var callback = originalCommand[originalCommand.length - 1];
+
+    // Callbacks expect (err, reply) as args.
+
+    if (typeof callback == "function") {
+        if (reply.type == ERROR) {
+            callback(reply.value.utf8Slice(0, reply.value.length), null);
+        } else {
+            callback(null, maybeConvertReplyValue(originalCommand[0], reply));
+        }
+    }
+};
+
+Client.prototype.handlePublishedMessage_ = function (reply) {
+    // We're looking for a multibulk like: 
+    // ["message", "channelName", messageBuffer] 
+
+    if (reply.type != MULTIBULK ||
+        !(reply.value instanceof Array) ||
+        reply.value.length != 3 ||
+        reply.value[0].value.length != 7 ||
+        reply.value[0].value.asciiSlice(0, 7) != 'message') 
+        return false;
+
+    // This is tricky. We are returning true even though there 
+    // might not be any callback called! This may happen when a
+    // caller subscribes then unsubscribes while a published
+    // message is in transit to us. When the message arrives, no
+    // one is there to consume it. In essence, as long as the 
+    // reply type is a published message (see above), then we've
+    // "handled" the reply.
+        
+    if (Object.getOwnPropertyNames(this.channelCallbacks).length == 0) 
+        return true;
+
+    var channelNameOrPattern = reply.value[1].value;
+    var channelCallback = this.channelCallbacks[channelNameOrPattern];
+    if (typeof channelCallback == 'undefined') {
+        // No 1:1 channel name match. 
+        //
+        // Perhaps the subscription was for a pattern (PSUBSCRIBE)?
+        // Redis does not send the pattern that matched from an
+        // original PSUBSCRIBE request.  It sends the (fn)matching
+        // channel name instead.  Thus, let's try to fnmatch the
+        // channel the message was published to/on to a subscribed
+        // pattern, and callback the associated function.
+        // 
+        // A -> Redis     PSUBSCRIBE foo.*
+        // B -> Redis     PUBLISH foo.bar hello
+        // Redis -> A     MESSAGE foo.bar hello   (no pattern specified)
+
+        var channelNamesOrPatterns = 
+            Object.getOwnPropertyNames(this.channelCallbacks);
+
+        for (var i=0; i < channelNamesOrPatterns.length; ++i) {
+            var thisNameOrPattern = channelNamesOrPatterns[i];
+            if (fnmatch(thisNameOrPattern, channelNameOrPattern)) {
+                channelCallback = this.channelCallbacks[thisNameOrPattern];
+                break;
+            }
+        }
+    }
+
+    if (typeof(channelCallback) === 'function') {
+        // Good, we found a function to callback.
+
+        var payload = reply.value[2].value;
+        channelCallback(channelNameOrPattern, payload);
+        return true;
+    }
+
+    return false;
+}
+
+function maybeAsNumber(str) {
+    var value = parseInt(str, 10);
+
+    if (isNaN(value)) 
+        value = parseFloat(str);
+
+    if (isNaN(value)) 
+        return str;
+
+    return value;
+}
+
+function maybeConvertReplyValue(commandName, reply) {
+    if (reply.value === null)
+        return null;
+
+    // Redis' INFO command returns a BULK reply of the form:
+    // "redis_version:1.3.8
+    // arch_bits:64
+    // multiplexing_api:kqueue
+    // process_id:11604
+    // ..."
+    // 
+    // We convert that to a JS object like:
+    // { redis_version: '1.3.8'
+    // , arch_bits: '64'
+    // , multiplexing_api: 'kqueue'
+    // , process_id: '11604'
+    // , ... }
+
+    if (commandName === 'info' && reply.type === BULK) {
+        var info = {};
+        reply.value.asciiSlice(0, reply.value.length).split(/\r\n/g)
+            .forEach(function (line) {
+                var parts = line.split(':');
+                if (parts.length === 2)
+                    info[parts[0]] = parts[1];
+            });
+        return info;
+    }
+
+    // HGETALL returns a MULTIBULK where each consecutive reply-pair
+    // is a key and value for the Redis HASH.  We convert this into
+    // a JS object.
+
+    if (commandName === 'hgetall' && 
+        reply.type === MULTIBULK &&
+        reply.value.length % 2 === 0) {
+
+        var hash = {};
+        for (var i=0; i<reply.value.length; i += 2) 
+            hash[reply.value[i].value] = reply.value[i + 1].value;
+        return hash;
+    }
+
+    // Redis returns "+OK\r\n" to signify success.
+    // We convert this into a JS boolean with value true.
+    
+    if (reply.type === INLINE && reply.value.asciiSlice(0,2) === 'OK')
+        return true;
+
+    // ZSCORE returns a string representation of a floating point number.
+    // We convert this into a JS number.
+
+    if (commandName === "zscore")
+        return maybeAsNumber(reply.value);
+
+    // Multibulk replies are returned from our reply parser as an
+    // array like: [ {type:BULK, value:"foo"}, {type:BULK, value:"bar"} ]
+    // But, end-users want the value and don't care about the
+    // Redis protocol reply types.  We here extract the value from each
+    // object in the multi-bulk array.
+
+    if (reply.type === MULTIBULK)
+        return reply.value.map(function (element) { return element.value; });
+
+    // Otherwise, we have no conversions to offer.
+
+    return reply.value;
+}
+
+exports.maybeConvertReplyValue_ = maybeConvertReplyValue;
+
+var commands = [ 
+    "append",
+    "auth",
+    "bgsave",
+    "blpop",
+    "brpop",
+    "dbsize",
+    "decr",
+    "decrby",
+    "del",
+    "exists",
+    "expire",
+    "expireat",
+    "flushall",
+    "flushdb",
+    "get",
+    "getset",
+    "hdel",
+    "hexists",
+    "hget",
+    "hgetall",
+    "hincrby",
+    "hkeys",
+    "hlen",
+    "hmget",
+    "hmset",
+    "hset",
+    "hvals",
+    "incr",
+    "incrby",
+    "info",
+    "keys",
+    "lastsave",
+    "len",
+    "lindex",
+    "llen",
+    "lpop",
+    "lpush",
+    "lrange",
+    "lrem",
+    "lset",
+    "ltrim",
+    "mget",
+    "move",
+    "mset",
+    "msetnx",
+    "psubscribe",
+    "publish",
+    "punsubscribe",
+    "randomkey",
+    "rename",
+    "renamenx",
+    "rpop",
+    "rpoplpush",
+    "rpush",
+    "sadd",
+    "save",
+    "scard",
+    "sdiff",
+    "sdiffstore",
+    "select",
+    "set",
+    "setnx",
+    "shutdown",
+    "sinter",
+    "sinterstore",
+    "sismember",
+    "smembers",
+    "smove",
+    "sort",
+    "spop",
+    "srandmember",
+    "srem",
+    "subscribe",
+    "sunion",
+    "sunionstore",
+    "ttl",
+    "type",
+    "unsubscribe",
+    "zadd",
+    "zcard",
+    "zcount",
+    "zincrby",
+    "zinter",
+    "zrange",
+    "zrangebyscore",
+    "zrank",
+    "zrem",
+    "zrembyrank",
+    "zremrangebyrank",
+    "zremrangebyscore",
+    "zrevrange",
+    "zrevrank",
+    "zscore",
+    "zunion",
+];
+
+// For internal use but maybe useful in rare cases or when the client command
+// set is not 100% up to date with Redis' latest commands.
+// client.sendCommand('GET', 'foo', function (err, value) {...});
+//
+// arguments[0]      = commandName
+// arguments[1..N-2] = Redis command arguments
+// arguments[N-1]    = callback function
+
+Client.prototype.sendCommand = function () {
+    var commandName = arguments[0].toLowerCase();
+    var originalCommand = Array.prototype.slice.call(arguments);
+
+    // Invariant: number of queued callbacks == number of commands sent to
+    // Redis whose replies have not yet been received and processed.  Thus,
+    // if no callback was given, we create a dummy callback.
+
+    var argCount = arguments.length;
+    if (typeof arguments[argCount - 1] == 'function')
+        --argCount;
+
+    // All requests are formatted as multi-bulk.
+    // The first line of a multi-bulk request is "*<number of parts to follow>\r\n".
+    // Next is: "$<length of the command name>\r\n<command name>\r\n".
+
+    // Write the request as we go into a request Buffer.  Recall that buffers
+    // are fixed length.  We thus guess at how much space is needed.  If we
+    // need to grow beyond this, we create a new buffer, copy the old one, and
+    // continue.  Once we're ready to write the buffer, we use a 0-copy slice
+    // to send just that which we've written to the buffer.
+    //
+    // We reuse the buffer after each request. When the buffer "grows" to
+    // accomodate a request, it stays that size until it needs to grown again,
+    // which may of course be never.
+
+    var offset = this.requestBuffer.utf8Write('*' + argCount.toString() + CRLF +
+                                              '$' + commandName.length + CRLF +
+                                              commandName + CRLF, 0);
+
+    var self = this;
+
+    function ensureSpaceFor(atLeast) {
+      var currentLength = self.requestBuffer.length;
+
+      if (offset + atLeast > currentLength) {
+        // If we know how much space we need, use that + 10%.
+        // Else double the size of the buffer.
+
+        var bufferLength = Math.max(currentLength * 2, atLeast * 1.1);
+        var newBuffer = new Buffer(Math.round(bufferLength));
+        self.requestBuffer.copy(newBuffer, 0, 0, offset); // target, targetStart, srcStart, srcEnd
+        self.requestBuffer = newBuffer;
+      }
+    }
+
+    // Serialize the arguments into the request buffer
+    // If the request is a Buffer, just copy.  Else if
+    // the arg has a .toString() method, call it and write
+    // it to the request buffer as UTF8.
+
+    var extrasLength = 5;   // '$', '\r\n', '\r\n'
+
+    for (var i=1; i < argCount; ++i) {
+        var arg = arguments[i];
+        if (arg instanceof Buffer) {
+            ensureSpaceFor(arg.length + arg.length.toString().length + extrasLength);
+            offset += this.requestBuffer.asciiWrite('$' + arg.length + CRLF, offset);
+            offset += arg.copy(this.requestBuffer, offset, 0);  // target, targetStart, srcStart
+            offset += this.requestBuffer.asciiWrite(CRLF, offset);
+        } else if (arg.toString) {
+            var asString = arg.toString();
+            var serialized = '$' + process._byteLength(asString) + CRLF + asString + CRLF;
+            ensureSpaceFor(process._byteLength(serialized));
+            offset += this.requestBuffer.utf8Write(serialized, offset);
+        }
+    }
+
+    this.originalCommands.push(originalCommand);
+    this.stream.write(this.requestBuffer.slice(0, offset));
+
+    if (exports.debugMode) 
+        sys.debug("[SEND] " + debugFilter(this.requestBuffer, offset));
+};
+
+commands.forEach(function (commandName) {
+    Client.prototype[commandName] = function () {
+        if (this.stream.readyState != "open") 
+            throw new Error("Sorry, the command cannot be sent to Redis. " +
+                            "The connection state is '" + 
+                            this.stream.readyState + "'.");
+
+        var args = Array.prototype.slice.call(arguments);
+        args.unshift(commandName);
+        this.sendCommand.apply(this, args);
+    };
+});
+
+// Wraps 'subscribe' and 'psubscribe' methods to manage a single
+// callback function per subscribed channel name/pattern.
+//
+// 'nameOrPattern' is a channel name like "hello" or a pattern like 
+// "h*llo", "h?llo", or "h[ae]llo".
+//
+// 'callback' is a function that is called back with 2 args: 
+// channel name/pattern and message payload.
+//
+// Note: You are not permitted to do anything but subscribe to 
+// additional channels or unsubscribe from subscribed channels 
+// when there are >= 1 subscriptions active.  Should you need to
+// issue other commands, use a second client instance.
+
+Client.prototype.subscribeTo = function (nameOrPattern, callback) {
+    if (typeof this.channelCallbacks[nameOrPattern]  === 'function')
+        return;
+
+    if (typeof(callback) !== 'function')
+        throw new Error("requires a callback function");
+
+    this.channelCallbacks[nameOrPattern] = callback;
+
+    var method = nameOrPattern.match(/[\*\?\[]/) 
+               ? "psubscribe" 
+               : "subscribe";
+
+    this[method](nameOrPattern);
+};
+
+Client.prototype.unsubscribeFrom = function (nameOrPattern) {
+    if (typeof this.channelCallbacks[nameOrPattern] === 'undefined') 
+        return;
+
+    delete this.channelCallbacks[nameOrPattern];
+
+    var method = nameOrPattern.match(/[\*\?\[]/) 
+               ? "punsubscribe" 
+               : "unsubscribe";
+
+    this[method](nameOrPattern);
+};
+
+// Multi-bulk replies return an array of other replies.  Perhaps all you care
+// about is the representation of such buffers as UTF-8 encoded strings? Use
+// this to convert each such Buffer to a (UTF-8 encoded) String in-place.
+
+exports.convertMultiBulkBuffersToUTF8Strings = function (o) {
+    if (o instanceof Array) {
+        for (var i=0; i<o.length; ++i) 
+            if (o[i] instanceof Buffer) 
+                o[i] = o[i].utf8Slice(0, o[i].length);
+    } else if (o instanceof Object) {
+        var props = Object.getOwnPropertyNames(o);
+        for (var i=0; i<props.length; ++i) 
+            if (o[props[i]] instanceof Buffer) 
+                o[props[i]] = o[props[i]].utf8Slice(0, o[props[i]].length);
+    }
+};
+

File deps/redis-node-client/redisclient.js

-// Redis client for Node.js
-// Author: Brian Hammond <brian at fictorial dot com>
-// Copyright (C) 2009 Fictorial LLC
-// License: MIT
-
-var sys = require("sys"), 
-    tcp = require("tcp");
-
-var crlf = "\r\n", 
-    crlf_len = 2;
-
-
-var inline_commands = { 
-  auth:1, bgsave:1, dbsize:1, decr:1, decrby:1, del:1,
-  exists:1, expire:1, flushall:1, flushdb:1, get:1, incr:1, incrby:1, info:1,
-  keys:1, lastsave:1, lindex:1, llen:1, lpop:1, lrange:1, ltrim:1, mget:1,
-  move:1, randomkey:1, rename:1, renamenx:1, rpop:1, save:1, scard:1, sdiff:1,
-  sdiffstore:1, select:1, shutdown:1, sinter:1, sinterstore:1, smembers:1,
-  spop:1, srandmember:1, sunion:1, sunionstore:1, ttl:1, type:1, 
-  zrange:1, zrevrange:1, zcard:1, zrangebyscore:1
-};
-
-var bulk_commands = { 
-  getset:1, lpush:1, lrem:1, lset:1, rpush:1, sadd:1, set:1,
-  setnx:1, sismember:1, smove:1, srem:1, zadd:1, zrem:1, zscore:1,
-  rpoplpush:1
-};
-
-var multi_bulk_commands = {
-  mset:1, msetnx:1
-};
-
-var Client = exports.Client = function (port, host) {
-  process.EventEmitter.call(this);
-
-  this.host = host || '127.0.0.1';
-  this.port = port || 6379;
-  this.callbacks = [];
-  this.conn = null;
-};
-
-// The client emits "connect" when a connection is established, and emits
-// "close" when a connection is closed (passing boolean true if failed in
-// error).
-//
-// Note that calling a Redis client method when the connection is closed will
-// automatically attempt to reconnect to Redis first.
-
-sys.inherits(Client, process.EventEmitter);
-
-// Callback a function after we've ensured we're connected to Redis.
-
-Client.prototype.connect = function (callback) {
-  if (!this.conn) {
-    this.conn = new process.tcp.Connection();
-  }
-  if (this.conn.readyState === "open" && typeof(callback) === 'function') {
-    callback();
-  } else {
-    var self = this;
-
-    this.conn.addListener("connect", function () {
-      this.setEncoding("binary");
-      this.setTimeout(0);          // try to stay connected.
-      this.setNoDelay();
-      self.emit("connect");
-      if (typeof(callback) === 'function')
-        callback();
-    }); 
-
-    this.conn.addListener("data", function (data) {
-      if (!self.buffer)
-        self.buffer = "";
-      self.buffer += data;
-      self.handle_replies();
-    });
-
-    this.conn.addListener("end", function () {
-      if (self.conn && self.conn.readyState) {
-        self.conn.close();
-        self.conn = null;
-      }
-    });
-
-    this.conn.addListener("close", function (encountered_error) {
-      self.conn = null;
-      self.emit("close", encountered_error);
-    });
-
-    this.conn.connect(this.port, this.host);
-  }
-};
-
-Client.prototype.close = function () {
-  if (this.conn && this.conn.readyState === "open") {
-    this.conn.close();
-    this.conn = null;
-  }
-};
-
-// Reply handlers read replies from the current reply buffer.  At the time of
-// the call the buffer will start with at least the prefix associated with the
-// relevant reply type which is at this time always of length 1.  
-//
-// Note the buffer may not contain a full reply in which case these reply
-// handlers return null.  In this case the buffer is left intact for future
-// "receive" events to append onto, and the read-replies process repeats.
-// Repeat ad infinitum.  
-//
-// Each handler returns [ value, next_command_index ] on success, null on
-// underflow.
-
-var prefix_len = 1;
-
-// Bulk replies resemble:
-// $6\r\nFOOBAR\r\n
-
-Client.prototype.handle_bulk_reply = function (start_at, buf) {
-  var buffer = buf || this.buffer;
-  start_at = (start_at || 0) + prefix_len;
-  var crlf_at = buffer.indexOf(crlf, start_at);
-  if (crlf_at === -1) 
-    return null;
-  var value_len_str = buffer.substring(start_at, crlf_at);
-  var value_len = parseInt(value_len_str, 10);
-  if (value_len === NaN) 
-    throw new Error("invalid bulk value len: " + value_len_str);
-  if (value_len === -1)                 // value doesn't exist
-    return [ null, crlf_at + crlf_len ];  
-  var value_at = crlf_at + crlf_len;
-  var next_reply_at = value_at + value_len + crlf_len;
-  if (next_reply_at > buffer.length)
-    return null;
-  var value = buffer.substr(value_at, value_len);
-  return [ value, next_reply_at ];
-}
-
-// Mult-bulk replies resemble:
-// *4\r\n$3\r\nFOO\r\n$3\r\nBAR\r\n$5\r\nHELLO\r\n$5\r\nWORLD\r\n
-// *4 is the number of bulk replies to follow.
-
-Client.prototype.handle_multi_bulk_reply = function (buf) {
-  var buffer = buf || this.buffer;
-  var crlf_at = buffer.indexOf(crlf, prefix_len);
-  if (crlf_at === -1) 
-    return null;
-  var count_str = buffer.substring(prefix_len, crlf_at);
-  var count = parseInt(count_str, 10);
-  if (count === NaN) 
-    throw new Error("invalid multi-bulk count: " + count_str);
-  var next_reply_at = crlf_at + crlf_len;
-  if (count === -1)                   // value doesn't exist
-    return [ null, next_reply_at ];  
-  if (count === 0)
-    return [ [], next_reply_at ];
-  if (next_reply_at >= buffer.length) 
-    return null;
-  var results = [];
-  for (var i = 0; i < count; ++i) {
-    var bulk_reply = this.handle_bulk_reply(next_reply_at, buffer);
-    if (bulk_reply === null)             // no full multi-bulk cmd
-      return null;
-    var bulk_reply_value = bulk_reply[0];
-    results.push(bulk_reply_value);
-    next_reply_at = bulk_reply[1];
-  }
-  return [ results, next_reply_at ];
-};
-
-// Single line replies resemble:
-// +OK\r\n
-
-Client.prototype.handle_single_line_reply = function (buf) {
-  var buffer = buf || this.buffer;
-  var crlf_at = buffer.indexOf(crlf, prefix_len);
-  if (crlf_at === -1) 
-    return null;
-  var value = buffer.substring(prefix_len, crlf_at);
-  if (value === 'OK') 
-    value = true;
-  var next_reply_at = crlf_at + crlf_len;
-  return [ value, next_reply_at ];
-};
-
-// Integer replies resemble:
-// :1000\r\n
-
-Client.prototype.handle_integer_reply = function (buf) {
-  var buffer = buf || this.buffer;
-  var crlf_at = buffer.indexOf(crlf, prefix_len);
-  if (crlf_at === -1) 
-    return null;
-  var value_str = buffer.substring(prefix_len, crlf_at);
-  var value = parseInt(value_str, 10);
-  if (value === NaN) 
-    throw new Error("invalid integer reply: " + value_str);
-  var next_reply_at = crlf_at + crlf_len;
-  return [ value, next_reply_at ];
-};
-
-// Error replies resemble:
-// -ERR you suck at tennis\r\n
-
-Client.prototype.handle_error_reply = function (buf) {
-  var buffer = buf || this.buffer;
-  var crlf_at = buffer.indexOf(crlf, prefix_len);
-  if (crlf_at === -1) 
-    return null;
-  var value = buffer.substring(prefix_len, crlf_at);
-  var next_reply_at = crlf_at + crlf_len;
-  if (value.indexOf("ERR ") === 0)
-    value = value.substr("ERR ".length);
-  return [ value, next_reply_at ];
-}
-
-// Try to read as many replies from the current buffer as we can.  Leave
-// partial replies in the buffer, else eat 'em.  Dispatch any promises waiting
-// for these replies.  Error replies emit error on the promise, else success is
-// emitted.
-
-Client.prototype.handle_replies = function () {
-  while (this.buffer.length > 0) {
-    if (GLOBAL.DEBUG) {
-      write_debug('---');
-      write_debug('buffer: ' + this.buffer);
-    }
-    var prefix = this.buffer.charAt(0);
-    var result, is_error = false;
-    switch (prefix) {
-      case '$': result = this.handle_bulk_reply();                   break;
-      case '*': result = this.handle_multi_bulk_reply();             break;
-      case '+': result = this.handle_single_line_reply();            break;
-      case ':': result = this.handle_integer_reply();                break;
-      case '-': result = this.handle_error_reply(); is_error = true; break;
-    }
-    // The handlers return null when there's not enough data
-    // in the buffer to read a full reply.  Leave the buffer alone until
-    // we receive more data.
-    if (result === null) 
-      break;
-    if (GLOBAL.DEBUG) {
-      write_debug('prefix: ' + prefix);
-      write_debug('result: ' + JSON.stringify(result));
-    }
-    var next_reply_at = result[1];
-    this.buffer = this.buffer.substring(next_reply_at);
-    var callback = this.callbacks.shift();
-    var result_value = result[0];
-    if( callback.func ) {
-      if (is_error) 
-        callback.func(true, result_value);
-      else {
-        result_value = post_process_results(callback.command, result_value);
-        callback.func(false, result_value);
-      }
-    }
-  }
-};
-
-function write_debug(data) {
-  if (!GLOBAL.DEBUG || !data) return;
-  sys.puts(data.replace(/\r\n/g, '<CRLF>'));
-}
-
-function try_convert_to_number(str) {
-  var value = parseInt(str, 10);
-  if (value === NaN) 
-    value = parseFloat(str);
-  if (value === NaN) 
-    return str;
-  return value;
-}
-
-function format_inline(name, args) {
-  var command = name;
-  for (var arg in args) 
-    command += ' ' + args[arg].toString();
-  return command + crlf;
-}
-
-function format_bulk_command(name, args) {
-  var output = name;
-  for (var i = 0; i < args.length - 1; ++i) 
-    output += ' ' + args[i].toString();
-  var last_arg = args[args.length - 1].toString();
-  return output + ' ' + last_arg.length + crlf + last_arg + crlf;
-}
-
-function format_multi_bulk_command(name, args) {
-  var output = '*' + (args.length + 1) + crlf + '$' + name.length + crlf + name + crlf;
-  for (var i = 0; i < args.length; ++i) {
-    var arg_as_str = args[i].toString();
-    output += '$' + arg_as_str.length + crlf + arg_as_str + crlf;
-  }
-  return output;
-}
-
-function make_command_sender(name) {
-  Client.prototype[name] = function () {
-    if (GLOBAL.DEBUG) {
-      var description = "client." + name + "( ";
-      for (var a in arguments) 
-        description += "'" + arguments[a] + "',";
-      description = description.substr(0, description.length - 1) + " )";
-    }
-    var actual_callback = null;
-    var args = arguments;
-    if( typeof( arguments[arguments.length-1] ) === "function" ) {
-      actual_callback = arguments[arguments.length-1];
-      [].pop.call(args);
-    }
-    else {
-        sys.debug( name );
-        sys.debug( typeof( arguments[arguments.length-1] ));
-    }
-    var self = this;
-    this.connect(function () {
-      var command;
-      if (inline_commands[name]) 
-        command = format_inline(name, args);
-      else if (bulk_commands[name]) 
-        command = format_bulk_command(name, args);
-      else if (multi_bulk_commands[name]) 
-        command = format_multi_bulk_command(name, args);
-      else 
-        throw new Error('unknown command type for "' + name + '"');
-      if (GLOBAL.DEBUG) {
-        write_debug("---");
-        write_debug("call:   " + description);
-        write_debug("command:" + command);
-      }
-      self.callbacks.push({ command:name.toLowerCase(), func: actual_callback });
-      self.conn.write(command);
-    });
-  };
-}
-
-for (var name in inline_commands) 
-  make_command_sender(name);
-
-for (var name in bulk_commands)   
-  make_command_sender(name);
-
-for (var name in multi_bulk_commands)   
-  make_command_sender(name);
-
-function post_process_results(command, result) {
-  var new_result = result;
-  switch (command) {
-    case 'info':
-      var info = {};
-      result.split(/\r\n/g).forEach(function (line) {
-        var parts = line.split(':');
-        if (parts.length === 2)
-          info[parts[0]] = try_convert_to_number(parts[1]);
-      });
-      new_result = info;
-      break;
-    case 'keys': 
-      new_result = result.split(' '); 
-      break;
-    case 'lastsave': 
-    case 'scard':
-    case 'zcard':
-    case 'zscore':
-      new_result = try_convert_to_number(result); 
-      break;
-    default: 
-      break;
-  }
-  return new_result;
-}
-
-// Read this: http://code.google.com/p/redis/wiki/SortCommand
-// 'key' is what to sort, 'options' is how to sort.
-// 'options' is an object with optional properties:
-//   'by_pattern': 'pattern'
-//   'limit': [start, end]
-//   'get_patterns': [ 'pattern', 'pattern', ... ]
-//   'ascending': true|false
-//   'lexicographically': true|false
-//   'store_key': 'a_key_name'
-
-Client.prototype.sort = function (key, options, func) {
-  var self = this;
-  this.connect(function () {
-    var opts = [];
-    if (typeof(options) == 'object') {
-      if (options.by_pattern) 
-        opts.push('by ' + options.by_pattern);
-      if (options.get_patterns) {
-        options.get_patterns.forEach(function (pat) {
-          opts.push('get ' + pat);
-        });
-      }
-      if (!options.ascending)
-        opts.push('desc');
-      if (options.lexicographically)
-        opts.push('alpha');
-      if (options.store_key) 
-        opts.push('store ' + options.store_key);
-      if (options.limit)
-        opts.push('limit ' + options.limit[0] + ' ' + options.limit[1]);
-    } 
-    var command = 'sort ' + key + ' ' + opts.join(' ') + crlf;
-    write_debug("call:    client.sort(...)\ncommand: " + command);
-    self.callbacks.push({ command:'sort', func: func });
-    self.conn.write(command);
-  });
-}
-
-Client.prototype.quit = function () {
-  if (this.conn.readyState != "open") {
-    this.conn.close();
-  } else {
-    this.conn.write('quit' + crlf);
-    this.conn.close();
-  }
-};
-
-Client.prototype.make_master = function () {
-  var self = this;
-  this.connect(function () {
-    self.callbacks.push({ command:'slaveof' });
-    self.conn.write('slaveof no one');
-  });
-};
-
-Client.prototype.make_slave_of = function (host, port) {
-  var self = this;
-  this.connect(function () {
-    port = port || 6379;
-    var command = 'slaveof ' + host + ' ' + port;
-    self.callbacks.push({ promise:null, command:'slaveof' });
-    self.conn.write(command);
-  });
-};