├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── examples └── tour.js ├── index.js ├── lib ├── client.js ├── command.js ├── commandConfig.js ├── commandList.js ├── perf.js ├── redis.js ├── replies │ ├── bulkReply.js │ ├── errorReply.js │ ├── inlineReply.js │ ├── integerReply.js │ └── multibulkReply.js └── reply.js ├── package.json └── test ├── general_commands.vows.js ├── hash_commands.vows.js ├── list_commands.vows.js ├── pubsub.vows.js ├── sample.png ├── set_commands.vows.js ├── sort_command.vows.js ├── string_commands.vows.js ├── transactions.vows.js ├── utils.js ├── vows.js └── zset_commands.vows.js /.gitignore: -------------------------------------------------------------------------------- 1 | **.swp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Brian Noguchi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | VOWS = vows --spec 3 | 4 | TESTS = test/*.vows.js 5 | 6 | test: 7 | @$(VOWS) $(TESTS) 8 | 9 | .PHONY: test 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## redis-node - Complete Redis Client for Node.js 2 | --- 3 | 4 | Blog post coming. 5 | 6 | ### Features include: 7 | - FAST!!!! (See [benchmarks](http://github.com/bnoguchi/redis-node/benchmarks/bench.js)) 8 | - A comprehensive test suite. 9 | - Fully asynchronous. 10 | - Support for all Redis commands. 11 | - PUBLISH and SUBSCRIBE support. 12 | - Full transactional support (including nested transactions) (i.e., MULTI/EXEC/DISCARD) (to my knowledge, not present in [redis-node-client](http://github.com/fictorial/redis-node-client)). 13 | - Idiomatic command syntax. 14 | - Automatic re-establishment of connections to the Redis server. 15 | 16 | ### Installation 17 | npm install redis-node 18 | 19 | ### A Quick Tour 20 | ```javascript 21 | // See ./examples/tour.js 22 | var sys = require("sys"); 23 | var redis = require("redis-node"); 24 | var client = redis.createClient(); // Create the client 25 | client.select(2); // Select database 2 26 | 27 | // Assign the string "world" to the "hello" key. 28 | // You can provide a callback to handle the response from Redis 29 | // that gets asynchronously run upon seeing the response. 30 | client.set("hello", "world", function (err, status) { 31 | if (err) throw err; 32 | console.log(status); // true 33 | }); 34 | 35 | // ... but you don't have to provide a callback. 36 | client.set("hello", "world"); 37 | 38 | // We may or may not be connected yet, but that's ok, since 39 | // the client queues up any commands. 40 | 41 | // The commands are also idiomatic 42 | client.hmset("hash", { t: "rex", steg: "asaurus" }, function (err, status) { 43 | if (err) throw err; 44 | sys.log(status); // true 45 | }); 46 | 47 | // Support for transactions 48 | console.log("Transfer from checking to savings."); 49 | client.transaction( function () { 50 | client.decrby("checking", 100, function (err, balance) { 51 | if (err) throw err; 52 | console.log("Checking Balance: " + balance); 53 | }); 54 | client.incrby("savings", 100, function (err, balance) { 55 | if (err) throw err; 56 | console.log("Savings Balance: " + balance); 57 | }); 58 | }); 59 | 60 | // With automatic transaction discard if there are any syntactic errors 61 | client.transaction( function () { 62 | client.set("I'm missing a 2nd argument"); // Our client with automatically discard the transaction 63 | }); 64 | 65 | // Close the connection 66 | setTimeout( function () { 67 | client.close(); 68 | }, 1000); 69 | ``` 70 | 71 | See test/ for examples of each command. 72 | 73 | # API Reference 74 | ## redis.createClient(port, host, options) 75 | Creates a new Client instance connected to the Redis server running on host:port. 76 | `host` defaults to `127.0.0.1` 77 | `port` defaults to `6379` 78 | You can pass in an options hash, too. They default to: 79 | - `maxReconnectionAttempts` (10) The number of times to try reconnecting to the Redis server before giving up. 80 | - `reconnectionDelay` (500) How many milliseconds to wait before the 1st reconnection attempt. Using the strategy of exponential backoff, the delay doubles with every re-connection attempt. 81 | 82 | ## Events 83 | The redis-node client emits important events related to the connection with the Redis 84 | server. You can bind an event handler to any of the following events: 85 | 86 | - `connected` 87 | 88 | Emitted when the client successfully makes a connection FOR THE FIRST TIME. 89 | You usually will not need to bind an event handler to `connected` because the 90 | client takes care of queueing up any commands you send it and flushes them 91 | to the Redis server once it is connected. 92 | 93 | - `disconnected` 94 | 95 | Emitted when we drop a connection with the Redis server. This can happen if the 96 | connection times out due to no recent activity from the client. 97 | 98 | - `reconnecting` 99 | 100 | Emitted just before the client attempts to reconnect to the Redis server. 101 | 102 | - `reconnected` 103 | 104 | Emitted when the client successfully makes a successful reconnection. 105 | 106 | - `noconnection` 107 | 108 | Emitted when the client gives up its connection attempts. 109 | 110 | - `connection error` 111 | 112 | Emitted when the there is an error that is a result of the connection with the 113 | Redis server. The error object is passed to `callback`. If you do not register 114 | a listener with this event, then the error is thrown and the program exits. 115 | 116 | ## Commands operating on all value types 117 | 118 | ### client.exists(key, callback) 119 | Test if a key exists. 120 | Passes `true` to callback if it exists. 121 | Passes `false` to callback if it does not. 122 | ```javascript 123 | client.exists("some-key", function (err, doesExist) { 124 | console.log(doesExist); 125 | }); 126 | ``` 127 | 128 | ### client.del(key1, key2, ..., keyn, callback) 129 | Delete a key. 130 | Passes the number of keys that were deleted to `callback`. 131 | ```javascript 132 | client.del("key1", "key2", function (err, numRemoved) { 133 | console.log(numRemoved); // 2 134 | }); 135 | ``` 136 | 137 | ### client.type(key, callback) 138 | Passes the type of value stored at key to `callback`. One of: 139 | - `none` if the key does not exist 140 | - `string` if the key contains a `String` value 141 | - `list` if the key contains a `List` value 142 | - `set` if the key contains a `Set` value 143 | - `zset` if the key contains a `Sorted Set` value 144 | - `hash` if the key contains a `Hash` value 145 | 146 | ```javascript 147 | client.type("key-with-string", function (err, type) { 148 | console.log(type); // Either: 'none', 'string', 'list', 'set', 'zset', or 'hash' 149 | }); 150 | ``` 151 | 152 | ### client.keys(pattern, callback) 153 | Passes all the keys matching a given pattern to `callback`. 154 | ```javascript 155 | // The * pattern returns an array of all keys 156 | client.keys("*", function (err, arrayOfKeys) { 157 | arrayOfKeys.forEach( function (key) { 158 | console.log(key); 159 | }); 160 | }); 161 | 162 | // .* patterns 163 | client.keys("key*", function (err, arrayOfKeys) { 164 | arrayOfKeys.forEach( function (key) { 165 | console.log(key); 166 | }); 167 | }); 168 | 169 | // ? patterns 170 | client.keys("?ar", function (err, arrayOfKeys) { 171 | arrayOfKeys.forEach( function (key) { 172 | console.log(key); // e.g., 'car', 'bar', 'far' 173 | }); 174 | }); 175 | ``` 176 | ### client.randomkey(callback) 177 | Passes a random key from the key space to `callback`. 178 | ```javascript 179 | client.randomkey( function (err, key) { 180 | console.log(key); 181 | }); 182 | ``` 183 | ### client.rename(oldName, newName, callback) 184 | Renames the old key name `oldName` to the new key name `newName` 185 | Passes `true` to `callback`. 186 | ```javascript 187 | client.rename("old", "new", function (err, didSucceed) { 188 | console.log(didSucceed); // true 189 | }); 190 | ``` 191 | ### client.renamenx(oldName, newName, callback) 192 | Renames the old key name `oldName` to the new key name `newName`, 193 | if the `newName` key does not already exist. 194 | Passes `1` if `newName` key did not already exist, to `callback`. 195 | Passes `0` if `newName` key did already exist, to `callback`. 196 | ```javascript 197 | client.renamenx("old", "new", function (err, didSucceed) { 198 | console.log(!!didSucceed); // true 199 | }); 200 | ``` 201 | ### client.dbsize(callback) 202 | Passes the number of keys in the current db. 203 | ```javascript 204 | client.dbsize( function (err, numKeys) { 205 | console.log(numKeys); 206 | }); 207 | ``` 208 | ### client.expire(key, ttl, callback) 209 | Tells Redis to delete the `key` after `ttl` seconds. 210 | If we are using Redis < 2.1.3 and if a `ttl` was already set with 211 | another prior `client.expire` invocation, then the new `ttl` does 212 | NOT override the old `ttl`. 213 | If we are using Redis >= 2.1.3 and if a `ttl` was already set with 214 | another prior `client.expire` invocation, then the new `ttl` DOES 215 | override the old `ttl`. 216 | The expiry can be removed from the key if the key is set to a new value using 217 | the `client.set(key, value)` command or when a key is destroyed via the 218 | `client.del(key)` command. 219 | Passes `1` to `callback` if `key` has no current `ttl` expiry. 220 | Passes `0` to `callback` if `key` does not exist or if we 221 | are using Redis < 2.1.3, and `key` already has a current `ttl` expiry. 222 | ```javascript 223 | client.expire("key", 2, function (err, didSetExpiry) { 224 | console.log(!!didSetExpiry); 225 | }); 226 | ``` 227 | ### client.expireat(key, unixtime, callback) 228 | Tells Redis to delete the `key` at the `unixtime` datetime in the future. 229 | Works similarly to `client.expire(key, ttl, callback)` 230 | ```javascript 231 | client.expireat("key", parseInt((+new Date) / 1000, 10) + 2, function (err, didSetExpiry) { 232 | console.log(didSetExpiry); 233 | }); 234 | ``` 235 | ### client.ttl(key, callback) 236 | Gets the time to live (i.e., how many seconds before `key` expires) in seconds 237 | of `key`. 238 | Passes the number of seconds before `key` expires to `callback`. 239 | Passes `-1` to `callback` if `key` has no ttl expiry. 240 | ```javascript 241 | client.ttl("key", function (err, ttl) { 242 | console.log(ttl); 243 | }); 244 | ``` 245 | ### client.select(dbIndex, callback) 246 | Selects the DB with the specified `dbIndex`. 247 | Passes `true` to `callback`. 248 | ```javascript 249 | client.select(2, function (err, didSucceed) { 250 | console.log(didSucceed); // true 251 | }); 252 | ``` 253 | ### client.move(key, dbIndex, callback) 254 | Moves `key` from the currently selected DB to the `dbIndex` DB. 255 | You can use `client.move` as a locking primitive. 256 | Passes `1` to `callback` if `key` was moved successfully. 257 | Passes `0` if the target `key` was already there or if the source `key` 258 | was not found at all. 259 | ```javascript 260 | client.move("key", 3, function (err, didSucceed) { 261 | console.log(!!didSucceed); 262 | }); 263 | ``` 264 | ### client.flushdb(callback) 265 | Deletes all the keys of the currently selected DB. The command never fails. 266 | Passes `true` to `callback`. 267 | ```javascript 268 | client.flushdb( function (err, didSucceed) { 269 | console.log(didSucceed); // true 270 | }); 271 | ``` 272 | ### client.flushall(callback) 273 | Deletes all the keys of all the existing databases, not just the currently 274 | selected one. This command never fails. 275 | Passes `true` to `callback`. 276 | ```javascript 277 | client.flushall( function (didSucceed) { 278 | console.log(didSucceed); // true 279 | }); 280 | ``` 281 | ## Commands operating on all value types 282 | 283 | ### client.set(key, value, callback) 284 | Sets `key` to `value`. `value` can be a String, Number, or Buffer. 285 | Passes `true` to `callback`. 286 | ```javascript 287 | client.set("key", "value", function (err, didSet) { 288 | console.log(didSet); // true 289 | }); 290 | ``` 291 | ### client.get(key, callback) 292 | Passes the Buffer value at `key` to callback if the key exists. 293 | Passes null to `callback` if `key` does not exist. 294 | 295 | ## MULTI/EXEC (aka transactions) 296 | 297 | ### client.transaction(transactionBlock) 298 | Sends commands inside the function `transactionBlock` as a transaction. Behind the scenes, we precede the commands inside `transactionBlock` with a MULTI command and commit the commands with an EXEC command. If there is a syntax error with any of the commands sent in the transaction, EXEC will never be called; instead a DISCARD command will be sent to the Redis server to roll back the transaction. 299 | ```javascript 300 | client.transaction( function () { 301 | client.rpush("txn", 1); 302 | client.rpush("txn", 2); 303 | client.rpush("txn", 3, function (err, count) { 304 | console.log(count); // 3 305 | }); 306 | }); 307 | ``` 308 | # Test Coverage 309 | See [./test/](https://github.com/bnoguchi/redis-node) for the list of tests. 310 | Currently, the tests are implemented via the [Vows](https://github.com/cloudhead/vows). 311 | However, the tests will only work with my fork of vows, so install my branch to see the tests pass: 312 | git clone git://github.com/bnoguchi/vows.git 313 | git checkout teardownFix 314 | npm install 315 | Then, to run the tests from the command line. 316 | make test 317 | You can also run the tests directly with the vows binary from the command line. 318 | vows test/*.vows.js 319 | 320 | # Coming Sooner or Later 321 | - A distributed API for interacting with a Redis cluster. 322 | - UDP Support 323 | 324 | # Contributors 325 | - [Brian Noguchi](http://github.com/bnoguchi) 326 | - [Tim Smart](http://github.com/Tim-Smart) 327 | - [Graeme Worthy](http://github.com/graemeworthy) 328 | 329 | # Other Redis Clients for Node.js 330 | - [redis-node-client](http://github.com/fictorial/redis-node-client) 331 | - [node_redis](http://github.com/mranney/node_redis) 332 | 333 | ### 3rd Party Libraries 334 | - [Vows Testing Framework](http://github.com/cloudhead/vows) 335 | 336 | ### License 337 | MIT License 338 | 339 | --- 340 | ### Author 341 | Brian Noguchi 342 | -------------------------------------------------------------------------------- /examples/tour.js: -------------------------------------------------------------------------------- 1 | // Run using 2 | // node examples/tour.js 3 | var sys = require("sys"); 4 | var redis = require("redis-node"); 5 | var client = redis.createClient(); // Create the client 6 | client.select(2); // Select database 2 7 | 8 | // Assign the string "world" to the "hello" key. 9 | // You can provide a callback to handle the response from Redis 10 | // that gets asynchronously run upon seeing the response. 11 | client.set("hello", "world", function (err, status) { 12 | if (err) throw err; 13 | console.log(status); // true 14 | }); 15 | 16 | // ... but you don't have to provide a callback. 17 | client.set("hello", "world"); 18 | 19 | // We may or may not be connected yet, but that's ok, since 20 | // the client queues up any commands. 21 | 22 | // The commands are also idiomatic 23 | client.hmset("hash", { t: "rex", steg: "asaurus" }, function (err, status) { 24 | if (err) throw err; 25 | sys.log(status); // true 26 | }); 27 | 28 | // Support for transactions 29 | console.log("Transfer from checking to savings."); 30 | client.transaction( function () { 31 | client.decrby("checking", 100, function (err, balance) { 32 | if (err) throw err; 33 | console.log("Checking Balance: " + balance); 34 | }); 35 | client.incrby("savings", 100, function (err, balance) { 36 | if (err) throw err; 37 | console.log("Savings Balance: " + balance); 38 | }); 39 | }); 40 | 41 | // With automatic transaction discard if there are any syntactic errors 42 | client.transaction( function () { 43 | client.set("I'm missing a 2nd argument"); // Our client with automatically discard the transaction 44 | }); 45 | 46 | // Close the connection 47 | setTimeout( function () { 48 | client.close(); 49 | }, 1000); 50 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/redis"); 2 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var commandConfig = require("./commandConfig"), 26 | Command = require("./command").Command, 27 | Reply = require("./reply").Reply, 28 | Buffer = require("buffer").Buffer, 29 | CRLF = "\r\n"; 30 | 31 | var net = require("net"), 32 | util = require("util"), 33 | EventEmitter = require("events").EventEmitter, 34 | ErrorReply = require("./replies/errorReply").ErrorReply; 35 | 36 | exports.COMMAND_ORPHANED_ERROR = "connection lost before reply received"; 37 | exports.NO_CONNECTION_ERROR = "failed to establish a connection to Redis"; 38 | 39 | var toArray = function (args) { 40 | var i = 0, 41 | len = args.length, 42 | arr = new Array(len); 43 | for ( ; i < len; i++) { 44 | arr[i] = args[i]; 45 | } 46 | return arr; 47 | }; 48 | 49 | // Array.prototype.shift is slow, so we use Tim's Queue data structure instead 50 | // that has a faster shift(). 51 | // 52 | // Queue class adapted from Tim Caswell's pattern library 53 | // http://github.com/creationix/pattern/blob/master/lib/pattern/queue.js 54 | var Queue = function () { 55 | this.tail = []; 56 | this.head = toArray(arguments); 57 | this.offset = 0; 58 | }; 59 | 60 | Queue.prototype.peek = function () { 61 | return this.head[this.offset] || this.tail[0]; 62 | if (this.offset === this.head.length) { 63 | var tmp = this.head; 64 | tmp.length = 0; 65 | this.head = this.tail; 66 | this.tail = tmp; 67 | this.offset = 0; 68 | if (this.head.length === 0) return; 69 | } 70 | return this.head[this.offset]; 71 | }; 72 | 73 | Queue.prototype.shift = function () { 74 | if (this.offset === this.head.length) { 75 | var tmp = this.head; 76 | tmp.length = 0; 77 | this.head = this.tail; 78 | this.tail = tmp; 79 | this.offset = 0; 80 | if (this.head.length === 0) return; 81 | } 82 | return this.head[this.offset++]; 83 | } 84 | 85 | Queue.prototype.push = function (item) { 86 | return this.tail.push(item); 87 | }; 88 | 89 | Object.defineProperty(Queue.prototype, 'length', { 90 | get: function () { 91 | return this.head.length - this.offset + this.tail.length; 92 | } 93 | }); 94 | 95 | /** 96 | * Emits the following events: 97 | * -connected: when connected 98 | * -reconnected: upon a reconnection 99 | * -reconnecting: when about to try reconnecting 100 | * -noconnection: when a connection or reconnection fails 101 | * @options is a hash with the following optional keys: 102 | * -maxReconnectionAttempts 103 | * -utf8 - defaults to `true`, which automatically converts utf8 to Buffer, 104 | * so you don't have to do so explicitly. When set to `false`, 105 | * performance is higher, but you have to explicitly detect and 106 | * convert utf8 strings to Buffer. 107 | * e.g., 108 | * if (isUtf8(string)) string = new Buffer(string); 109 | */ 110 | var Client = exports.Client = function Client (port, host, options) { 111 | // Inherit from EventEmitter 112 | EventEmitter.call(this); 113 | 114 | // Defaults 115 | this.utf8 = true; 116 | // Configure according to options 117 | options = options || {}; 118 | [this.DEFAULT_OPTIONS, options].forEach( function (obj) { 119 | for (var key in obj) if (obj.hasOwnProperty(key)) { 120 | this[key] = obj[key]; 121 | } 122 | }); 123 | 124 | // The buffer that stores the reply values when parsing the incoming data stream 125 | // from Redis. 126 | this.replyBuffer = new Buffer(512); 127 | 128 | // Stores all commands whose responses haven't been sent to a callback 129 | this.commandHistory = new Queue(); 130 | this.channelCallbacks = {}; 131 | 132 | // State specifying if we're in the middle of a transaction or not. 133 | this.isSendingTxnCmds = false; 134 | this.isListeningForTxnAcks = false; 135 | this.currTxnCommands = []; // TODO Change [] to new Queue() 136 | this.cmdsToRunAfterTxn = []; 137 | 138 | // For storing queued commands that build up when there isn't a connection 139 | // or if we shouldn't send yet because we're in the middle of a transaction. 140 | this.queuedCommandHistory = new Queue(); 141 | // this.queuedCommandBuffers = []; 142 | 143 | this.connectionsMade = 0; 144 | 145 | // Setup the TCP connection 146 | var stream = this.stream = net.createConnection(this.port = port, this.host = host); 147 | 148 | stream.on("data", this.handleData.bind(this)); 149 | // currReply = null; 150 | // stream.on("data", function (data) { 151 | // // A partial reply has to outlive data, so it can parse the next incoming data 152 | // var atDataIndex = 0, 153 | // dataLen = data.length; 154 | // 155 | // while (atDataIndex < dataLen) { 156 | // if (!currReply) { 157 | // var typeCode = data[atDataIndex++]; 158 | // currReply = Reply.fromTypeCode(typeCode, client); 159 | // continue; 160 | // } 161 | // atDataIndex = currReply.parse(data, atDataIndex); 162 | // if (currReply.isComplete) { 163 | // client.emit("reply", currReply); 164 | // currReply = null; 165 | // } 166 | // } 167 | // }); 168 | this.on("reply", this.handleReply.bind(this)); 169 | 170 | // var replyStream = new ReplyStream(stream, this); 171 | // replyStream.on("reply", this.handleReply.bind(this)); 172 | 173 | var client = this; // For closures 174 | stream.on("connect", function () { 175 | var eventName = client.connectionsMade === 0 176 | ? "connected" 177 | : "reconnected"; 178 | 179 | stream.setNoDelay(); 180 | stream.setTimeout(0); 181 | 182 | client.reconnectionAttempts = 0; // Reset to 0 183 | client.reconnectionDelay = 500; 184 | if (client.reconnectionTimer) { 185 | clearTimeout(client.reconnectionTimer); 186 | client.reconnectionTimer = null; 187 | } 188 | 189 | client.connectionsMade++; 190 | client.expectingClose = false; 191 | 192 | if (client.connectionsMade > 1 && client.commandHistory.length > 0) { 193 | util.log("[RECONNECTION] some commands orphaned (" + client.commandHistory.length + "). notifying..."); 194 | client.callbackOrphanedCommandsWithError(); 195 | } 196 | if (client.currDB) client.select(client.currDB); 197 | 198 | client.flushQueuedCommands(); 199 | client.emit(eventName); 200 | }); 201 | 202 | stream.on("error", function (e) { 203 | client.emit("connection error", e); 204 | if (client.listeners("connection error").length === 0) { 205 | throw e; 206 | } 207 | }); 208 | 209 | stream.on("end", function () { 210 | stream.end(); 211 | }); 212 | 213 | stream.on("close", function () { 214 | client.emit("disconnected"); 215 | // Don't reconnect on first connection failure 216 | // to avoid un-necessary work 217 | if (client.connectionsMade === 0) { 218 | // client.callbackOrphanedCommandsWithError(); // TODO 219 | // client.callbackQueuedCommandsWithError(); // TODO 220 | client.giveupConnectionAttempts(); 221 | } else { 222 | client.attemptReconnect(); 223 | } 224 | }); 225 | }; 226 | util.inherits(Client, EventEmitter); 227 | 228 | Client.prototype.DEFAULT_OPTIONS = { 229 | maxReconnectionAttempts: 10, 230 | reconnectionAttempts: 0, 231 | reconnectionDelay: 500, // Doubles with every try 232 | reconnectionTimer: null 233 | }; 234 | 235 | Client.prototype.handleData = function (data) { 236 | // A partial reply has to outlive data, so it can parse the next incoming data 237 | var atDataIndex = 0, 238 | dataLen = data.length, 239 | currReply = this.currReply; 240 | 241 | while (atDataIndex < dataLen) { 242 | currReply = this.currReply; 243 | if (!currReply) { 244 | currReply = this.currReply = Reply.fromTypeCode(data[atDataIndex++], this); 245 | continue; 246 | } 247 | atDataIndex = currReply.parse(data, atDataIndex); 248 | if (currReply.isComplete) { 249 | this.emit("reply", currReply); 250 | this.currReply = null; 251 | } 252 | } 253 | }; 254 | 255 | Client.prototype.handleReply = function (reply, isParsingExecReply) { 256 | // util.log(util.inspect(reply)); // Uncomment this to see the reply 257 | /* Handle special case of PubSub */ 258 | var pubSubCallback, replyValue; 259 | if (reply.isMessage || reply.isPMessage) { 260 | replyValue = reply.replyValue; 261 | pubSubCallback = this.channelCallbacks[replyValue.channelPattern || replyValue.channelName]; 262 | pubSubCallback(replyValue.channelName, replyValue.message, replyValue.channelPattern) 263 | return; 264 | } 265 | 266 | // Now handle all other replies 267 | 268 | // 1. Find the command name corresponding to the reply 269 | // 2. Find or define a callback (needed for ALL reply types) 270 | var commandForReply, txnCommand, commandName, commandCallback; 271 | if (isParsingExecReply) { 272 | txnCommand = this.currTxnCommands.shift(); 273 | commandName = txnCommand.commandName; 274 | commandCallback = txnCommand.callback; 275 | replyValue = reply; // reply is just an element in an array representing a multibulk reply 276 | } else { 277 | commandForReply = this.commandHistory.shift(); 278 | commandName = commandForReply.commandName; 279 | commandCallback = commandForReply.commandCallback || Command.prototype.commandCallback; 280 | replyValue = reply.replyValue; 281 | } 282 | 283 | // util.log(util.inspect(commandForReply)); // Uncomment this to see which command corresponds to this 284 | 285 | /* Handle Errors */ 286 | if (reply instanceof ErrorReply) { 287 | commandCallback(new Error(reply.replyValue), null); 288 | return; 289 | } 290 | 291 | /* Handle Non-errors */ 292 | 293 | // If we switched database numbers, then save that 294 | // number in this.currDB, so that we can switch back 295 | // to the same DB on reconnection. We do this because 296 | // reconnecting to Redis as a client connects you to 297 | // database number 1 always. 298 | if (commandName === "select") { 299 | this.currDB = commandForReply.db; 300 | } 301 | 302 | // Handle specific command result typecasting 303 | if (!isParsingExecReply) { 304 | if (commandConfig[commandName]) { 305 | replyValue = commandConfig[commandName].typecastReplyValue(replyValue, commandForReply); 306 | } 307 | } 308 | 309 | commandCallback(null, replyValue); 310 | }; 311 | 312 | Client.prototype.close = function () { 313 | this.expectingClose = true; 314 | this.stream.end(); 315 | }; 316 | 317 | Client.prototype.giveupConnectionAttempts = function () { 318 | this.noConnection = true; 319 | this.emit("noconnection", this); 320 | }; 321 | 322 | Client.prototype.attemptReconnect = function () { 323 | var stream = this.stream, 324 | maxAttempts, 325 | delay, 326 | client; 327 | if (stream.writable && stream.readable) return; 328 | if (this.expectingClose) return; // TODO 329 | maxAttempts = this.maxReconnectionAttempts; 330 | if (maxAttempts === 0) return; 331 | if (this.reconnectionAttempts++ >= maxAttempts) { 332 | return this.giveupConnectionAttempts(); 333 | } 334 | 335 | delay = (this.reconnectionDelay *= 2); // Exponential backoff 336 | client = this; 337 | this.reconnectionTimer = setTimeout( function () { 338 | client.emit("reconnecting", client); 339 | stream.connect(client.port, client.host); 340 | }, delay); 341 | }; 342 | 343 | Client.prototype.flushQueuedCommands = function () { 344 | var queuedCommands = this.queuedCommandHistory, 345 | commandHistory = this.commandHistory, 346 | stream = this.stream, 347 | i = 0, len = queuedCommands.length, 348 | command; 349 | while (stream.writable && (command = queuedCommands.shift())) { 350 | // this.writeCmdToStream(command); 351 | commandHistory.push(command.toHash()); 352 | command.writeToStream(); 353 | } 354 | }; 355 | 356 | Client.prototype.callbackCommandWithError = function (command, errorMessage) { 357 | var callback = command[command.length-1]; 358 | if (typeof callback === "function") { 359 | callback(new Error(errorMessage)); 360 | } 361 | }; 362 | 363 | // TODO Remove [i] dependency 364 | Client.prototype.callbackOrphanedCommandsWithError = function () { 365 | for (var i = 0, len = this.commandHistory.length; i < len; i++) { 366 | this.callbackCommandWithError(this.commandHistory[i], exports.COMMAND_ORPHANED_ERROR); 367 | } 368 | this.commandHistory = new Queue(); 369 | }; 370 | 371 | var commands = require("./commandList"); 372 | 373 | commands.forEach( function (commandName) { 374 | Client.prototype[commandName] = function () { 375 | var len = arguments.length, 376 | args = new Array(1 + len); 377 | args[0] = commandName; 378 | for (var i = 0, len = arguments.length; i < len; i++) { 379 | args[i+1] = arguments[i]; 380 | } 381 | this.sendCommand(args); 382 | }; 383 | }); 384 | 385 | // This is called every time we receive a 'QUEUED' ACKnowledgment from Redis for 386 | // each command sent after MULTI but before EXEC 387 | Client.prototype.onTxnAck = function (err, reply) { 388 | // if (!err && reply !== "QUEUED") { 389 | // err = command.commandName + " was not queued in the transaction."; 390 | // } 391 | this.numUnackedTxnCmds--; 392 | if (err) { // If there was an error in the syntax of this command 393 | // Remove the transaction commands still ahead of me: 394 | while(this.numUnackedTxnCmds--) { 395 | this.commandHistory.shift(); 396 | } 397 | // Tell the Redis server to cancel the transaction, 398 | // so it doesn't block other clients' commands 399 | this.isListeningForTxnAcks = false; 400 | var client = this; 401 | this.discard( function (errDiscard, reply) { 402 | client.currTxnCommands.length = 0; // TODO Queue 403 | client.runPostTxnCommands(); 404 | }); 405 | // TODO How do I inform the user that the transaction was rolled back? 406 | // throw err; 407 | } else { 408 | // Only if we've sent and received acks for all commands in the transaction 409 | if (this.didRegisterAllTxnCommands && this.numUnackedTxnCmds === 0) { 410 | this.sendExecToServer(); 411 | } 412 | } 413 | }; 414 | 415 | 416 | var transactionManager = { 417 | isSendingTxnCmds: false, 418 | 419 | // true once EXEC is sent 420 | isListeningForTxnAcks: false, 421 | 422 | didRegisterAllCommands: false, 423 | 424 | numUnackedCmds: 0, 425 | 426 | beforeSendCmd: function (command) { 427 | var intendedCmdCallback = this.onTxnAck.bind(client); 428 | this.numUnackedCmds++; 429 | } 430 | }; 431 | 432 | /** 433 | * Send the command to the Redis server. 434 | * 435 | * sendCommand([commandName, arg1, arg2,..., callback]) 436 | */ 437 | Client.prototype.sendCommand = function (args) { 438 | // Intercept non-transactional commands when we are still waiting to hear 439 | // acks for transactional commands. 440 | if (args[0] !== "discard" && !this.isSendingTxnCmds && this.isListeningForTxnAcks) {// && (this.numUnackedTxnCmds > 0 || this.cmdsToRunAfterTxn.length > 0)) { 441 | this.cmdsToRunAfterTxn.push(args); 442 | return; 443 | } 444 | var command = new Command(args, this); 445 | // this.writeCmdToStream(args); 446 | 447 | if (this.isSendingTxnCmds) { 448 | this.numUnackedTxnCmds++; 449 | // command.transformCuzPartOfTransaction(); 450 | var intendedCommandCallback = command.commandCallback || Command.prototype.commandCallback; 451 | command.commandCallback = this.onTxnAck.bind(this); 452 | this.currTxnCommands.push({commandName: command.commandName, callback: intendedCommandCallback}); 453 | } 454 | if (command.isPubSub) { 455 | this.inPubSubMode = true; 456 | } else { 457 | if (this.inPubSubMode) throw new Error("Client is in Pub/Sub mode. Only Pub/Sub commands are allowed in this mode. Use another client for other commands."); 458 | } 459 | if (!this.stream.writable) { // TODO Analyze this condition with transaction scenario 460 | this.queuedCommandHistory.push(command); 461 | // this.queuedCommandBuffers.push(command.toBuffer()); 462 | } else { 463 | this.commandHistory.push(command.toHash()); 464 | command.writeToStream(); 465 | } 466 | }; 467 | 468 | Client.prototype.writeCmdToStream = function (commandAsArray) { 469 | var commandName = commandAsArray.shift(), 470 | commandCallback, 471 | cmdStr, 472 | useBuffer = false, 473 | stream = this.stream; 474 | 475 | // Remove the callback 476 | if (typeof commandAsArray[commandAsArray.length-1] === "function") { 477 | commandCallback = commandAsArray.pop(); 478 | } 479 | // Derive additional values or derivative values from the commandAsArray 480 | var numArgs = commandAsArray.length, 481 | lastArg = commandAsArray[numArgs-1]; 482 | 483 | // Handle the transactional scenario 484 | if (this.isSendingTxnCmds) { 485 | this.numUnackedTxnCmds++; 486 | var intendedCmdCallback = commandCallback || Command.prototype.commandCallback; 487 | commandCallback = this.onTxnAck.bind(this); 488 | this.currTxnCommands.push({commandName: commandName, callback: intendedCommandCallback}); 489 | } 490 | 491 | // Handle the PubSub scenario 492 | var isPubSub = /^p(un)?subscribe$/.test(commandName); 493 | if (isPubSub) { 494 | this.inPubSubMode = true; 495 | } else { 496 | if (this.inPubSubMode) throw new Error("Client is in Pub/Sub mode. Only Pub/Sub commands are allowed in this mode. Use another client for other commands."); 497 | } 498 | 499 | var i, arg; 500 | if (!stream.writable) { 501 | this.queuedCommandHistory.push(commandAsArray); 502 | } else { 503 | var hash = { 504 | commandName: commandName 505 | }; 506 | if (commandCallback) hash.commandCallback = commandCallback; 507 | if (commandName === "select") hash.db = commandAsArray[1]; 508 | if (lastArg === "withscores") hash.withscores = true; 509 | this.commandHistory.push(hash); 510 | if (commandName === "get" && lastArg && lastArg.encoding) { 511 | hash.encoding = lastArg.encoding; 512 | numArgs--; 513 | } 514 | for (i = 0; i < numArgs; i++) { 515 | if (Buffer.isBuffer(commandAsArray[i])) { 516 | useBuffer = true; 517 | break; 518 | } 519 | } 520 | 521 | cmdStr = "*" + (numArgs+1) + CRLF + // Bulks to expect 522 | "$" + commandName.length + CRLF + // Command Name Bytelength 523 | commandName + CRLF; 524 | 525 | if (useBuffer) { 526 | stream.write(cmdStr); 527 | for (i = 0; i < numArgs; i++) { 528 | arg = commandAsArray[i]; 529 | if (Buffer.isBuffer(arg)) { 530 | stream.write("$" + arg.length + CRLF); 531 | stream.write(arg); 532 | stream.write(CRLF); 533 | } else { 534 | arg = arg + ''; 535 | stream.write("$" + arg.length + CRLF + arg + CRLF); 536 | } 537 | } 538 | } else { 539 | for (i = 0; i < numArgs; i++) { 540 | arg = commandAsArray + ''; 541 | cmdStr += "$" + arg.length + CRLF + arg + CRLF; 542 | } 543 | stream.write(cmdStr); 544 | } 545 | } 546 | 547 | }; 548 | 549 | // redis-node preserves the order of command and transaction calls in your app. 550 | 551 | /** 552 | * What you call to initiate a transaction. 553 | * Example: 554 | * client.transaction( function (t) { 555 | * t.rpush("list", "value", function () { 556 | * // ... Do stuff with the result of this command 557 | * }); 558 | * t.lpop("list", function () { 559 | * // ... Do stuff with the result of this command 560 | * }); 561 | * }); 562 | * @param {Function} doStuffInsideTransaction is a function that wraps one or more commands that you want executed inside the transaction. 563 | */ 564 | Client.prototype.transaction = function (doStuffInsideTransaction) { 565 | // The following if handles nested transactions: e.g., 566 | // client.transaction( function (t) { 567 | // client.transaction (function (t2) { 568 | // // ... Do stuff here 569 | // } 570 | // }); 571 | if (this.isSendingTxnCmds) { 572 | doStuffInsideTransaction(); 573 | } else if (!this.isListeningForTxnAcks) { 574 | this.sendMultiToServer(); 575 | this.isListeningForTxnAcks = true; 576 | this.isSendingTxnCmds = true; 577 | this.didRegisterAllTxnCommands = false; 578 | this.numUnackedTxnCmds = 0; 579 | doStuffInsideTransaction(); 580 | this.didRegisterAllTxnCommands = true; 581 | this.isSendingTxnCmds = false; 582 | if (this.numUnackedTxnCmds === 0) this.sendExecToServer(); 583 | } else { 584 | this.cmdsToRunAfterTxn.push(doStuffInsideTransaction); 585 | } 586 | }; 587 | 588 | Client.prototype.sendMultiToServer = function () { 589 | this.multi( function (err, reply) { 590 | if (err) throw err; 591 | if (reply !== true) throw new Error("Expected 'OK'. Reply is " + util.inspect(reply)); 592 | }); 593 | }; 594 | 595 | Client.prototype.sendExecToServer = function () { 596 | var client = this; 597 | this.isListeningForTxnAcks = false; 598 | this.exec( function (err, replies) { 599 | if (err) throw err; 600 | var reply; 601 | while (reply = replies.shift()) { 602 | client.handleReply(reply, true); 603 | } 604 | }); 605 | this.runPostTxnCommands(); 606 | }; 607 | 608 | Client.prototype.runPostTxnCommands = function () { 609 | var nextCmds = this.cmdsToRunAfterTxn, nextCmdAsArray; 610 | while (nextCmdAsArray = nextCmds.shift()) { 611 | if (typeof nextCmdAsArray === "function") { 612 | this.transaction(nextCmdAsArray); 613 | break; 614 | } else { 615 | this.sendCommand(nextCmdAsArray); 616 | } 617 | } 618 | }; 619 | 620 | //var commandFns, 621 | // commandBuilder; 622 | //for (var commandName in commandConfig) { 623 | // commandFns = commandConfig[commandName]; 624 | // if (commandBuilder = commandFns.buildCommandArray) { 625 | // Client.prototype[commandName] = (function (commandBuilder) { 626 | // return function () { 627 | // var args = commandBuilder.apply(this, arguments); 628 | // this.sendCommand.apply(this, args); 629 | // }; 630 | // })(commandBuilder); 631 | // } 632 | //}; 633 | 634 | Client.prototype.sort = function (key, options, callback) { 635 | var args = ["sort", key]; 636 | if (options.by) { 637 | args.push("by", options.by); 638 | } 639 | if (options.limit) { 640 | args.push("limit", options.limit[0], options.limit[1]); 641 | } 642 | if (options.get) { 643 | if (options.get instanceof Array) { 644 | options.get.forEach( function (target) { 645 | args.push("get", target); 646 | }); 647 | } else { 648 | args.push("get", options.get); 649 | } 650 | } 651 | if (options.order) { 652 | args.push(options.order); 653 | } 654 | if (options.alpha === true) { 655 | args.push("alpha"); 656 | } 657 | if (options.store) { 658 | args.push("store", options.store); 659 | } 660 | if (callback) { 661 | args.push(callback); 662 | } 663 | this.sendCommand(args); 664 | }; 665 | 666 | // TODO Either use this or the version that uses commandConfig 667 | // (in latter case, uncomment hmset in commandList) 668 | Client.prototype.hmset = function (key, hash, callback) { 669 | var args = ["hmset", key]; 670 | for (var property in hash) if (hash.hasOwnProperty(property)) { 671 | args.push(property, hash[property]); 672 | }; 673 | if (callback) args.push(callback); 674 | this.sendCommand(args); 675 | }; 676 | 677 | Client.prototype.subscribeTo = function (nameOrPattern, callback) { 678 | var callbacks = this.channelCallbacks, 679 | methodName; 680 | if (callbacks[nameOrPattern]) return; 681 | if (typeof callback !== "function") { 682 | throw new Error("You must provide a callback function to subscribe"); 683 | } 684 | callbacks[nameOrPattern] = callback; 685 | methodName = (/[\*\?\[]/).test(nameOrPattern) ? "psubscribe" : "subscribe"; 686 | this[methodName](nameOrPattern, function (err, reply) { 687 | if (err) throw err; // TODO Analyze this 688 | }); 689 | }; 690 | 691 | Client.prototype.unsubscribeFrom = function (nameOrPattern) { 692 | var callbacks = this.channelCallbacks, 693 | methodName; 694 | if (!callbacks[nameOrPattern]) return; 695 | delete this.channelCallbacks[nameOrPattern]; 696 | methodName = (/[\*\?\[]/).test(nameOrPattern) ? "punsubscribe" : "unsubscribe"; 697 | }; 698 | 699 | /** 700 | * Sample calls: 701 | * client.zunionstore("tokey", ["key1", "key2"]); 702 | * client.zunionstore("tokey", {key1: 4, key2: 7}); 703 | * client.zunionstore("tokey", {key1: 4, key2: 7}, "sum"); 704 | * client.zunionstore("tokey", {key1: 4, key2: 7}, "min"); 705 | * client.zunionstore("tokey", {key1: 4, key2: 7}, "max"); 706 | */ 707 | ["zunionstore", "zinterstore"].forEach( function (commandName) { 708 | Client.prototype[commandName] = function () { 709 | var args = toArray(arguments), 710 | commandArgs = [commandName], 711 | dstkey = args.shift(), 712 | numKeys, 713 | keys = args.shift(), // Either an array of keys or a hash mapping keys to weights 714 | aggregateType, 715 | callback; 716 | if (typeof args[args.length-1] === "function") { 717 | callback = args.pop(); 718 | } 719 | aggregateType = args.shift(); // either "sum", "min", or "max" 720 | commandArgs.push(dstkey); 721 | if (keys instanceof Array) { 722 | numKeys = keys.length; 723 | commandArgs.push(numKeys); 724 | for (var i = 0; i < numKeys; i++) { 725 | commandArgs.push(keys[i]); 726 | } 727 | } else if (keys instanceof Object) { 728 | var weights = []; 729 | numKeys = 0; 730 | for (var keyName in keys) if (keys.hasOwnProperty(keyName)) { 731 | weights.push(keys[keyName]); 732 | commandArgs.push(keyName); 733 | numKeys++; 734 | } 735 | commandArgs.splice(2, 0, numKeys); 736 | commandArgs.push("WEIGHTS"); 737 | for (var i = 0; i < numKeys; i++) { 738 | commandArgs.push(weights[i]); 739 | } 740 | } 741 | if (aggregateType) { 742 | commandArgs.push("AGGREGATE", aggregateType); 743 | } 744 | if (callback) commandArgs.push(callback); 745 | this.sendCommand(commandArgs); 746 | }; 747 | }); 748 | 749 | var commandsAcceptingArrayAsArgs = [ 750 | 'del', 751 | 'sinter', 752 | 'sunion' 753 | ]; 754 | 755 | commandsAcceptingArrayAsArgs.forEach( function (commandName) { 756 | Client.prototype[commandName] = function () { 757 | var arglen = arguments.length, 758 | args, 759 | i, len, numKeys; 760 | if (Array.isArray(arguments[0])) { // If we're passing in an array of keys 761 | numKeys = arguments[0].length; 762 | args = new Array(numKeys + arglen); 763 | for (i = 0; i < numKeys; i++) { 764 | args[i+1] = arguments[0][i]; 765 | } 766 | for (i = 1, len = arguments.length; i < len; i++) { 767 | args[numKeys + i] = arguments[i]; 768 | } 769 | } else { // Else we're passing in the list of keys as part of the comma-separated arguments list 770 | args = new Array(1 + arglen); 771 | for (i = 0, len = arguments.length; i < len; i++) { 772 | args[i+1] = arguments[i]; 773 | } 774 | } 775 | args[0] = commandName; 776 | this.sendCommand(args); 777 | } 778 | }); 779 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var util = require("util"); 26 | var Buffer = require("buffer").Buffer, 27 | CRLF = "\r\n"; 28 | 29 | 30 | var Command = exports.Command = function (commandAsArray, client) { 31 | var commandName = this.commandName = commandAsArray.shift(); 32 | if (typeof commandAsArray[commandAsArray.length-1] === "function") { 33 | this.commandCallback = commandAsArray.pop(); 34 | } 35 | 36 | // Helps the client to accept the option {encoding: "binary"} for client.get 37 | // Useful when retrieving a stored image 38 | if (commandName === "get" && commandAsArray[commandAsArray.length-1].encoding) { 39 | this.encoding = commandAsArray.pop().encoding; 40 | } 41 | this.commandArgs = commandAsArray; 42 | this.client = client; 43 | this.isPubSub = /^p?(un)?subscribe$/.test(commandName); 44 | }; 45 | 46 | Command.prototype = { 47 | // So we keep things lightweight in our commandHistory 48 | toHash: function () { 49 | var commandName = this.commandName; 50 | var hash = { 51 | commandName: commandName 52 | }; 53 | if (this.hasOwnProperty("commandCallback")) { 54 | hash.commandCallback = this.commandCallback; 55 | } 56 | if (commandName === "select") { 57 | hash.db = this.commandArgs[0]; 58 | } 59 | var args = this.commandArgs; 60 | if (args[args.length-1] === "withscores") { 61 | hash.withscores = true; 62 | } 63 | if (this.encoding) hash.encoding = this.encoding; 64 | return hash; 65 | }, 66 | 67 | // Default Callback in case a commandCallback is not explicitly 68 | // set on the command instance 69 | commandCallback: function (err, reply) { 70 | if (err) util.log(err); 71 | }, 72 | 73 | hasBufferArgs: function () { 74 | var args = this.commandArgs; 75 | for (var i = 0, len = args.length; i < len; i++) { 76 | if (Buffer.isBuffer(args[i])) { 77 | return true; 78 | } 79 | } 80 | return false; 81 | }, 82 | /** 83 | * Returns the command in a form that can get sent over the stream 84 | * to the Redis server. Either returns a Buffer or a String. 85 | */ 86 | writeToStream: function () { 87 | var commandArgs = this.commandArgs, 88 | commandName = this.commandName, 89 | numArgs = commandArgs.length, 90 | expectedBulks = 1 + numArgs, // +1 for commandName 91 | useBuffer = this.hasBufferArgs(), 92 | cmdStr = "*" + (1 + numArgs) + CRLF + // Bulks to expect; +1 for commandName 93 | "$" + commandName.length + CRLF + // Command Name Bytelength 94 | commandName + CRLF, 95 | stream = this.client.stream, 96 | i, arg; 97 | if (useBuffer) { 98 | stream.write(cmdStr); 99 | for (i = 0; i < numArgs; i++) { 100 | arg = commandArgs[i]; 101 | if (Buffer.isBuffer(arg)) { 102 | stream.write("$" + arg.length + CRLF); 103 | stream.write(arg); 104 | stream.write(CRLF); 105 | } else { 106 | arg = arg + ''; 107 | stream.write("$" + arg.length + CRLF + arg + CRLF); 108 | } 109 | } 110 | } else if (this.client.utf8) { 111 | stream.write(cmdStr); 112 | for (i = 0; i < numArgs; i++) { 113 | arg = commandArgs[i] + ''; 114 | if (!Buffer.isBuffer(arg)) arg = new Buffer(arg); 115 | stream.write("$" + arg.length + CRLF); 116 | stream.write(arg); 117 | stream.write(CRLF); 118 | } 119 | } else { 120 | for (i = 0; i < numArgs; i++) { 121 | arg = commandArgs[i] + ''; 122 | cmdStr += "$" + arg.length + CRLF + arg + CRLF; 123 | } 124 | stream.write(cmdStr); 125 | } 126 | // delete this.client; // Removes client from command, so command's easier on the eyes when util inspecting 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /lib/commandConfig.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var commandConfig = { 26 | // TODO Move this functionality into Reply.prototype? 27 | info: { 28 | typecastReplyValue: function (replyValue) { 29 | var info = {}; 30 | replyValue.replace(/\r\n$/, '').split("\r\n").forEach( function (line) { 31 | var parts = line.split(":"); 32 | info[parts[0]] = parts[1]; 33 | }); 34 | return replyValue = info; 35 | } 36 | }, 37 | exists: { 38 | typecastReplyValue: function (replyValue) { 39 | return replyValue === 1; 40 | } 41 | }, 42 | 43 | zrange: { 44 | typecastReplyValue: function (replyValue, originalCommand) { 45 | if (!originalCommand.withscores) { 46 | return replyValue; 47 | } 48 | var arr = replyValue, hash, currKey, newArr = []; 49 | for (var i = 0, len = arr.length; i < len; i++) { 50 | if ((i % 2) === 0) { 51 | currKey = arr[i]; 52 | } else { 53 | hash = {}; 54 | hash[currKey] = arr[i]; 55 | newArr.push(hash); 56 | } 57 | } 58 | return replyValue = newArr; 59 | } 60 | } 61 | }; 62 | 63 | commandConfig.zrangebyscore = commandConfig.zrevrange = commandConfig.zrange; 64 | 65 | module.exports = commandConfig; 66 | -------------------------------------------------------------------------------- /lib/commandList.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | // Commands that we dynamically define in client.js as wrappers around 26 | // our Client.prototype.sendCommand method 27 | module.exports = [ 28 | "append", 29 | "auth", 30 | "bgsave", 31 | "blpop", 32 | "brpop", 33 | "dbsize", 34 | "decr", 35 | "decrby", 36 | "discard", 37 | "exec", 38 | "exists", 39 | "expire", 40 | "expireat", 41 | "flushall", 42 | "flushdb", 43 | "get", 44 | "getset", 45 | "hdel", 46 | "hexists", 47 | "hget", 48 | "hgetall", 49 | "hincrby", 50 | "hkeys", 51 | "hlen", 52 | "hmget", 53 | "hset", 54 | "hvals", 55 | "incr", 56 | "incrby", 57 | "info", 58 | "keys", 59 | "lastsave", 60 | "len", 61 | "lindex", 62 | "llen", 63 | "lpop", 64 | "lpush", 65 | "lrange", 66 | "lrem", 67 | "lset", 68 | "ltrim", 69 | "mget", 70 | "move", 71 | "mset", 72 | "msetnx", 73 | "multi", 74 | "psubscribe", 75 | "publish", 76 | "punsubscribe", 77 | "randomkey", 78 | "rename", 79 | "renamenx", 80 | "rpop", 81 | "rpoplpush", 82 | "rpush", 83 | "sadd", 84 | "save", 85 | "scard", 86 | "sdiff", 87 | "sdiffstore", 88 | "select", 89 | "set", 90 | "setex", 91 | "setnx", 92 | "shutdown", 93 | "sinterstore", 94 | "sismember", 95 | "smembers", 96 | "smove", 97 | "spop", 98 | "srandmember", 99 | "srem", 100 | "subscribe", 101 | "substr", 102 | "sunionstore", 103 | "ttl", 104 | "type", 105 | "unsubscribe", 106 | "zadd", 107 | "zcard", 108 | "zcount", 109 | "zincrby", 110 | "zrange", 111 | "zrangebyscore", 112 | "zrank", 113 | "zrem", 114 | "zrembyrank", 115 | "zremrangebyrank", 116 | "zremrangebyscore", 117 | "zrevrange", 118 | "zrevrank", 119 | "zscore" 120 | ]; 121 | 122 | -------------------------------------------------------------------------------- /lib/perf.js: -------------------------------------------------------------------------------- 1 | exports.toSmallString = function (buffer, until) { 2 | until = until || buffer.length; 3 | var ret = "", 4 | i = 0; 5 | for ( ; i < until; i++) { 6 | ret += String.fromCharCode(buffer[i]); 7 | } 8 | return ret; 9 | }; 10 | -------------------------------------------------------------------------------- /lib/redis.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var Client = require("./client").Client, 26 | DEFAULT_HOST = "127.0.0.1", 27 | DEFAULT_PORT = 6379; 28 | 29 | exports.createClient = function (port, host, options) { 30 | return new Client(port || DEFAULT_PORT, host || DEFAULT_HOST, options); 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /lib/replies/bulkReply.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var Buffer = require("buffer").Buffer, 26 | CR = require("../reply").CR, // \r 27 | IntegerReply = require("./integerReply").IntegerReply; 28 | 29 | var BulkReply = exports.BulkReply = function BulkReply (client, context) { 30 | this.client = client; 31 | this.isBinaryData = context && context.isBinaryData; // TODO 32 | 33 | this.isComplete = false; 34 | this.replyValue = null; 35 | this.bytesWritten = 0; 36 | this.expectedProxy = {replyValue: "", i: 0, isComplete: false}; 37 | }; 38 | 39 | BulkReply.prototype.parse = function (data, atDataIndex) { 40 | var dataLen = data.length, 41 | sliceTo, 42 | val, 43 | client = this.client, 44 | expected, 45 | expectedProxy; 46 | while (atDataIndex < dataLen) { 47 | if (typeof this.expected === "undefined") { 48 | expectedProxy = this.expectedProxy; 49 | atDataIndex = IntegerReply.prototype.parse.call(expectedProxy, data, atDataIndex); 50 | if (expectedProxy.isComplete) { 51 | this.remaining = this.expected = expectedProxy.replyValue; 52 | if (this.expected <= 0) { 53 | this.replyValue = null; 54 | this.isComplete = true; 55 | break; 56 | } 57 | // Resize if necessary 58 | if (client.replyBuffer.length < this.expected) { 59 | client.replyBuffer = new Buffer(this.expected); 60 | } 61 | } 62 | continue; 63 | } 64 | expected = this.expected; 65 | sliceTo = atDataIndex + this.remaining; 66 | // If the data packet won't contain all the expected data 67 | if (dataLen < sliceTo) { 68 | sliceTo = dataLen; 69 | numNewBytes = sliceTo - atDataIndex; 70 | if (numNewBytes > 12) { 71 | data.copy(client.replyBuffer, this.bytesWritten, atDataIndex, sliceTo); 72 | this.bytesWritten += numNewBytes; 73 | } else { 74 | for (var j = atDataIndex; j < sliceTo; j++) { 75 | client.replyBuffer[this.bytesWritten++] = data[j]; 76 | } 77 | } 78 | this.remaining -= numNewBytes; 79 | atDataIndex = sliceTo; // === dataLen 80 | } else { // Else the data packet contains enough data to complete the reply 81 | var encoding = this.client.commandHistory.peek() || this.client.currTxnCommands[0]; 82 | if (encoding) encoding = encoding.encoding; 83 | if (this.bytesWritten > 0) { 84 | numNewBytes = sliceTo - atDataIndex; 85 | if (numNewBytes > 12) { 86 | data.copy(client.replyBuffer, this.bytesWritten, atDataIndex, sliceTo); 87 | this.bytesWritten += numNewBytes; 88 | } else { 89 | for (var j = atDataIndex; j < sliceTo; j++) { 90 | client.replyBuffer[this.bytesWritten++] = data[j]; 91 | } 92 | } 93 | this.replyValue = client.replyBuffer.toString(encoding || "utf8", 0, expected); // Default typecast to utf8 94 | } else { 95 | this.replyValue = data.toString(encoding || "utf8", atDataIndex, sliceTo); 96 | } 97 | 98 | this.isComplete = true; 99 | 100 | // Try advancing beyond CRLF 101 | if (data[sliceTo] === CR) { 102 | atDataIndex = sliceTo + 2; 103 | } else { 104 | atDataIndex = sliceTo; 105 | } 106 | break; 107 | } 108 | } 109 | return atDataIndex; 110 | }; 111 | -------------------------------------------------------------------------------- /lib/replies/errorReply.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var CR = require("../reply").CR, 26 | LF = require("../reply").LF, 27 | InlineReply = require("./inlineReply").InlineReply; 28 | 29 | var toSmallString = require("../perf").toSmallString; 30 | 31 | var ErrorReply = exports.ErrorReply = function ErrorReply (client) { 32 | this.client = client; 33 | 34 | this.isComplete = false; 35 | this.replyValue = null; 36 | }; 37 | 38 | ErrorReply.prototype.parse = function (data, atDataIndex) { 39 | var dataLen = data.length, 40 | sliceFrom = this.sliceFrom = this.sliceFrom || atDataIndex, 41 | val, 42 | client = this.client; 43 | while (atDataIndex < dataLen) { 44 | if (data[atDataIndex] === CR) { 45 | if (++atDataIndex < dataLen) { 46 | atDataIndex++; 47 | this.isComplete = true; 48 | break; 49 | } 50 | } else if (data[atDataIndex] === LF) { 51 | atDataIndex++; 52 | this.isComplete = true; 53 | break; 54 | } else { 55 | atDataIndex++; 56 | } 57 | } 58 | if (!this.line) { 59 | this.line = data.slice(sliceFrom, atDataIndex); 60 | } else { 61 | var minLen = this.line.length + (atDataIndex - sliceFrom); 62 | // Resize buffer if necessary 63 | if (client.replyBuffer.length < minLen) { 64 | client.replyBuffer = new Buffer(minLen); 65 | } 66 | this.line.copy(client.replyBuffer, 0, 0); 67 | data.copy(client.replyBuffer, this.line.length, sliceFrom, atDataIndex); 68 | this.replyValue = client.replyBuffer.slice(0, minLen); 69 | } 70 | if (this.isComplete) { 71 | this.replyValue = this.line; 72 | if (this.line.length > 10) { 73 | this.replyValue = this.line.toString("utf8", 0, this.line.length); 74 | } else { 75 | this.replyValue = toSmallString(this.line); 76 | } 77 | } 78 | return atDataIndex; 79 | }; 80 | -------------------------------------------------------------------------------- /lib/replies/inlineReply.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var CR = require("../reply").CR; 26 | 27 | var toSmallString = require("../perf").toSmallString; 28 | 29 | var InlineReply = exports.InlineReply = function InlineReply (client) { 30 | this.client = client; 31 | 32 | this.isComplete = false; 33 | this.replyValue = ""; 34 | this.i = 0; 35 | }; 36 | 37 | // TODO Extract this algo into its own re-usable function 38 | InlineReply.prototype.parse = function (data, atDataIndex) { 39 | // TODO Improve by 40 | // - Count chars to end 41 | var dataLen = data.length, 42 | sliceFrom = this.sliceFrom = this.sliceFrom || atDataIndex, 43 | client = this.client, 44 | line = this.replyValue, lineLen; 45 | 46 | while (atDataIndex < dataLen) { 47 | if (data[atDataIndex] === CR) { 48 | this.isComplete = true; 49 | atDataIndex += 2; 50 | if (line === "OK") this.replyValue = true; 51 | else this.replyValue = line; 52 | break; 53 | } else { 54 | line += String.fromCharCode(data[atDataIndex++]); 55 | } 56 | } 57 | 58 | // line = this.line; 59 | // 60 | // if (!line) { 61 | // line = this.line = data.slice(sliceFrom, atDataIndex); 62 | // } else { 63 | // lineLen = line.length; 64 | // var minLen = lineLen + (atDataIndex - sliceFrom); 65 | // // Resize buffer if necessary 66 | // if (client.replyBuffer.length < minLen) { 67 | // client.replyBuffer = new Buffer(minLen); 68 | // } 69 | // 70 | // // Speed hack 71 | // if (lineLen > 10) { 72 | // line.copy(client.replyBuffer, 0, 0); 73 | // } else { 74 | // for (var i = lineLen-1; i >= 0; i--) { 75 | // client.replyBuffer[i] = line[i]; 76 | // } 77 | // } 78 | // 79 | // // Speed hack 80 | // if (atDataIndex - sliceFrom > 10) { 81 | // data.copy(client.replyBuffer, lineLen, sliceFrom, atDataIndex); 82 | // } else { 83 | // for (var i = sliceFrom; i < atDataIndex; i++) { 84 | // client.replyBuffer[i] = data[i]; 85 | // } 86 | // } 87 | // line = this.line = client.replyBuffer.slice(0, minLen); 88 | // } 89 | // if (this.isComplete) { 90 | // lineLen = line.length; 91 | // if (lineLen > 15) { 92 | // this.replyValue = line.toString("ascii", 0, lineLen); 93 | // } else { 94 | // this.replyValue = toSmallString(line); 95 | // } 96 | // if (this.replyValue === "OK") this.replyValue = true; 97 | // 98 | // delete this.line; 99 | // atDataIndex += 2; // Move beyond the CRLF 100 | // } 101 | // delete this.sliceFrom; 102 | return atDataIndex; 103 | }; 104 | -------------------------------------------------------------------------------- /lib/replies/integerReply.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var CR = require("../reply").CR, 26 | LF = require("../reply").LF, 27 | InlineReply = require("./inlineReply").InlineReply; 28 | 29 | 30 | var toSmallString = require("../perf").toSmallString; 31 | 32 | var IntegerReply = exports.IntegerReply = function IntegerReply () { 33 | this.isComplete = false; 34 | this.replyValue = ""; 35 | this.i = 0; 36 | }; 37 | 38 | IntegerReply.prototype.parse = function (data, atDataIndex) { 39 | var dataLen = data.length, 40 | line = this.replyValue; 41 | while (atDataIndex < dataLen) { 42 | if (data[atDataIndex] === CR) { 43 | if (++atDataIndex < dataLen) { 44 | atDataIndex++; 45 | this.isComplete = true; 46 | break; 47 | } 48 | } else if (data[atDataIndex] === LF) { 49 | atDataIndex++; 50 | this.isComplete = true; 51 | break; 52 | } else { 53 | line += String.fromCharCode(data[atDataIndex++]); 54 | } 55 | } 56 | if (this.isComplete) this.replyValue = parseInt(line, 10); 57 | else this.replyValue = line; 58 | 59 | // atDataIndex = InlineReply.prototype.parse.call(this, data, atDataIndex); 60 | // if (this.isComplete) { 61 | // this.replyValue = parseInt(this.replyValue, 10); 62 | // } 63 | return atDataIndex; 64 | }; 65 | -------------------------------------------------------------------------------- /lib/replies/multibulkReply.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var Reply = require("../reply").Reply, 26 | CR = require("../reply").CR, 27 | LF = require("../reply").LF, 28 | BulkReply = require("./bulkReply").BulkReply, 29 | IntegerReply = require("./integerReply").IntegerReply; 30 | 31 | MultibulkReply = exports.MultibulkReply = function (client, context) { 32 | this.client = client; 33 | this.context = context; 34 | this.isComplete = false; 35 | this.replyValue = null; 36 | this.expectedProxy = {replyValue: "", i: 0, isComplete: false}; 37 | }; 38 | 39 | Object.defineProperty(MultibulkReply.prototype, 'latest', { 40 | get: function () { 41 | var ctx = this.context; 42 | return (ctx.scope === "exec") ? 43 | ctx.currCommandName : 44 | ctx.scope; 45 | } 46 | }); 47 | 48 | MultibulkReply.prototype.parse = function (data, atDataIndex) { 49 | var dataLen = data.length, 50 | childReplies = this.replies, 51 | expected, expectedProxy; 52 | while (atDataIndex < dataLen) { 53 | if (typeof this.expected === "undefined") { 54 | expectedProxy = this.expectedProxy; 55 | atDataIndex = IntegerReply.prototype.parse.call(expectedProxy, data, atDataIndex); 56 | if (expectedProxy.isComplete) { 57 | expected = this.expected = expectedProxy.replyValue; 58 | if (expected === -1) { 59 | this.replyValue = null; 60 | this.isComplete = true; 61 | break; 62 | } 63 | if (expected === 0) { // For '*0\r\n' 64 | this.replyValue = this.isHashValuable[this.latest] ? null : []; 65 | this.isComplete = true; 66 | break; 67 | } 68 | childReplies = this.replies = new Array(expected); 69 | this.numChildren = 0; 70 | } 71 | continue; 72 | } 73 | 74 | latestReply = childReplies[this.numChildren-1]; 75 | if (latestReply && !latestReply.isComplete) { 76 | atDataIndex = latestReply.parse(data, atDataIndex); 77 | } else { 78 | var newReply = Reply.fromTypeCode( data[atDataIndex++], this.client, this.context ); 79 | if (!newReply) { 80 | continue; 81 | } 82 | latestReply = childReplies[this.numChildren++] = newReply; 83 | } 84 | if (latestReply.isComplete) { 85 | if (this.numChildren === 1) { 86 | // Find the child2ValueFn to use to add any child replyValue to this.replyValue. 87 | // We do this by evaluating the first reply in this multibulk. This gives us enough 88 | // clues about what form this.replyValue should take - either a Message, PMessage, 89 | // Hash, or Array. 90 | 91 | // Determine if this is a PUBSUB message by peeking at the first child reply 92 | var childReply1Value = latestReply.replyValue, 93 | childReply1ValueLen = childReply1Value && childReply1Value.length; 94 | if (expected === 3 && childReply1ValueLen === 7 && childReply1Value === "message") { 95 | // If this is a PUBSUB message 96 | this.isMessage = true; 97 | this.child2ValueFn = this.child2MessageValue; 98 | this.replyValue = {}; 99 | } else if (expected === 4 && childReply1ValueLen === 8 && childReply1Value === "pmessage") { 100 | // If this is a PUBSUB pmessage 101 | this.isPMessage = true; 102 | this.child2ValueFn = this.child2PMessageValue; 103 | this.replyValue = {}; 104 | } else { 105 | this.child2ValueFn = this.getTransformerFromContext(); 106 | } 107 | } 108 | this.child2ValueFn(); 109 | if (this.expected === this.numChildren) { 110 | this.isComplete = true; 111 | break; 112 | } 113 | } 114 | } 115 | return atDataIndex; 116 | }; 117 | 118 | MultibulkReply.prototype.child2MessageValue = function () { 119 | var childReplies = this.replies, 120 | numChildReplies = this.numChildren, 121 | childReply = childReplies[numChildReplies-1]; 122 | if (numChildReplies === 1) { 123 | // Do nothing because the 1st reply is just "message" or "pmessage" 124 | } else if (numChildReplies === 2) { 125 | this.replyValue.channelName = childReply.replyValue; 126 | } else if (numChildReplies === 3) { // === expected 127 | this.replyValue.message = childReply.replyValue; 128 | } else { 129 | throw new Error("Out of bounds unexpected."); 130 | } 131 | }; 132 | 133 | MultibulkReply.prototype.child2PMessageValue = function () { 134 | var childReplies = this.replies, 135 | numChildReplies = this.numChildren, 136 | childReply = childReplies[numChildReplies-1]; 137 | if (numChildReplies === 1) { 138 | // Do nothing because the 1st reply is just "message" or "pmessage" 139 | } else if (numChildReplies === 2) { 140 | this.replyValue.channelPattern = childReply.replyValue; 141 | } else if (numChildReplies === 3) { // === expected 142 | this.replyValue.channelName = childReply.replyValue; 143 | } else if (numChildReplies === 4) { 144 | this.replyValue.message = childReply.replyValue; 145 | } else { 146 | throw new Error("Out of bounds unexpected."); 147 | } 148 | }; 149 | 150 | MultibulkReply.prototype.child2HashValue = function () { 151 | var childReplies = this.replies, 152 | numChildReplies = this.numChildren, 153 | latestChildReply = childReplies[numChildReplies-1]; 154 | if (numChildReplies % 2 === 1) { 155 | this.nextKey = latestChildReply.replyValue; 156 | } else { 157 | this.replyValue[this.nextKey] = latestChildReply.replyValue; 158 | } 159 | }; 160 | 161 | MultibulkReply.prototype.child2ArrayValue = function () { 162 | var childReplies = this.replies, 163 | numChildReplies = this.numChildren, 164 | latestChildReply = childReplies[numChildReplies-1]; 165 | this.replyValue.push(latestChildReply.replyValue); 166 | }; 167 | 168 | // Multibulks can be found with: 169 | // -Transactions => return an array of replies 170 | // -Sort => return an array where elements are hashes or values 171 | // -Hgetall => return a hash 172 | // -Mget => return an array 173 | // -Others => return an array of values 174 | // Most extreme case is a transaction of sorts 175 | MultibulkReply.prototype.getTransformerFromContext = function () { 176 | var context = this.context, 177 | latest = this.latest, 178 | ret; 179 | 180 | if (latest === "sort") { 181 | if (!context.parsingSort) { 182 | ret = this.child2ArrayValue; 183 | this.replyValue = []; 184 | } 185 | else { 186 | ret = this.child2HashValue; 187 | this.replyValue = {}; 188 | } 189 | } else if (this.isHashValuable[latest]) { 190 | ret = this.child2HashValue; 191 | this.replyValue = {}; 192 | } else { 193 | // Among other scenarios, this else also takes care of (latest === "exec") or 194 | // when we're in special transaction exiting territory 195 | ret = this.child2ArrayValue; 196 | this.replyValue = []; 197 | } 198 | return ret; 199 | }; 200 | 201 | MultibulkReply.prototype.isHashValuable = { 202 | hgetall: true 203 | }; 204 | -------------------------------------------------------------------------------- /lib/reply.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | © 2010 by Brian Noguchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | */ 24 | 25 | var util = require("util"); 26 | 27 | var PLUS = 0x2B, // + 28 | MINUS = 0x2D, // - 29 | DOLLAR = 0x24, // $ 30 | STAR = 0x2A, // * 31 | COLON = 0x3A, // : 32 | 33 | CR = exports.CR = 0x0D, // \r 34 | LF = exports.LF = 0x0A; // \n 35 | 36 | var Reply = exports.Reply = function Reply () {}; 37 | 38 | var ErrorReply = require("./replies/errorReply").ErrorReply, 39 | InlineReply = require("./replies/inlineReply").InlineReply, 40 | IntegerReply = require("./replies/integerReply").IntegerReply, 41 | BulkReply = require("./replies/bulkReply").BulkReply, 42 | MultiBulkReply = require("./replies/multibulkReply").MultibulkReply; 43 | 44 | Reply.type2constructor = {}; 45 | Reply.type2constructor[MINUS] = ErrorReply; 46 | Reply.type2constructor[PLUS] = InlineReply; 47 | Reply.type2constructor[COLON] = IntegerReply; 48 | Reply.type2constructor[DOLLAR] = BulkReply; 49 | Reply.type2constructor[STAR] = MultibulkReply; 50 | 51 | /** 52 | * Factory method for creating replies. Figures out what type of reply (error, status, 53 | * integer, bulk, multibulk) to construct. 54 | */ 55 | Reply.fromTypeCode = function (typeCode, client, context) { 56 | var replyClass = this.type2constructor[typeCode], 57 | newContext; 58 | if (context && (context.scope === "exec")) { 59 | context.currTxnCmdIndex = (typeof context.currTxnCmdIndex === "undefined") ? -1 : context.currTxnCmdIndex; 60 | context.currTxnCmdIndex++; 61 | } 62 | if (replyClass === MultibulkReply) { 63 | // Setup the context to pass to the new MULTIBULK reply 64 | newContext = {}; 65 | if (client.commandHistory.length > 0) { // If this isn't a message or pmessage 66 | if (!context || !context.scope) { 67 | newContext.scope = client.commandHistory.peek().commandName; 68 | } else if (context.scope === "sort") { 69 | newContext.scope = context.scope; 70 | newContext.parsingSort = true; 71 | } else if (context.scope === "exec") { 72 | newContext.scope = context.scope; 73 | newContext.currCommandName = client.currTxnCommands[context.currTxnCmdIndex].commandName; 74 | } 75 | } 76 | } else if (!replyClass) { 77 | // throw new Error("Invalid type code: " + util.inspect(String.fromCharCode(typeCode))); 78 | return replyClass; 79 | } 80 | return new replyClass(client, newContext); 81 | }; 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-node", 3 | "description": "A Complete Redis Client for Node.js", 4 | "version": "0.4.0", 5 | "keywords": [ "redis", "node", "client", "redis-client", "redis-node" ], 6 | "author": "Brian Noguchi ", 7 | "contributors": [ 8 | { "name": "Brian Noguchi", "web": "http://ngchi.wordpress.com" }, 9 | { "name": "Tim Smart", "web": "http://github.com/Tim-Smart" }, 10 | { "name": "Graeme Worthy", "web": "http://github.com/graemeworthy" } 11 | ], 12 | "licenses": [ "MIT" ], 13 | "repository": { 14 | "type": "git", 15 | "url": "http://github.com/bnoguchi/redis-node.git" 16 | }, 17 | "bugs": { "web": "http://github.com/bnoguchi/redis-node/issues" }, 18 | "directories": { "lib": "./lib", "test": "./test" }, 19 | "os": [ "linux", "darwin" ], 20 | "dependencies": {}, 21 | "engines": { "node": ">=0.4.0" }, 22 | "main": "./index.js" 23 | } 24 | -------------------------------------------------------------------------------- /test/general_commands.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClientFactory = require("./utils").usingClient, 3 | usingClient = usingClientFactory.gen(), 4 | usingClient2 = usingClientFactory.gen(), 5 | usingClient3 = usingClientFactory.gen(), 6 | assert = require("assert"), 7 | redis = require("../lib/redis"); 8 | 9 | // TODO Test flushdb and flushall 10 | vows.describe("Redis General Commands").addBatch({ 11 | 'selecting a new DB': { 12 | topic: function () { 13 | var client = this.client = redis.createClient(); 14 | client.select(6, this.callback); 15 | }, 16 | 17 | 'should return true': function (err, result) { 18 | assert.isTrue(result); 19 | }, 20 | teardown: function () { 21 | this.client.close(); 22 | delete this.client; 23 | } 24 | }, 25 | 26 | 'the command EXISTS': usingClient({ 27 | 'on an existing key': { 28 | topic: function (client) { 29 | client.set("existing-key", "asdf"); 30 | client.exists("existing-key", this.callback); 31 | }, 32 | 33 | 'should return true': function (err, doesExist) { 34 | assert.isTrue(doesExist); 35 | } 36 | }, 37 | 38 | 'on a non-existing key': { 39 | topic: function (client) { 40 | client.exists("non-existing-key", this.callback); 41 | }, 42 | 43 | 'should return false': function (err, doesExist) { 44 | assert.isFalse(doesExist); 45 | } 46 | } 47 | }), 48 | 49 | 'the command DEL': usingClient({ 50 | 'on one existing key': { 51 | topic: function (client) { 52 | client.set("key-to-del", "foo"); 53 | client.del("key-to-del", this.callback); 54 | }, 55 | 56 | 'should return 1': function (err, value) { 57 | assert.equal(value, 1); 58 | } 59 | }, 60 | 61 | 'on multiple existing keys': { 62 | topic: function (client) { 63 | client.set("key-to-del-1", "foo"); 64 | client.set("key-to-del-2", "foo"); 65 | client.del("key-to-del-1", "key-to-del-2", this.callback); 66 | }, 67 | 68 | 'should return 2': function (err, value) { 69 | assert.equal(value, 2); 70 | } 71 | }, 72 | 73 | 'on multiple existing keys via array': { 74 | topic: function (client) { 75 | client.set("key-to-del-3", "foo"); 76 | client.set("key-to-del-4", "foo"); 77 | client.del(["key-to-del-3", "key-to-del-4"], this.callback); 78 | }, 79 | 80 | 'should return 2': function (err, value) { 81 | assert.equal(value, 2); 82 | } 83 | }, 84 | 85 | 'on a non-existent key': { 86 | topic: function (client) { 87 | client.del("non-existent-key", this.callback); 88 | }, 89 | 90 | 'should return 0': function (err, value) { 91 | assert.equal(value, 0); 92 | } 93 | } 94 | }), 95 | 96 | 'the command TYPE': usingClient({ 97 | 'when a set': { 98 | topic: function (client) { 99 | client.sadd("set-type-key", "a"); 100 | client.type("set-type-key", this.callback); 101 | }, 102 | "should return 'set'": function (err, type) { 103 | assert.equal(type, 'set'); 104 | } 105 | }, 106 | 'when a zset': { 107 | topic: function (client) { 108 | client.zadd("zset-type-key", 2, "a"); 109 | client.type("zset-type-key", this.callback); 110 | }, 111 | "should return 'zset'": function (err, type) { 112 | assert.equal(type, 'zset'); 113 | } 114 | }, 115 | 'when a hash': { 116 | topic: function (client) { 117 | client.hset("hash-type-key", "k", "v"); 118 | client.type("hash-type-key", this.callback); 119 | }, 120 | "should return 'hash'": function (err, type) { 121 | assert.equal(type, 'hash'); 122 | } 123 | }, 124 | 'when a list': { 125 | topic: function (client) { 126 | client.rpush("list-type-key", "a"); 127 | client.type("list-type-key", this.callback); 128 | }, 129 | "should return 'list'": function (err, type) { 130 | assert.equal(type, 'list'); 131 | } 132 | }, 133 | 'when a string': { 134 | topic: function (client) { 135 | client.set("string-type-key", "a"); 136 | client.type("string-type-key", this.callback); 137 | }, 138 | "should return 'string'": function (err, type) { 139 | assert.equal(type, 'string'); 140 | } 141 | }, 142 | 'when non-existent': { 143 | topic: function (client) { 144 | client.type("non-existent-key", this.callback); 145 | }, 146 | "should return 'none'": function (err, type) { 147 | assert.equal(type, 'none'); 148 | } 149 | } 150 | }), 151 | 152 | 'the command KEYS': usingClient({ 153 | 'using .* pattern matching': { 154 | topic: function (client) { 155 | client.set("star-suffix-key-1", "v1"); 156 | client.set("star-suffix-key-2", "v2"); 157 | client.keys("star-suffix*", this.callback); 158 | }, 159 | 160 | 'should return a list of all matching keys': function (err, list) { 161 | assert.deepEqual(list, ["star-suffix-key-1", "star-suffix-key-2"]); 162 | } 163 | }, 164 | 165 | 'using * pattern matching': { 166 | topic: function (client) { 167 | var client2 = this.client2 = redis.createClient(); 168 | client2.select(7); 169 | client2.set("a", 1); 170 | client2.set("b", 2); 171 | client2.set("the 3rd key", 3); 172 | client2.keys("*", this.callback); 173 | client2.flushdb(); 174 | }, 175 | 176 | 'should return a list of ALL keys': function (err, list) { 177 | assert.length(list, 3); 178 | ["a", "b", "the 3rd key"].forEach( function (val) { 179 | assert.include(list, val); 180 | }); 181 | }, 182 | teardown: function () { 183 | this.client2.close(); 184 | delete this.client2; 185 | } 186 | }, 187 | 188 | 'using ? pattern matching': { 189 | topic: function (client) { 190 | var client2 = this.client2 = redis.createClient(); 191 | client2.select(8); 192 | client2.set("bar", 1); 193 | client2.set("car", 2); 194 | client2.set("dar", 3); 195 | client2.set("far", 4); 196 | client2.keys("?ar", this.callback); 197 | client2.flushdb(); 198 | }, 199 | 200 | 'should return a list of all matching keys': function (err, list) { 201 | assert.length(list, 4); 202 | ["bar", "car", "dar", "far"].forEach( function (val) { 203 | assert.include(list, val); 204 | }); 205 | }, 206 | 207 | teardown: function () { 208 | this.client2.close(); 209 | delete this.client2; 210 | } 211 | } 212 | }), 213 | 214 | 'the command RANDOMKEY': usingClient({ 215 | topic: function (client) { 216 | var client2 = this.client2 = redis.createClient(); 217 | client2.select(9); 218 | client2.set("foo", "bar"); 219 | client2.set("hello", "world"); 220 | client2.randomkey(this.callback); 221 | client2.flushdb(); 222 | }, 223 | 224 | 'should return a random key': function (err, key) { 225 | assert.match(key, /^(foo|hello)$/); 226 | }, 227 | 228 | teardown: function () { 229 | this.client2.close(); 230 | delete this.client2; 231 | } 232 | }), 233 | 234 | 'the command RENAME': usingClient({ 235 | topic: function (client) { 236 | client.set("rename-1", "identity crisis"); 237 | client.rename("rename-1", "rename-2", this.callback); 238 | }, 239 | 'should return true': function (err, val) { 240 | assert.isTrue(val); 241 | }, 242 | 243 | 'after execution, when querying the existence of the old key': { 244 | topic: function (_, client) { 245 | client.exists("rename-1", this.callback); 246 | }, 247 | 'should return false': function (err, doesExist) { 248 | assert.isFalse(doesExist); 249 | } 250 | }, 251 | 252 | 'after execution, when querying the existence of the new key': { 253 | topic: function (_, client) { 254 | client.exists("rename-2", this.callback); 255 | }, 256 | 'should return true': function (err, doesExist) { 257 | assert.isTrue(doesExist); 258 | } 259 | } 260 | }), 261 | 262 | 'the command RENAMENX': usingClient({ 263 | 'renaming to a non-existing key': { 264 | topic: function (client) { 265 | client.set("rename-3", "anonymous"); 266 | client.renamenx("rename-3", "rename-4", this.callback); 267 | }, 268 | 'should return 1, specifying the key was renamed': function (err, value) { 269 | assert.equal(value, 1); 270 | }, 271 | 'after execution, when querying the existence of the old key': { 272 | topic: function (_, client) { 273 | client.exists("rename-3", this.callback); 274 | }, 275 | 'should return false': function (err, doesExist) { 276 | assert.isFalse(doesExist); 277 | } 278 | }, 279 | 'after execution, when querying the existence of the new key': { 280 | topic: function (_, client) { 281 | client.exists("rename-4", this.callback); 282 | }, 283 | 'should return true': function (err, doesExist) { 284 | assert.isTrue(doesExist); 285 | } 286 | } 287 | }, 288 | 'renaming to an existing key': { 289 | topic: function (client) { 290 | client.set("rename-5", "anonymous"); 291 | client.set("rename-6", "anonymous"); 292 | client.renamenx("rename-5", "rename-6", this.callback); 293 | }, 294 | 'should return 0, specifying the target key already exists': function (err, value) { 295 | assert.equal(value, 0); 296 | }, 297 | 'after execution, when querying the existence of the source key': { 298 | topic: function (_, client) { 299 | client.exists("rename-5", this.callback); 300 | }, 301 | 'should return true': function (err, doesExist) { 302 | assert.isTrue(doesExist); 303 | } 304 | }, 305 | 'after execution, when querying the existence of the target key': { 306 | topic: function (_, client) { 307 | client.exists("rename-6", this.callback); 308 | }, 309 | 'should return true': function (err, doesExist) { 310 | assert.isTrue(doesExist); 311 | } 312 | } 313 | } 314 | }), 315 | 316 | 'the command MOVE': { 317 | 'when the key exists in the source db but not the target db': usingClient({ 318 | topic: function (client) { 319 | client.rpush("db-moving-key", "a"); 320 | client.move("db-moving-key", 5, this.callback); 321 | }, 322 | 'should return an integer reply of 1': function (err, reply) { 323 | assert.equal(reply, 1); 324 | }, 325 | 'after moving, when in the source database': { 326 | topic: function (_, client) { 327 | client.exists("db-moving-key", this.callback); 328 | }, 329 | 'should be absent from the source database': function (err, doesExist) { 330 | assert.isFalse(doesExist); 331 | }, 332 | 'after moving, when in the destination database': { 333 | topic: function (_, _, client) { 334 | var client2 = this.client2 = redis.createClient(); 335 | client2.select(5); 336 | client2.lrange("db-moving-key", 0, -1, this.callback); 337 | client2.flushdb(); 338 | }, 339 | 'should appear in the destination database': function (err, list) { 340 | assert.deepEqual(list, ["a"]); 341 | }, 342 | teardown: function () { 343 | this.client2.close(); 344 | delete this.client2; 345 | } 346 | }, 347 | } 348 | }), 349 | 350 | 'when the key does not exist in the source db': usingClient({ 351 | topic: function (client) { 352 | client.move("non-existing-db-moving-key", 5, this.callback); 353 | }, 354 | 'should return an integer reply of 0': function (err, reply) { 355 | assert.equal(reply, 0); 356 | } 357 | }), 358 | 359 | 'when the key already exists in the target db': usingClient({ 360 | topic: function (client) { 361 | client.select(5); 362 | client.set("existing-db-moving-key", "hi"); 363 | client.select(6); 364 | client.set("existing-db-moving-key", "hi"); 365 | client.move("non-existing-db-moving-key", 5, this.callback); 366 | }, 367 | 'should return an integer reply of 0': function (err, reply) { 368 | assert.equal(reply, 0); 369 | } 370 | }) 371 | } 372 | }).addBatch({ 373 | 'the command DBSIZE': usingClient2({ 374 | topic: function (client) { 375 | client.flushdb(); 376 | client.set("foo", "bar"); 377 | client.set("hello", "world"); 378 | client.dbsize(this.callback); 379 | }, 380 | 381 | 'should return the number of keys in the DB': function (err, numKeys) { 382 | assert.equal(numKeys, 2); 383 | } 384 | }) 385 | }).addBatch({ 386 | 'the command EXPIRE': usingClient3({ 387 | 'on a key without a current expiry': { 388 | topic: function (client) { 389 | client.set("to-expire", "foo"); 390 | client.expire("to-expire", 2, this.callback); 391 | }, 392 | 393 | 'should return 1': function (err, isTimeoutSetStatus) { 394 | assert.equal(isTimeoutSetStatus, 1); 395 | }, 396 | 397 | 'after execution, before the time is up': { 398 | topic: function (_, client) { 399 | client.exists("to-expire", this.callback); 400 | }, 401 | 402 | 'should evaluate the key as existing': function (err, doesExist) { 403 | assert.isTrue(doesExist); 404 | } 405 | }, 406 | 407 | 'after execution, after the time is up': { 408 | topic: function (_, client) { 409 | var self = this; 410 | setTimeout(function () { 411 | client.exists("to-expire", self.callback); 412 | }, 3000); 413 | }, 414 | 415 | 'should evaluate the key as non-existing': function (err, doesExist) { 416 | assert.isFalse(doesExist); 417 | } 418 | } 419 | }, 420 | 421 | 'on a key with a current expiry': { 422 | topic: function (client) { 423 | client.set("already-has-expiry", "foo"); 424 | client.expire("already-has-expiry", 2); 425 | client.expire("already-has-expiry", 12, this.callback); 426 | }, 427 | "should return 0 to specify that the timeout wasn't set since the key already has an associated timeout": function (err, isTimeoutSetStatus) { 428 | assert.equal(isTimeoutSetStatus, 0); 429 | } 430 | }, 431 | 432 | 'on a non-existent key': { 433 | topic: function (client) { 434 | client.expire("non-existent-key", 2, this.callback); 435 | }, 436 | "should return 0 to specify that the key doesn't exist": function (err, status) { 437 | assert.equal(status, 0); 438 | } 439 | } 440 | }), 441 | 442 | // TODO PERSIST 443 | // TODO Allow passing a date object to EXPIREAT 444 | 'the command EXPIREAT': usingClient3({ 445 | 'on a key without a current expiry': { 446 | topic: function (client) { 447 | client.set("to-expireat", "foo"); 448 | client.expireat("to-expireat", parseInt((+new Date) / 1000, 10) + 2, this.callback); 449 | }, 450 | 451 | 'should return 1': function (err, isTimeoutSetStatus) { 452 | assert.equal(isTimeoutSetStatus, 1); 453 | }, 454 | 455 | 'after execution, before the time is up': { 456 | topic: function (_, client) { 457 | client.exists("to-expireat", this.callback); 458 | }, 459 | 460 | 'should evaluate the key as existing': function (err, doesExist) { 461 | assert.isTrue(doesExist); 462 | } 463 | }, 464 | 465 | 'after execution, after the time is up': { 466 | topic: function (_, client) { 467 | var self = this; 468 | setTimeout(function () { 469 | client.exists("to-expireat", self.callback); 470 | }, 3000); 471 | }, 472 | 473 | 'should evaluate the key as non-existing': function (err, doesExist) { 474 | assert.isFalse(doesExist); 475 | } 476 | } 477 | }, 478 | 479 | 'on a key with a current expiry': { 480 | topic: function (client) { 481 | client.set("already-has-expiryat", "foo"); 482 | client.expireat("already-has-expiryat", parseInt((+new Date) / 1000, 10) + 2); 483 | client.expireat("already-has-expiryat", parseInt((+new Date) / 1000, 10) + 12, this.callback); 484 | }, 485 | "should return 0 to specify that the timeout wasn't set since the key already has an associated timeout": function (err, isTimeoutSetStatus) { 486 | assert.equal(isTimeoutSetStatus, 0); 487 | } 488 | }, 489 | 490 | 'on a non-existent key': { 491 | topic: function (client) { 492 | client.expireat("non-existent-key", parseInt((+new Date) / 1000, 10) + 2, this.callback); 493 | }, 494 | "should return 0 to specify that the key doesn't exist": function (err, status) { 495 | assert.equal(status, 0); 496 | } 497 | } 498 | }), 499 | 500 | 'the command TTL': usingClient3({ 501 | 'for a key with no expiry': { 502 | topic: function (client) { 503 | client.set("ttl-1", "foo"); 504 | client.ttl("ttl-1", this.callback); 505 | }, 506 | 507 | 'should return -1': function (err, ttl) { 508 | assert.equal(ttl, -1); 509 | } 510 | }, 511 | 512 | 'for a key with an expiry': { 513 | topic: function (client) { 514 | client.setex("ttl-2", 2, "foo") 515 | client.ttl("ttl-2", this.callback); 516 | }, 517 | 518 | 'should return the remaining ttl in seconds': function (err, ttl) { 519 | assert.strictEqual(ttl > 0, true); 520 | } 521 | } 522 | }) 523 | }).export(module, {}); 524 | -------------------------------------------------------------------------------- /test/hash_commands.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClient = require("./utils").usingClient.gen(), 3 | assert = require("assert"), 4 | redis = require("../lib/redis"); 5 | 6 | vows.describe("Redis Hash Commands").addBatch({ 7 | 'the command HSET': usingClient({ 8 | 'on a non existing key': { 9 | topic: function (client) { 10 | client.hset("hset-1", "foo", "bar", this.callback); 11 | }, 12 | 'should return the integer reply 1': function (err, reply) { 13 | assert.equal(reply, 1); 14 | } 15 | }, 16 | 'on an existing key, non-existing field': { 17 | topic: function (client) { 18 | client.hset("hset-2", "foo", "bar"); 19 | client.hset("hset-2", "hello", "world", this.callback); 20 | }, 21 | 'should return the integer reply 1': function (err, reply) { 22 | assert.equal(reply, 1); 23 | } 24 | }, 25 | 'on an existing key, existing field': { 26 | topic: function (client) { 27 | client.hset("hset-3", "foo", "bar"); 28 | client.hset("hset-3", "foo", "foo", this.callback); 29 | }, 30 | 'should return the integer reply 0 to specify an update': function (err, reply) { 31 | assert.equal(reply, 0); 32 | } 33 | } 34 | }), 35 | 36 | 'the command HGET': usingClient({ 37 | topic: function (client) { 38 | client.hset("hset-4", "foo", "bar"); 39 | return client; 40 | }, 41 | 'if the key holds the hash with the field': { 42 | topic: function (client) { 43 | client.hget("hset-4", "foo", this.callback); 44 | }, 45 | 'should return the value': function (err, value) { 46 | assert.equal(value, "bar"); 47 | } 48 | }, 49 | 'if the key holds a hash without the field': { 50 | topic: function (client) { 51 | client.hget("hset-4", "hello", this.callback); 52 | }, 53 | 'should return null': function (err, nil) { 54 | assert.isNull(nil); 55 | } 56 | }, 57 | "if the key doesn't exist": { 58 | topic: function (client) { 59 | client.hget("non-existent-key", "foo", this.callback); 60 | }, 61 | 'should return null': function (err, nil) { 62 | assert.isNull(nil); 63 | } 64 | } 65 | }), 66 | 67 | 'the command HMGET': usingClient({ 68 | topic: function (client) { 69 | client.hset("hset-5", "foo", "bar"); 70 | client.hset("hset-5", "hello", "world"); 71 | return client; 72 | }, 73 | 74 | 'where all fields exist': { 75 | topic: function (client) { 76 | client.hmget("hset-5", "foo", "hello", this.callback); 77 | }, 78 | 'should return all the values': function (err, list) { 79 | assert.deepEqual(list, ["bar", "world"]); 80 | } 81 | }, 82 | 83 | 'where some of the specified fields do not exist': { 84 | topic: function (client) { 85 | client.hmget("hset-5", "foo", "nope", this.callback); 86 | }, 87 | 'should return nil values for non-existent fields': function (err, list) { 88 | assert.deepEqual(list, ["bar", null]); 89 | } 90 | }, 91 | 92 | "where the key doesn't exist": { 93 | topic: function (client) { 94 | client.hmget("non-existent-key", "foo", "bar", this.callback); 95 | }, 96 | 'should be treated like empty hashes': function (err, list) { 97 | assert.deepEqual(list, [null, null]); 98 | } 99 | } 100 | }), 101 | 102 | 'the command HMSET': usingClient({ 103 | topic: function (client) { 104 | client.hmset("hset-6", {foo: "bar", hello: "world"}, this.callback); 105 | }, 106 | 'should always return true (+OK) status': function (err, status) { 107 | assert.isTrue(status); 108 | } 109 | }), 110 | 111 | 'the command HINCRBY': usingClient({ 112 | "if the key doesn't exist": { 113 | topic: function (client) { 114 | client.hincrby("hset-7", "counter", 10, this.callback); 115 | }, 116 | 'should return the new value at the new hash': function (err, counter) { 117 | assert.equal(counter, 10); 118 | } 119 | }, 120 | "if the key exists but the field doesn't": { 121 | topic: function (client) { 122 | client.hset("hset-8", "name", "8th"); 123 | client.hincrby("hset-8", "counter", 20, this.callback); 124 | }, 125 | 'should return the new value at the new field': function (err, counter) { 126 | assert.equal(counter, 20); 127 | } 128 | }, 129 | 'if the key exists and the field exists': { 130 | topic: function (client) { 131 | client.hset("hset-9", "counter", 15); 132 | client.hincrby("hset-9", "counter", 20, this.callback); 133 | }, 134 | 'should return the new incremented value': function (err, counter) { 135 | assert.equal(counter, 35); 136 | } 137 | }, 138 | 'using a negative increment': { 139 | topic: function (client) { 140 | client.hset("hset-10", "counter", 15); 141 | client.hincrby("hset-10", "counter", -4, this.callback); 142 | }, 143 | 'should return the newly decremented value': function (err, counter) { 144 | assert.equal(counter, 11); 145 | } 146 | } 147 | }), 148 | 149 | 'the command HEXISTS': usingClient({ 150 | topic: function (client) { 151 | client.hset("hset-11", "foo", "bar"); 152 | return client; 153 | }, 154 | 'if the key exists and the field exists': { 155 | topic: function (client) { 156 | client.hexists("hset-11", "foo", this.callback); 157 | }, 158 | "should return the integer value 1": function (err, reply) { 159 | assert.equal(reply, 1); 160 | } 161 | }, 162 | "if the key exists and the field doesn't exist": { 163 | topic: function (client) { 164 | client.hexists("hset-11", "hello", this.callback); 165 | }, 166 | 'should return the integer value 0': function (err, reply) { 167 | assert.equal(reply, 0); 168 | } 169 | }, 170 | "if the key doesn't exist": { 171 | topic: function (client) { 172 | client.hexists("non-existent-key", "foo", this.callback); 173 | }, 174 | 'should return the integer value 0': function (err, reply) { 175 | assert.equal(reply, 0); 176 | } 177 | } 178 | }), 179 | 180 | 'the command HDEL': usingClient({ 181 | topic: function (client) { 182 | client.hset("hset-12", "foo", "bar"); 183 | return client; 184 | }, 185 | 'if the field is present in the hash': { 186 | topic: function (client) { 187 | client.hdel("hset-12", "foo", this.callback); 188 | }, 189 | 'should return the integer value 1': function (err, reply) { 190 | assert.equal(reply, 1); 191 | } 192 | }, 193 | "if the field isn't present in the hash": { 194 | topic: function (client) { 195 | client.hdel("hset-12", "hello", this.callback); 196 | }, 197 | 'should return the integer value 0': function (err, reply) { 198 | assert.equal(reply, 0); 199 | } 200 | } 201 | }), 202 | 203 | 'the command HLEN': usingClient({ 204 | topic: function (client) { 205 | client.hmset("hset-13", {foo: "bar", hello: "world"}); 206 | client.hlen("hset-13", this.callback); 207 | }, 208 | 'should return the number of fields contained by the hash': function (err, num) { 209 | assert.equal(num, 2); 210 | } 211 | }), 212 | 213 | 'the command HKEYS': usingClient({ 214 | topic: function (client) { 215 | client.hmset("hset-14", {foo: "bar", hello: "world"}); 216 | client.hkeys("hset-14", this.callback); 217 | }, 218 | 'should return the list of field names': function (err, list) { 219 | assert.deepEqual(list, ["foo", "hello"]); 220 | } 221 | }), 222 | 223 | 'the command HVALS': usingClient({ 224 | topic: function (client) { 225 | client.hmset("hset-15", {foo: "bar", hello: "world"}); 226 | client.hvals("hset-15", this.callback); 227 | }, 228 | 'should return the list of values': function (err, list) { 229 | assert.deepEqual(list, ["bar", "world"]); 230 | } 231 | }), 232 | 233 | 'the command HGETALL': usingClient({ 234 | 'on an existing hash': { 235 | topic: function (client) { 236 | client.hmset("hset-16", {foo: "bar", hello: "world"}); 237 | client.hgetall("hset-16", this.callback); 238 | }, 239 | 'should return the hash': function (err, hash) { 240 | assert.deepEqual(hash, {foo: "bar", hello: "world"}); 241 | } 242 | }, 243 | 'on a non-existent key': { 244 | topic: function (client) { 245 | client.hgetall("hset-non-existent", this.callback); 246 | }, 247 | 'should return null': function (err, hash) { 248 | assert.isNull(hash); 249 | } 250 | } 251 | }) 252 | }).export(module, {}); 253 | -------------------------------------------------------------------------------- /test/list_commands.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClient = require("./utils").usingClient.gen(), 3 | assert = require("assert"), 4 | redis = require("../lib/redis"); 5 | 6 | vows.describe("Redis List Commands").addBatch({ 7 | 'the command RPUSH': usingClient({ 8 | 'on a non-existent key': { 9 | topic: function (client) { 10 | client.rpush('list-1', 'foo', this.callback); 11 | }, 12 | 'should return 1 representing the list length after the push': function (err, length) { 13 | assert.equal(length, 1); 14 | }, 15 | 'on an existing key of type list': { 16 | topic: function (_, client) { 17 | client.rpush('list-1', 'bar', this.callback); 18 | }, 19 | 'should return 2 representing the list length after the push': function (err, length) { 20 | assert.equal(length, 2); 21 | } 22 | } 23 | }, 24 | 'on an existing key not of type list': { 25 | topic: function (client) { 26 | client.set("non-list", 5); 27 | client.rpush("non-list", "bar", this.callback); 28 | }, 29 | 30 | 'should result in an error': function (err, val) { 31 | assert.instanceOf(err, Error); 32 | assert.isUndefined(val); 33 | } 34 | } 35 | }), 36 | 37 | 'the command LPUSH': usingClient({ 38 | topic: function (client) { 39 | client.lpush('list-2', 'foo', this.callback); 40 | }, 41 | 'should return 1 representing the list length after the push': function (err, length) { 42 | assert.equal(length, 1); 43 | } 44 | }), 45 | 46 | 'the command LLEN': usingClient({ 47 | topic: function (client) { 48 | client.rpush('list-3', 1); 49 | client.rpush('list-3', 2); 50 | client.rpush('list-3', 3); 51 | client.llen('list-3', this.callback); 52 | }, 53 | 54 | 'should return the length of the list': function (err, length) { 55 | assert.equal(length, 3); 56 | } 57 | }), 58 | 59 | 'the command LRANGE': usingClient({ 60 | topic: function (client) { 61 | client.rpush('list-4', 1); 62 | client.rpush('list-4', 2); 63 | client.rpush('list-4', 3); 64 | client.rpush('list-4', 4); 65 | return client; 66 | }, 67 | 'from 0 to -1': { 68 | topic: function (client) { 69 | client.lrange('list-4', 0, -1, this.callback); 70 | }, 71 | 'should return the entire list': function (err, list) { 72 | assert.deepEqual(list, [1, 2, 3, 4]); 73 | } 74 | }, 75 | 'from 0 to 0': { 76 | topic: function (client) { 77 | client.lrange('list-4', 0, 0, this.callback); 78 | }, 79 | 'should return a 1-element list of the 1st element': function (err, list) { 80 | assert.deepEqual(list, [1]); 81 | }, 82 | }, 83 | 'from -1 to -1': { 84 | topic: function (client) { 85 | client.lrange('list-4', -1, -1, this.callback); 86 | }, 87 | 'should return a 1-element list of the last element': function (err, list) { 88 | assert.deepEqual(list, [4]); 89 | } 90 | } 91 | }), 92 | 93 | 'the command LTRIM': usingClient({ 94 | topic: function (client) { 95 | client.rpush('list-5', 1); 96 | client.rpush('list-5', 2); 97 | client.rpush('list-5', 3); 98 | client.rpush('list-5', 4); 99 | return client; 100 | }, 101 | 102 | 'from 0 to 1': { 103 | topic: function (client) { 104 | client.ltrim('list-5', 0, 1, this.callback); 105 | }, 106 | 107 | 'should return true': function (err, status) { 108 | assert.isTrue(status); 109 | }, 110 | 111 | 'after execution, reading the list': { 112 | topic: function (_, client) { 113 | client.lrange('list-5', 0, -1, this.callback); 114 | 115 | }, 116 | 'should result in the modified list': function (err, list) { 117 | assert.deepEqual(list, [1, 2]); 118 | } 119 | } 120 | } 121 | }), 122 | 123 | 'the command LINDEX': usingClient({ 124 | topic: function (client) { 125 | client.rpush('list-6', 1); 126 | client.rpush('list-6', 2); 127 | client.rpush('list-6', 3); 128 | client.rpush('list-6', 4); 129 | return client; 130 | }, 131 | 132 | 'with a positive in-range index': { 133 | topic: function (client) { 134 | client.lindex('list-6', 2, this.callback); 135 | }, 136 | 'should return the member at the specified index': function (err, val) { 137 | assert.equal(val, 3); 138 | } 139 | }, 140 | 141 | 'with a negative in-range index': { 142 | topic: function (client) { 143 | client.lindex('list-6', -3, this.callback); 144 | }, 145 | 'should return the member at the specified index': function (err, val) { 146 | assert.equal(val, 2); 147 | } 148 | }, 149 | 150 | 'with an out of range index': { 151 | topic: function (client) { 152 | client.lindex('list-6', 4, this.callback); 153 | }, 154 | 'should return the member at the specified index': function (err, val) { 155 | assert.isNull(val); 156 | } 157 | }, 158 | 159 | 'with a non-list key': { 160 | topic: function (client) { 161 | client.set("non-list-key", "hey"); 162 | client.lindex("non-list-key", 0, this.callback); 163 | }, 164 | 'should return an error': function (err, val) { 165 | assert.instanceOf(err, Error); 166 | } 167 | } 168 | }), 169 | 170 | 'the command LSET': usingClient({ 171 | topic: function (client) { 172 | client.rpush('list-7', 1); 173 | client.rpush('list-7', 2); 174 | client.rpush('list-7', 3); 175 | client.rpush('list-7', 4); 176 | return client; 177 | }, 178 | 179 | 'with a positive in-range index': { 180 | topic: function (client) { 181 | client.lset('list-7', 3, 100, this.callback); 182 | }, 183 | 'should return true': function (err, status) { 184 | assert.isTrue(status); 185 | }, 186 | 'after execution, the value of the list member at the index': { 187 | topic: function (_, client) { 188 | client.lindex('list-7', 3, this.callback); 189 | }, 190 | 'should return the new value': function (err, status) { 191 | assert.equal(status, 100); 192 | } 193 | } 194 | }, 195 | 196 | 'with a negative in-range index': { 197 | topic: function (client) { 198 | client.lset('list-7', -3, 150, this.callback); 199 | }, 200 | 'should return true': function (err, status) { 201 | assert.isTrue(status); 202 | }, 203 | 'after execution, the value of the list member at the index': { 204 | topic: function (_, client) { 205 | client.lindex('list-7', 1, this.callback); 206 | }, 207 | 'should return the new value': function (err, status) { 208 | assert.equal(status, 150); 209 | } 210 | } 211 | }, 212 | 213 | 'with an out of range index': { 214 | topic: function (client) { 215 | client.lset('list-7', 4, 200, this.callback); 216 | }, 217 | 'should generate an error': function (err, val) { 218 | assert.instanceOf(err, Error); 219 | } 220 | } 221 | }), 222 | 223 | 'the command LREM': usingClient({ 224 | topic: function (client) { 225 | client.rpush('list-8', 1); 226 | client.rpush('list-8', 2); 227 | client.rpush('list-8', 1); 228 | client.rpush('list-8', 2); 229 | client.rpush('list-8', 1); 230 | client.rpush('list-8', 2); 231 | return client; 232 | }, 233 | 234 | 'with positive count': { 235 | topic: function (client) { 236 | client.lrem("list-8", 1, 2, this.callback); 237 | }, 238 | 'should return the number of removed elements': function (err, numRemoved) { 239 | assert.equal(numRemoved, 1); 240 | }, 241 | 'after execution, when querying the new list': { 242 | topic: function (_, client) { 243 | client.lrange('list-8', 0, -1, this.callback); 244 | }, 245 | 'should read it correctly': function (err, list) { 246 | assert.deepEqual(list, [1, 1, 2, 1, 2]); 247 | } 248 | }, 249 | 250 | 'with negative count': { 251 | topic: function (_, client) { 252 | client.lrem("list-8", -1, 2, this.callback); 253 | }, 254 | 'should return the number of removed elements': function (err, numRemoved) { 255 | assert.equal(numRemoved, 1); 256 | }, 257 | 'after execution, when querying the new list': { 258 | topic: function (_, _, client) { 259 | client.lrange('list-8', 0, -1, this.callback); 260 | }, 261 | 'should read it correctly': function (err, list) { 262 | assert.deepEqual(list, [1, 1, 2, 1]); 263 | } 264 | }, 265 | 266 | 'with count 0': { 267 | topic: function (_, _, client) { 268 | client.lrem("list-8", 0, 1, this.callback); 269 | }, 270 | 'should return the number of removed elements': function (err, numRemoved) { 271 | assert.equal(numRemoved, 3); 272 | }, 273 | 'after execution, when querying the new list': { 274 | topic: function (_, _, _, client) { 275 | client.lrange('list-8', 0, -1, this.callback); 276 | }, 277 | 'should read it correctly': function (err, list) { 278 | assert.deepEqual(list, [2]); 279 | } 280 | } 281 | } 282 | } 283 | } 284 | }), 285 | 286 | 'the command LPOP': usingClient({ 287 | 'for an existing key in a non-empty list': { 288 | topic: function (client) { 289 | client.rpush('list-9', 1); 290 | client.rpush('list-9', 2); 291 | client.lpop('list-9', this.callback); 292 | }, 293 | 'should return the recently popped first element': function (err, val) { 294 | assert.equal(val, 1); 295 | } 296 | }, 297 | 'for an existing key in an empty list': { 298 | topic: function (client) { 299 | client.rpush('list-10', 1); 300 | client.lpop('list-10'); 301 | client.lpop('list-10', this.callback); 302 | }, 303 | 'should return null': function (err, val) { 304 | assert.isNull(val); 305 | } 306 | }, 307 | 'for a non-existing key': { 308 | topic: function (client) { 309 | client.lpop('non-existent-key', this.callback); 310 | }, 311 | 'should return null': function (err, val) { 312 | assert.isNull(val); 313 | } 314 | } 315 | }), 316 | 317 | 'the command RPOP': usingClient({ 318 | 'for an existing key in a non-empty list': { 319 | topic: function (client) { 320 | client.rpush('list-11', 1); 321 | client.rpush('list-11', 2); 322 | client.rpop('list-11', this.callback); 323 | }, 324 | 'should return the recently popped last element': function (err, val) { 325 | assert.equal(val, 2); 326 | } 327 | }, 328 | 'for an existing key in an empty list': { 329 | topic: function (client) { 330 | client.rpush('list-12', 1); 331 | client.rpop('list-12'); 332 | client.rpop('list-12', this.callback); 333 | }, 334 | 'should return null': function (err, val) { 335 | assert.isNull(val); 336 | } 337 | }, 338 | 'for a non-existing key': { 339 | topic: function (client) { 340 | client.rpop('non-existent-key', this.callback); 341 | }, 342 | 'should return null': function (err, val) { 343 | assert.isNull(val); 344 | } 345 | } 346 | }), 347 | 348 | // Symmetric to BRPOP 349 | 'the command BLPOP': usingClient({ 350 | 'on a non-empty list': { 351 | topic: function (client) { 352 | client.rpush("non-empty-list", "a"); 353 | client.blpop("non-empty-list", 2, this.callback); 354 | }, 355 | 'should return a 2-element array [key, popped value]': function (err, element) { 356 | assert.deepEqual(element, ["non-empty-list", "a"]); 357 | } 358 | }, 359 | 'on an empty list': { 360 | topic: function (client) { 361 | client.blpop("empty-list", 2, this.callback); 362 | }, 363 | 'should return null after the timeout': function (err, nil) { 364 | assert.isNull(nil); 365 | }, 366 | 367 | 'and then an element is pushed onto that list by another client': { 368 | topic: function (_, client) { 369 | var client2 = this.client2 = redis.createClient(); 370 | client2.select(6); 371 | client.blpop("list-to-add-1-to", 2, this.callback); 372 | client2.rpush("list-to-add-1-to", "just-in-time"); 373 | }, 374 | 'should pop off the newly pushed element and return [key, elt]': function (err, result) { 375 | assert.deepEqual(result, ["list-to-add-1-to", "just-in-time"]); 376 | }, 377 | teardown: function () { 378 | this.client2.close(); 379 | delete this.client2; 380 | } 381 | } 382 | }, 383 | 'on a series of empty and non-empty lists': { 384 | topic: function (client) { 385 | client.rpush("non-empty-list-1", "a"); 386 | client.rpush("non-empty-list-2", "b"); 387 | client.blpop("empty-list", "non-empty-list-1", "non-empty-list-2", 2, this.callback); 388 | }, 389 | 'should return a 2-element array [key, popped value] from the first non-empty key': function (err, result) { 390 | assert.deepEqual(result, ["non-empty-list-1", "a"]); 391 | } 392 | } 393 | }), 394 | 395 | // TODO Make the tests more extensive 396 | 'the command RPOPLPUSH': usingClient({ 397 | topic: function (client) { 398 | client.rpush('list-13', 1); 399 | client.rpush('list-13', 2); 400 | client.rpush('list-13', 3); 401 | client.rpush('list-13', 4); 402 | client.rpush('list-14', 100); 403 | client.rpoplpush('list-13', 'list-14', this.callback); 404 | }, 405 | 'should return the transferred value': function (err, val) { 406 | assert.equal(val, 4); 407 | }, 408 | 'after execution, when querying the source list': { 409 | topic: function (_, client) { 410 | client.lrange('list-13', 0, -1, this.callback); 411 | }, 412 | 'should read the new list': function (err, list) { 413 | assert.deepEqual(list, [1, 2, 3]); 414 | } 415 | }, 416 | 'after execution, when querying the destination list': { 417 | topic: function (_, client) { 418 | client.lrange('list-14', 0, -1, this.callback); 419 | }, 420 | 'should read the new list': function (err, list) { 421 | assert.deepEqual(list, [4, 100]); 422 | } 423 | } 424 | }) 425 | }).export(module, {}); 426 | -------------------------------------------------------------------------------- /test/pubsub.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClient = require("./utils").usingClient, 3 | assert = require("assert"), 4 | redis = require("../lib/redis"), 5 | sys = require("sys"); 6 | 7 | vows.describe("Redis PubSub Commands").addBatch({ 8 | 'publishing': usingClient.gen()({ 9 | topic: function (client) { 10 | client.publish("channel-2", "sending this to no-one", this.callback); 11 | }, 12 | 'should return the number of clients who received the message': function (err, numReceiving) { 13 | assert.equal(numReceiving, 0); 14 | } 15 | }), 16 | 17 | 'publishing to a subscribed channel': { 18 | topic: function () { 19 | var subClient = this.subClient = redis.createClient(), 20 | pubClient = this.pubClient = redis.createClient(); 21 | subClient.select(6); 22 | pubClient.select(6); 23 | subClient.subscribeTo("channel-1", this.callback); 24 | setTimeout( function () { 25 | pubClient.publish("channel-1", JSON.stringify({a: 1})); 26 | }, 1000); 27 | }, 28 | 29 | 'should send the message and channel to the subscriber': function (channel, msg) { 30 | assert.equal(channel, "channel-1"); 31 | assert.deepEqual(JSON.parse(msg), {a: 1}); 32 | }, 33 | teardown: function () { 34 | this.subClient.close(); 35 | this.pubClient.close(); 36 | delete this.subClient; 37 | delete this.pubClient; 38 | } 39 | }, 40 | 41 | 'subscribe and unsubscribe': usingClient.gen()({ 42 | 'subscribing': { 43 | topic: function (client) { 44 | client.subscribe("channel-3"); 45 | client.subscribe("channel-4", this.callback); 46 | }, 47 | 'should return [command type, channel, num channels subscribed to]': function (err, triple) { 48 | assert.deepEqual(triple, ["subscribe", "channel-4", 2]); 49 | }, 50 | 'and then unsubscribing': { 51 | topic: function (_, triple, client) { 52 | client.unsubscribe("channel-3"); 53 | client.unsubscribe("channel-4", this.callback); 54 | }, 55 | 'should return [command type, channel, num channels subscribed to]': function (err, triple) { 56 | assert.deepEqual(triple, ["unsubscribe", "channel-4", 0]); 57 | } 58 | } 59 | } 60 | }), 61 | 62 | 'psubscribe and punsubscribe': usingClient.gen()({ 63 | 'psubscribing': { 64 | topic: function (client) { 65 | client.psubscribe("channel-5.*", this.callback); 66 | }, 67 | 'should return [command type, channel, num channels subscribed to]': function (err, triple) { 68 | assert.deepEqual(triple, ["psubscribe", "channel-5.*", 1]); 69 | }, 70 | 'and then punsubscribing': { 71 | topic: function (_, triple, client) { 72 | client.punsubscribe("channel-5.*", this.callback); 73 | }, 74 | 'should return [command type, channel, num channels subscribed to]': function (err, triple) { 75 | assert.deepEqual(triple, ["punsubscribe", "channel-5.*", 0]); 76 | } 77 | } 78 | } 79 | }) 80 | }).export(module, {error: false}); 81 | -------------------------------------------------------------------------------- /test/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bnoguchi/redis-node/728b688bd4242e605ea3ff9e11ff029b1bf02162/test/sample.png -------------------------------------------------------------------------------- /test/set_commands.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClient = require("./utils").usingClient.gen(), 3 | assert = require("assert"), 4 | redis = require("../lib/redis"); 5 | 6 | vows.describe("Redis Set Commands").addBatch({ 7 | 'the command SADD': usingClient({ 8 | "adding a member to a set that doesn't contain it": { 9 | topic: function (client) { 10 | client.sadd('set-1', 1, this.callback); 11 | }, 12 | 'should return status 1': function (err, status) { 13 | assert.equal(status, 1) 14 | }, 15 | 16 | "adding a member to a set that does contain it": { 17 | topic: function (_, client) { 18 | client.sadd('set-1', 1, this.callback); 19 | }, 20 | 'should return status 0': function (err, status) { 21 | assert.equal(status, 0) 22 | } 23 | } 24 | } 25 | }), 26 | 27 | 'the command SISMEMBER': usingClient({ 28 | topic: function (client) { 29 | client.sadd('set-2', 1); 30 | return client; 31 | }, 32 | 'querying for a member': { 33 | topic: function (client) { 34 | client.sismember('set-2', 1, this.callback); 35 | }, 36 | 37 | 'should return status 1': function (err, result) { 38 | assert.equal(result, 1); 39 | } 40 | }, 41 | 'querying for a non-member': { 42 | topic: function (client) { 43 | client.sismember('set-2', 2, this.callback); 44 | }, 45 | 46 | 'should return status 0': function (err, result) { 47 | assert.equal(result, 0); 48 | } 49 | } 50 | }), 51 | 52 | 'the command SCARD': usingClient({ 53 | topic: function (client) { 54 | client.sadd('set-3', 10); 55 | client.sadd('set-3', 20); 56 | client.scard('set-3', this.callback); 57 | }, 58 | 'should return the size of the set': function (err, cardinality) { 59 | assert.equal(cardinality, 2); 60 | } 61 | }), 62 | 63 | 'the command SREM': usingClient({ 64 | topic: function (client) { 65 | client.sadd('set-4', 10); 66 | client.sadd('set-4', 20); 67 | return client; 68 | }, 69 | 'removing an existing member': { 70 | topic: function (client) { 71 | client.srem('set-4', 20, this.callback); 72 | }, 73 | 'should return with status 1': function (err, status) { 74 | assert.equal(status, 1); 75 | } 76 | }, 77 | 'attempting to remove a non-member': { 78 | topic: function (client) { 79 | client.srem('set-4', 314, this.callback); 80 | }, 81 | 'should return with status 0': function (err, status) { 82 | assert.equal(status, 0); 83 | } 84 | } 85 | }), 86 | 87 | 'the command SPOP': usingClient({ 88 | topic: function (client) { 89 | client.sadd('set-5', "cat"); 90 | client.sadd('set-5', "dog"); 91 | client.spop('set-5', this.callback); 92 | }, 93 | 'should remove a random element': function (err, val) { 94 | assert.match(val, /^(cat|dog)$/); 95 | } 96 | }), 97 | 98 | 'the command SDIFF': usingClient({ 99 | topic: function (client) { 100 | [1, 2, 3, 4, 5].forEach( function (n) { 101 | client.sadd('set-6', n); 102 | }); 103 | 104 | [4, 5, 6, 7, 8].forEach( function (n) { 105 | client.sadd('set-7', n); 106 | }); 107 | 108 | client.sdiff('set-6', 'set-7', this.callback); 109 | }, 110 | 111 | 'should return the different between the 2 sets': function (err, list) { 112 | [1, 2, 3].forEach( function (n) { 113 | assert.include(list, n.toString()); // TODO Can I get by without toString? 114 | }); 115 | } 116 | }), 117 | 118 | 'the command SDIFFSTORE': usingClient({ 119 | topic: function (client) { 120 | [1, 2, 3, 4, 5].forEach( function (n) { 121 | client.sadd('set-8', n); 122 | }); 123 | 124 | [4, 5, 6, 7, 8].forEach( function (n) { 125 | client.sadd('set-9', n); 126 | }); 127 | 128 | client.sdiffstore('set-10', 'set-8', 'set-9', this.callback); 129 | }, 130 | 'should return the number of members in the diff list': function (err, card) { 131 | assert.equal(card, 3); 132 | }, 133 | 'after executing, when querying the destination key': { 134 | topic: function (_, client) { 135 | client.smembers('set-10', this.callback); 136 | }, 137 | 'should return the correct diff': function (err, list) { 138 | [1, 2, 3].forEach( function (n) { 139 | assert.include(list, n.toString()); 140 | }); 141 | } 142 | } 143 | }), 144 | 145 | 'the command SMEMBERS': usingClient({ 146 | topic: function (client) { 147 | client.sadd('set-11', 'a'); 148 | client.sadd('set-11', 'c'); 149 | client.sadd('set-11', 'e'); 150 | client.smembers('set-11', this.callback); 151 | }, 152 | 'should return the members of the set': function (err, list) { 153 | ['a', 'c', 'e'].forEach( function (member) { 154 | assert.include(list, member); 155 | }); 156 | } 157 | }), 158 | 159 | 'the command SMOVE': usingClient({ 160 | topic: function (client) { 161 | client.sadd('set-12', 'a'); 162 | client.sadd('set-12', 'b'); 163 | client.sadd('set-12', 'c'); 164 | 165 | client.sadd('set-13', 'x'); 166 | client.sadd('set-13', 'y'); 167 | client.sadd('set-13', 'z'); 168 | 169 | return client; 170 | }, 171 | 'on a member': { 172 | topic: function (client) { 173 | client.smove('set-12', 'set-13', 'a', this.callback); 174 | }, 175 | 'should return with integer reply 1': function (err, reply) { 176 | assert.equal(reply, 1); 177 | }, 178 | 'after executing, when querying the source set': { 179 | topic: function (_, client) { 180 | client.sismember('set-12', 'a', this.callback); 181 | }, 182 | 'should specify that the moved element is not present': function (err, isPresent) { 183 | assert.equal(isPresent, 0); 184 | } 185 | }, 186 | 'after executing, when querying the destination set': { 187 | topic: function (_, client) { 188 | client.sismember('set-13', 'a', this.callback); 189 | }, 190 | 'should specify that the moved element is present': function (err, isPresent) { 191 | assert.equal(isPresent, 1); 192 | } 193 | }, 194 | 'after executing, when querying the destination set': { 195 | } 196 | }, 197 | 'on a non-member': { 198 | topic: function (client) { 199 | client.smove('set-12', 'set-13', 'r', this.callback); 200 | }, 201 | 'should return with integer reply 0': function (err, reply) { 202 | assert.equal(reply, 0); 203 | } 204 | } 205 | }), 206 | 207 | 'the command SINTER': usingClient({ 208 | topic: function (client) { 209 | [1, 2, 3, 4, 5].forEach( function (n) { 210 | client.sadd('set-14', n); 211 | }); 212 | 213 | [4, 5, 6, 7, 8].forEach( function (n) { 214 | client.sadd('set-15', n); 215 | }); 216 | 217 | [5, 100, 101, 102].forEach( function (n) { 218 | client.sadd('set-16', n); 219 | }); 220 | return client; 221 | }, 222 | 'on multiple existing keys': { 223 | topic: function (client) { 224 | client.sinter('set-14', 'set-15', 'set-16', this.callback); 225 | }, 226 | 'should return the intersection': function (err, intersection) { 227 | assert.deepEqual(intersection, ['5']); 228 | } 229 | }, 230 | 'on multiple existing keys via array': { 231 | topic: function (_, client) { 232 | client.sinter(['set-14', 'set-15', 'set-16'], this.callback); 233 | }, 234 | 'should return the intersection': function (err, intersection) { 235 | assert.deepEqual(intersection, ['5']); 236 | } 237 | } 238 | }), 239 | 240 | 'the command SINTERSTORE': usingClient({ 241 | topic: function (client) { 242 | [1, 2, 3, 4, 5].forEach( function (n) { 243 | client.sadd('set-17', n); 244 | }); 245 | 246 | [4, 5, 6, 7, 8].forEach( function (n) { 247 | client.sadd('set-18', n); 248 | }); 249 | 250 | client.sinterstore('set-19', 'set-17', 'set-18', this.callback); 251 | }, 252 | 'should return the number of members in the intersection': function (err, card) { 253 | assert.equal(card, 2); 254 | }, 255 | 'after executing, when querying the destination key': { 256 | topic: function (_, client) { 257 | client.smembers('set-19', this.callback); 258 | }, 259 | 'should return the correct intersection': function (err, list) { 260 | [4, 5].forEach( function (n) { 261 | assert.include(list, n.toString()); 262 | }); 263 | } 264 | } 265 | }), 266 | 267 | 'the command SUNION': usingClient({ 268 | topic: function (client) { 269 | [1, 2, 3, 4, 5].forEach( function (n) { 270 | client.sadd('set-20', n); 271 | }); 272 | 273 | [4, 5, 6, 7, 8].forEach( function (n) { 274 | client.sadd('set-21', n); 275 | }); 276 | 277 | [5, 100, 101, 102].forEach( function (n) { 278 | client.sadd('set-22', n); 279 | }); 280 | return client; 281 | }, 282 | 'on multiple existing keys': { 283 | topic: function (client) { 284 | client.sunion('set-20', 'set-21', 'set-22', this.callback); 285 | }, 286 | 'should return the union': function (err, intersection) { 287 | assert.length(intersection, 11); 288 | [1, 2, 3, 4, 5, 6, 7, 8, 100, 101, 102].forEach( function (n) { 289 | assert.include(intersection, n.toString()); 290 | }); 291 | } 292 | }, 293 | 'on multiple existing keys via array': { 294 | topic: function (_, client) { 295 | client.sunion(['set-20', 'set-21', 'set-22'], this.callback); 296 | }, 297 | 'should return the union': function (err, intersection) { 298 | assert.length(intersection, 11); 299 | [1, 2, 3, 4, 5, 6, 7, 8, 100, 101, 102].forEach( function (n) { 300 | assert.include(intersection, n.toString()); 301 | }); 302 | } 303 | } 304 | }), 305 | 306 | 'the command SUNIONSTORE': usingClient({ 307 | topic: function (client) { 308 | [1, 2, 3, 4, 5].forEach( function (n) { 309 | client.sadd('set-23', n); 310 | }); 311 | 312 | [4, 5, 6, 7, 8].forEach( function (n) { 313 | client.sadd('set-24', n); 314 | }); 315 | 316 | client.sunionstore('set-25', 'set-23', 'set-24', this.callback); 317 | }, 318 | 'should return the number of members in the union': function (err, card) { 319 | assert.equal(card, 8); 320 | }, 321 | 'after executing, when querying the destination key': { 322 | topic: function (_, client) { 323 | client.smembers('set-25', this.callback); 324 | }, 325 | 'should return the correct union': function (err, list) { 326 | [1, 2, 3, 4, 5, 6, 7, 8].forEach( function (n) { 327 | assert.include(list, n.toString()); 328 | }); 329 | } 330 | } 331 | }), 332 | 333 | 'the command SRANDMEMBER': usingClient({ 334 | 'on a non-empty set': { 335 | topic: function (client) { 336 | ['a', 'b', 'c'].forEach( function (l) { 337 | client.sadd('set-to-rand', l); 338 | }); 339 | client.srandmember('set-to-rand', this.callback); 340 | }, 341 | 'should return a random member of the set': function (err, member) { 342 | assert.match(member, /^(a|b|c)$/); 343 | } 344 | }, 345 | 'on an empty set': { 346 | topic: function (client) { 347 | client.sadd('empty-set', 'a'); 348 | client.spop('empty-set'); 349 | client.srandmember('empty-set', this.callback); 350 | }, 351 | 'should return null': function (err, nil) { 352 | assert.isNull(nil); 353 | } 354 | }, 355 | 'on a non-existent key': { 356 | topic: function (client) { 357 | client.srandmember('non-existing-key', this.callback); 358 | }, 359 | 'should return null': function (err, nil) { 360 | assert.isNull(nil); 361 | } 362 | } 363 | }), 364 | }).export(module, {}); 365 | -------------------------------------------------------------------------------- /test/sort_command.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClient = require("./utils").usingClient.gen(), 3 | assert = require("assert"), 4 | redis = require("../lib/redis"); 5 | 6 | vows.describe("Redis Sort Commands").addBatch({ 7 | 'the command SORT': usingClient({ 8 | 'on a list of strings': { 9 | topic: function (client) { 10 | client.rpush("sortable-string-list", "a"); 11 | client.rpush("sortable-string-list", "b"); 12 | client.rpush("sortable-string-list", "c"); 13 | return client; 14 | }, 15 | 'sorting in ascending order': { 16 | topic: function (client) { 17 | client.sort('sortable-string-list', {order: 'asc', alpha: true}, this.callback); 18 | }, 19 | 'should return a list sorted in ascending alphabetic order': function (err, list) { 20 | assert.deepEqual(list, ["a", "b", "c"]); 21 | } 22 | }, 23 | 'sorting in descending order': { 24 | topic: function (client) { 25 | client.sort('sortable-string-list', {order: 'desc', alpha: true}, this.callback); 26 | }, 27 | 'should return a list sorted in descending alphabetic order': function (err, list) { 28 | assert.deepEqual(list, ["c", "b", "a"]); 29 | } 30 | } 31 | }, 32 | 'on a list of numbers': { 33 | topic: function (client) { 34 | client.rpush('sortable-number-list', 1); 35 | client.rpush('sortable-number-list', 2); 36 | client.rpush('sortable-number-list', 3); 37 | return client; 38 | }, 39 | 'sorting in ascending order': { 40 | topic: function (client) { 41 | client.sort('sortable-number-list', {order: 'asc'}, this.callback); 42 | }, 43 | 'should return a list sorted in ascending numeric order': function (err, list) { 44 | assert.deepEqual(list, [1, 2, 3]); 45 | } 46 | }, 47 | 'sorting in descending order': { 48 | topic: function (client) { 49 | client.sort('sortable-number-list', {order: 'desc'}, this.callback); 50 | }, 51 | 'should return a list sorted in descending numeric order': function (err, list) { 52 | assert.deepEqual(list, [3, 2, 1]); 53 | } 54 | }, 55 | 56 | 'using external weights with the BY pattern': { 57 | topic: function (client) { 58 | client.set('weight1', 2); 59 | client.set('weight2', 3); 60 | client.set('weight3', 1); 61 | return client; 62 | }, 63 | 'sorting in ascending order by weight': { 64 | topic: function (client) { 65 | client.sort('sortable-number-list', {order: 'asc', by: 'weight*'}, this.callback); 66 | }, 67 | 'should return a list sorted in ascending weighted order': function (err, list) { 68 | assert.deepEqual(list, [3, 1, 2]); 69 | } 70 | }, 71 | 'sorting in descending order by weight': { 72 | topic: function (client) { 73 | client.sort('sortable-number-list', {order: 'desc', by: 'weight*'}, this.callback); 74 | }, 75 | 'should return a list sorted in descending weighted order': function (err, list) { 76 | assert.deepEqual(list, [2, 1, 3]); 77 | } 78 | }, 79 | 80 | 'using the GET pattern': { 81 | topic: function (client) { 82 | client.set('deref1', 'a'); 83 | client.set('deref2', 'b'); 84 | client.set('deref3', 'c'); 85 | client.set('2deref1', 'd'); 86 | client.set('2deref2', 'e'); 87 | client.set('2deref3', 'f'); 88 | return client; 89 | }, 90 | 'sorting in ascending order by weight with a GET pattern': { 91 | topic: function (client) { 92 | client.sort('sortable-number-list', {order: 'asc', by: 'weight*', get: 'deref*'}, this.callback); 93 | }, 94 | 'should return a list of dereferenced values sorted in ascending weight order': function (err, list) { 95 | assert.deepEqual(list, ['c', 'a', 'b']); 96 | } 97 | }, 98 | 'sorting in descending order by weight with a GET pattern': { 99 | topic: function (client) { 100 | client.sort('sortable-number-list', {order: 'desc', by: 'weight*', get: 'deref*'}, this.callback); 101 | }, 102 | 'should return a list of dereferenced values sorted in descending weight order': function (err, list) { 103 | assert.deepEqual(list, ['b', 'a', 'c']); 104 | } 105 | }, 106 | 107 | 'sorting in ascending order by weight with 2 GET patterns': { 108 | topic: function (client) { 109 | client.sort('sortable-number-list', {order: 'asc', by: 'weight*', get: ['deref*', '2deref*']}, this.callback); 110 | }, 111 | 'should return a list of dereferenced values sorted in ascending weight order': function (err, list) { 112 | assert.deepEqual(list, ['c', 'f', 'a', 'd', 'b', 'e']); 113 | } 114 | }, 115 | 116 | 'storing the resulting list': { 117 | topic: function (client) { 118 | client.sort('sortable-number-list', {order: 'asc', by: 'weight*', get: 'deref*', store: 'write-to-from-sort-key'}, this.callback); 119 | }, 120 | 'should return an integer result': function (err, result) { 121 | assert.equal(result, 3); 122 | }, 123 | 124 | 'after executing, attempting to query the destination key': { 125 | topic: function (_, client) { 126 | client.lrange('write-to-from-sort-key', 0, -1, this.callback); 127 | }, 128 | 'should read the proper list': function (err, list) { 129 | assert.deepEqual(list, ['c', 'a', 'b']); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | }) 137 | }).export(module, {}); 138 | -------------------------------------------------------------------------------- /test/string_commands.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClient = require("./utils").usingClient.gen(), 3 | usingClient2 = require("./utils").usingClient.gen(), 4 | assert = require("assert"), 5 | redis = require("../lib/redis"), 6 | fs = require("fs"), 7 | Buffer = require("buffer").Buffer; 8 | 9 | vows.describe("Redis String Commands").addBatch({ 10 | 'the command SET': usingClient({ 11 | 'with proper syntax': { 12 | topic: function (client) { 13 | client.set("foo", "bar", this.callback); 14 | }, 15 | 16 | 'should return true': function (err, result) { 17 | assert.equal(result, true); 18 | } 19 | } 20 | }), 21 | 22 | 'the command GET': usingClient({ 23 | 'when no such key exists': { 24 | topic: function (client) { 25 | client.get("non-existent", this.callback); 26 | }, 27 | 28 | 'should return null': function (err, result) { 29 | assert.equal(result, null); 30 | } 31 | }, 32 | 33 | 'when already set': { 34 | topic: function (client) { 35 | client.set("already-set", "yes"); 36 | client.get("already-set", this.callback); 37 | }, 38 | 39 | 'should return the set value': function (err, result) { 40 | assert.equal(result, "yes"); 41 | } 42 | } 43 | }), 44 | 45 | 'setting and getting special characters': usingClient({ 46 | topic: function (client) { 47 | var specialJson = JSON.stringify({'a': 'ö'}); 48 | client.set("special-json", specialJson); 49 | client.get("special-json", this.callback); 50 | }, 51 | 52 | 'should return the same special characters': function (err, result) { 53 | var specialJson = JSON.stringify({'a': 'ö'}); 54 | console.log(specialJson); 55 | console.log(specialJson.length); 56 | assert.equal(result, specialJson); 57 | } 58 | }), 59 | 60 | 'setting and getting multiple bytes': usingClient({ 61 | topic: function (client) { 62 | var testValue = '\u00F6\u65E5\u672C\u8A9E', // ö日本語 63 | buffer = new Buffer(32), 64 | size = buffer.write(testValue,0, "utf8"); 65 | client.set("utf8-key", buffer.slice(0, size)); 66 | client.get("utf8-key", this.callback); 67 | }, 68 | 69 | 'should return the utf8 value of the buffer': function (err, result) { 70 | var testValue = '\u00F6\u65E5\u672C\u8A9E'; // ö日本語 71 | assert.equal(result, testValue); 72 | } 73 | }), 74 | 75 | 'setting large blobs': usingClient({ 76 | topic: function (client) { 77 | var self = this; 78 | fs.readFile(__filename, function (err, fileContents) { // no encoding = Buffer 79 | client.set("largetestfile", fileContents, self.callback); 80 | }); 81 | }, 82 | 'should return with a true (+OK) status': function (err, status) { 83 | assert.isTrue(status); 84 | }, 85 | 'and getting large blobs too': { 86 | topic: function (_, client) { 87 | client.get("largetestfile", this.callback); 88 | }, 89 | 'should return the entire large blob back': function (err, str) { 90 | fs.readFile(__filename, function (err, fileContents) { 91 | assert.equal(str, fileContents); 92 | }); 93 | } 94 | } 95 | }), 96 | 97 | // This test is borrowed and adapted from one in node-redis-client 98 | // To test binary safe values 99 | 'setting an image': usingClient({ 100 | topic: function (client) { 101 | var paths = [ "sample.png", "test/sample.png" ], 102 | path = paths.shift(); 103 | while (true) { 104 | try { 105 | var fileContents = fs.readFileSync(path, 'binary'); 106 | break; 107 | } catch (e) { 108 | path = paths.shift(); 109 | } 110 | } 111 | var imageBuffer = new Buffer(Buffer.byteLength(fileContents, 'binary')); 112 | imageBuffer.write(fileContents, 0, "binary"); 113 | 114 | client.set('png_image', imageBuffer, this.callback); 115 | }, 116 | 'should return a true (+OK) status': function (err, status) { 117 | assert.isTrue(status); 118 | }, 119 | 'and then getting the image, too': { 120 | topic: function (_, client) { 121 | client.get('png_image', {encoding: "binary"}, this.callback); 122 | }, 123 | 'should return the entire image': function (err, value) { 124 | var paths = [ "sample.png", "test/sample.png" ], 125 | path = paths.shift(); 126 | while (true) { 127 | try { 128 | var fileContents = fs.readFileSync(path, 'binary'); 129 | break; 130 | } catch (e) { 131 | path = paths.shift(); 132 | } 133 | } 134 | var imageBuffer = new Buffer(Buffer.byteLength(fileContents, 'binary')); 135 | imageBuffer.write(fileContents, 0, "binary"); 136 | 137 | // TODO Make value binary a Buffer vs the current binary string? 138 | assert.equal(value, imageBuffer.toString("binary")); 139 | } 140 | } 141 | }), 142 | 143 | 'the command GETSET': usingClient({ 144 | topic: function (client) { 145 | client.set("getset-key", "getset-from"); 146 | client.getset("getset-key", "getset-to", this.callback); 147 | }, 148 | 149 | 'should return the previous value': function (err, prevValue) { 150 | assert.equal(prevValue, "getset-from"); 151 | }, 152 | 153 | 'the new value': { 154 | topic: function (_, client) { 155 | client.get("getset-key", this.callback); 156 | }, 157 | 158 | 'should return the newly set value': function (err, newValue) { 159 | assert.equal(newValue, "getset-to"); 160 | } 161 | } 162 | }), 163 | 164 | 'the command MGET': usingClient({ 165 | 'retrieving multiple keys': { 166 | topic: function (client) { 167 | client.set("mget1", "a"); 168 | client.set("mget2", "b"); 169 | client.mget("mget1", "mget2", this.callback); 170 | }, 171 | 172 | 'should return the values': function (err, result) { 173 | assert.equal(result[0], "a"); 174 | assert.equal(result[1], "b"); 175 | } 176 | } 177 | 178 | }), 179 | 180 | 'the command SETNX': usingClient({ 181 | 'when no such key exists': { 182 | topic: function (client) { 183 | client.setnx("bar", "foo", this.callback); 184 | }, 185 | 186 | 'should succeed with 1': function (err, result) { 187 | assert.equal(result, 1); 188 | } 189 | }, 190 | 191 | 'when already set': { 192 | topic: function (client) { 193 | client.set("ack", "bar"); 194 | client.setnx("ack", "notbar", this.callback); 195 | }, 196 | 197 | 'should fail with 0': function (err, result) { 198 | assert.equal(result, 0); 199 | } 200 | } 201 | }), 202 | 203 | 'the command MSET': usingClient({ 204 | topic: function (client) { 205 | client.mset('mset-a', 1, 'mset-b', 2, 'mset-c', 3, 'mset-d', 4, 'mset-e', 5, this.callback); 206 | }, 207 | 'should return with status code true (+OK)': function (err, status) { 208 | assert.isTrue(status); 209 | } 210 | }), 211 | 212 | 'the command MSETNX': usingClient({ 213 | 'successfully': { 214 | topic: function (client) { 215 | client.msetnx('msetnx-a', 1, 'msetnx-b', 2, 'msetnx-c', 3, 'msetnx-d', 4, 'msetnx-e', 5, this.callback); 216 | }, 217 | 'should return 1 to indicate that all keys were set': function (err, result) { 218 | assert.equal(result, 1); 219 | } 220 | }, 221 | 'unsuccessfully': { 222 | topic: function (client) { 223 | client.set("msetnx-f", 6); 224 | client.msetnx("msetnx-f", 7, "msetnx-g", 7, this.callback); 225 | }, 226 | 'should return 0 to indicate that no key was set because at least 1 key already existed': function (err, result) { 227 | assert.equal(result, 0); 228 | } 229 | } 230 | }), 231 | 232 | 'the command INCR': usingClient({ 233 | 'incrementing an undefined key': { 234 | topic: function (client) { 235 | client.incr("counter", this.callback); 236 | }, 237 | 238 | 'should return 1': function (err, value) { 239 | assert.equal(value, 1); 240 | }, 241 | 242 | 'incrementing a defined key with value 1': { 243 | topic: function (_, client) { 244 | client.incr("counter", this.callback); 245 | }, 246 | 247 | 'should return 2': function (err, value) { 248 | assert.equal(value, 2); 249 | } 250 | } 251 | } 252 | }), 253 | 254 | 'the command INCRBY': usingClient({ 255 | 'incrementing 1 by 2': { 256 | topic: function (client) { 257 | client.incr("incrby-key"); 258 | client.incrby("incrby-key", 2, this.callback); 259 | }, 260 | 261 | 'should return 3': function (err, value) { 262 | assert.equal(value, 3); 263 | }, 264 | 265 | 'incrementing 3 by -1': { 266 | topic: function (_, client) { 267 | client.incrby("incrby-key", -1, this.callback); 268 | }, 269 | 270 | 'should return 2': function (err, value) { 271 | assert.equal(value, 2); 272 | } 273 | } 274 | } 275 | }), 276 | 277 | 'the command DECR': usingClient({ 278 | 'decrementing an undefined key': { 279 | topic: function (client) { 280 | client.decr("decr-key", this.callback); 281 | }, 282 | 283 | 'should return -1': function (err, value) { 284 | assert.equal(value, -1); 285 | }, 286 | 287 | 'decrementing a defined key with value -1': { 288 | topic: function (_, client) { 289 | client.decr("decr-key", this.callback); 290 | }, 291 | 292 | 'should return -2': function (err, value) { 293 | assert.equal(value, -2); 294 | } 295 | } 296 | } 297 | }), 298 | 299 | 'the command DECRBY': usingClient({ 300 | 'decrementing 10 by 4': { 301 | topic: function (client) { 302 | client.incrby("decrby-key", 10); 303 | client.decrby("decrby-key", 4, this.callback); 304 | }, 305 | 306 | 'should return 6': function (err, value) { 307 | assert.equal(value, 6); 308 | }, 309 | 310 | 'decrementing 6 by -3': { 311 | topic: function (_, client) { 312 | client.decrby("decrby-key", -3, this.callback); 313 | }, 314 | 315 | 'should return 9': function (err, value) { 316 | assert.equal(value, 9); 317 | } 318 | } 319 | } 320 | }), 321 | 322 | 'the command APPEND': usingClient({ 323 | 'for an existing key': { 324 | topic: function (client) { 325 | client.set("appendable-key", "the cat in the"); 326 | client.append("appendable-key", " hat", this.callback); 327 | }, 328 | 'should return the new string length': function (err, length) { 329 | assert.equal(length, "the cat in the hat".length); 330 | }, 331 | 'after executing, when querying the new string': { 332 | topic: function (_, client) { 333 | client.get('appendable-key', this.callback); 334 | }, 335 | 'should return the new string': function (err, str) { 336 | assert.equal(str, "the cat in the hat"); 337 | } 338 | } 339 | }, 340 | 341 | 'for a non existing key': { 342 | topic: function (client) { 343 | client.append('unwritten-appendable-key', "hey", this.callback); 344 | }, 345 | 'should return the length of the suffix': function (err, str) { 346 | assert.equal(str, "hey".length); 347 | }, 348 | 'after executing, when querying the new string': { 349 | topic: function (_, client) { 350 | client.get('unwritten-appendable-key', this.callback); 351 | }, 352 | 'should return the new string': function (err, str) { 353 | assert.equal(str, "hey"); 354 | } 355 | } 356 | } 357 | }), 358 | 359 | 'the command SUBSTR': usingClient({ 360 | topic: function (client) { 361 | client.set("key-to-substr", "whenever"); 362 | client.substr("key-to-substr", 4, 7, this.callback); 363 | }, 364 | 'should return the appropriate substring': function (err, substring) { 365 | assert.equal(substring, "ever"); 366 | } 367 | }) 368 | }).addBatch({ 369 | 'the command SETEX': usingClient2({ 370 | topic: function (client) { 371 | client.setex("to-expire-1", 2, "foo", this.callback); 372 | }, 373 | 374 | 'should return true': function (err, status) { 375 | assert.isTrue(status); 376 | }, 377 | 378 | 'after execution, before the time is up': { 379 | topic: function (_, client) { 380 | client.exists("to-expire-1", this.callback); 381 | }, 382 | 383 | 'should evaluate the key as existing': function (err, doesExist) { 384 | assert.isTrue(doesExist); 385 | } 386 | }, 387 | 388 | 'after execution, after the time is up': { 389 | topic: function (_, client) { 390 | var self = this; 391 | setTimeout(function () { 392 | client.exists("to-expire-1", self.callback); 393 | }, 3000); 394 | }, 395 | 396 | 'should evaluate the key as non-existing': function (err, doesExist) { 397 | assert.isFalse(doesExist); 398 | } 399 | } 400 | }) 401 | }).export(module, {}); 402 | -------------------------------------------------------------------------------- /test/transactions.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClient = require("./utils").usingClient.gen(), 3 | assert = require("assert"), 4 | redis = require("../lib/redis"); 5 | var sys = require("sys"); 6 | 7 | vows.describe("Redis Transactions").addBatch({ 8 | 'with proper syntax': usingClient({ 9 | topic: function (client) { 10 | var simultClient = this.simultClient = redis.createClient(); 11 | simultClient.select(6); 12 | var self = this; 13 | client.transaction( function () { 14 | client.rpush("txn1", 1); 15 | client.rpush("txn1", 2); 16 | client.rpush("txn1", 3, self.callback); 17 | }); 18 | }, 19 | 'should result in changes': function (err, count) { 20 | assert.equal(count, 3); 21 | }, 22 | teardown: function () { 23 | this.simultClient.close(); 24 | delete this.simultClient; 25 | } 26 | }), 27 | 'with proper syntax with multibulk': usingClient({ 28 | topic: function (client) { 29 | var self = this; 30 | client.rpush("txn1-a", 1); 31 | client.rpush("txn1-a", 2); 32 | client.rpush("txn1-a", 3); 33 | client.transaction( function () { 34 | client.lrange("txn1-a", 0, -1, self.callback); 35 | }); 36 | }, 37 | 'should return the correct list': function (err, list) { 38 | assert.deepEqual(list, [1,2,3]); 39 | } 40 | }), 41 | 'with commands that require special reply interpretation': usingClient({ 42 | topic: function (client) { 43 | var self = this; 44 | client.zadd("txn1-b", 1, 1); 45 | client.zadd("txn1-b", 2, 2); 46 | client.zadd("txn1-b", 3, 3); 47 | client.transaction( function () { 48 | client.zrange("txn1-b", 0, -1, self.callback); 49 | }); 50 | }, 51 | 'should return the correct result': function (err, list) { 52 | assert.deepEqual(list, [1, 2, 3]); 53 | } 54 | }), 55 | 'with hgetall': usingClient({ 56 | topic: function (client) { 57 | var self = this; 58 | client.hmset("txn1-c", {a: 1, b: 2}); 59 | client.transaction( function () { 60 | client.hget("txn1-c", "a"); 61 | client.hget("txn1-c", "b"); 62 | client.hgetall("txn1-c", self.callback); 63 | }); 64 | }, 65 | 'should transform the result just as it would outside a transaction': function (err, hash) { 66 | assert.deepEqual(hash, {a: 1, b: 2}); 67 | } 68 | }), 69 | 'nested': usingClient({ 70 | topic: function (client) { 71 | var self = this; 72 | client.transaction( function () { 73 | client.rpush("nested-txn", "a"); 74 | client.transaction( function () { 75 | client.rpush("nested-txn", "b"); 76 | }); 77 | client.rpush("nested-txn", "c"); 78 | client.rpush("nested-txn", "d", self.callback); 79 | }); 80 | }, 81 | 'should result in changes': function (err, count) { 82 | assert.equal(count, 4); 83 | } 84 | }), 85 | 'with improper syntax': usingClient({ 86 | topic: function (client) { 87 | client.transaction( function () { 88 | client.rpush("txn-invalid", 1); 89 | client.rpush("txn-invalid", 2); 90 | client.rpush("txn-invalid"); 91 | // simultClient.rpush("txn", 4, function (err, count) { 92 | // if (err) throw new Error(err); 93 | // checkEqual(count, 4, "Commands from other clients should fire after a transaction from a competing client"); 94 | // }); 95 | }); 96 | client.exists("txn-invalid", this.callback); 97 | }, 98 | // Atomicity 99 | 'should roll back the transaction': function (err, result) { 100 | assert.equal(result, 0); 101 | } 102 | // TODO Should throw an error to notify user of failed transaction 103 | }) 104 | }).export(module, {}); 105 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | var redis = require("../lib/redis"); 2 | 3 | 4 | var usingClient = exports.usingClient = function (client, subContexts) { 5 | return function (subContexts) { 6 | function setupClient () { 7 | client.select(6); 8 | client.remainingTests++; 9 | return client; 10 | } 11 | function teardown () { 12 | if (--client.remainingTests === 0) { 13 | client.close(); 14 | } 15 | } 16 | var context = {topic: setupClient, teardown: teardown}; 17 | var currSubContext; 18 | if (subContexts.hasOwnProperty("topic")) { 19 | context[""] = subContexts; 20 | } else { 21 | for (var subContextName in subContexts) { 22 | context[subContextName] = subContexts[subContextName]; 23 | } 24 | } 25 | return context; 26 | }; 27 | }; 28 | 29 | usingClient.gen = function (subContexts) { 30 | var client = redis.createClient(); 31 | client.select(6); 32 | client.flushdb(); 33 | client.remainingTests = 0; 34 | return usingClient(client, subContexts); 35 | }; 36 | -------------------------------------------------------------------------------- /test/vows.js: -------------------------------------------------------------------------------- 1 | // TODO Allow passing of arrays back to mget 2 | // TODO Allow passing of arrays to mset 3 | // TODO Test sort with STORE option 4 | 5 | var sys = require("sys"), 6 | vows = require("vows"), 7 | assert = require("assert"), 8 | redis = require("../lib/redis"), 9 | fs = require("fs"), 10 | ReplyStream = require("../lib/replyStream"), 11 | Buffer = require("buffer").Buffer, 12 | usingClient = require("./utils").usingClient; 13 | 14 | vows.describe("Redis").addBatch({ 15 | 'the command INFO': usingClient({ 16 | topic: function (client) { 17 | client.info(this.callback); 18 | }, 19 | 20 | 'should return the information as a hash': function (err, info) { 21 | assert.isObject(info); 22 | ['redis_version', 'redis_git_sha1', 'redis_git_dirty', 'arch_bits', 'multiplexing_api', 'process_id', 'uptime_in_seconds', 'uptime_in_days', 'connected_clients', 'connected_slaves', 'blocked_clients', 'used_memory', 'used_memory_human', 'changes_since_last_save', 'bgsave_in_progress', 'last_save_time', 'bgrewriteaof_in_progress', 'total_connections_received', 'total_commands_processed', 'expired_keys', 'hash_max_zipmap_entries', 'hash_max_zipmap_value', 'pubsub_channels', 'pubsub_patterns', 'vm_enabled', 'role'].forEach( function (key) { 23 | assert.include(info, key); 24 | assert.isString(info[key]); 25 | }); 26 | } 27 | }), 28 | }).addBatch({ 29 | // TODO Test BGSAVE 30 | 'the command SAVE': usingClient({ 31 | topic: function (client) { 32 | client.save(this.callback); 33 | }, 34 | 'should return a true status': function (err, status) { 35 | assert.isTrue(status); 36 | }, 37 | 'the command LASTSAVE': { 38 | topic: function (_, client) { 39 | client.lastsave(this.callback); 40 | }, 41 | 'should return the integer unix timestamp of the last successful save': function (err, timestamp) { 42 | assert.deepEqual(timestamp > 0, true); 43 | } 44 | } 45 | }) 46 | }).export(module, {}); 47 | -------------------------------------------------------------------------------- /test/zset_commands.vows.js: -------------------------------------------------------------------------------- 1 | var vows = require("vows"), 2 | usingClient = require("./utils").usingClient.gen(), 3 | assert = require("assert"), 4 | redis = require("../lib/redis"); 5 | 6 | vows.describe("Redis Sorted Set Commands").addBatch({ 7 | 'the command ZADD': usingClient({ 8 | "on a non-existent key": { 9 | topic: function (client) { 10 | client.zadd("zset-1", 1, "a", this.callback); 11 | }, 12 | 'should return 1 to specify that the element was added': function (err, reply) { 13 | assert.equal(reply, 1); 14 | } 15 | }, 16 | "to a zset that doesn't contain the member": { 17 | topic: function (client) { 18 | client.zadd("zset-2", 1, "a"); 19 | client.zadd("zset-2", 2, "b", this.callback); 20 | }, 21 | 'should return 1 to specify that the element was added': function (err, reply) { 22 | assert.equal(reply, 1); 23 | } 24 | }, 25 | "to a zset that already contains the member": { 26 | topic: function (client) { 27 | client.zadd("zset-3", 1, "a"); 28 | client.zadd("zset-3", 2, "a", this.callback); 29 | }, 30 | 'should return 0 to specify that the score was updated': function (err, reply) { 31 | assert.equal(reply, 0); 32 | } 33 | }, 34 | "to a non-zset key": { 35 | topic: function (client) { 36 | client.rpush("non-zset-key", "a"); 37 | client.zadd("non-zset-key", 2, "b", this.callback); 38 | }, 39 | 'should return an error': function (err, reply) { 40 | assert.instanceOf(err, Error); 41 | } 42 | } 43 | }), 44 | 45 | 'the command ZREM': usingClient({ 46 | 'on a zset that contains the key': { 47 | topic: function (client) { 48 | client.zadd("zset-4", 1, "a"); 49 | client.zrem("zset-4", "a", this.callback); 50 | }, 51 | 'should return integer reply 1 to specify removal': function (err, reply) { 52 | assert.equal(reply, 1); 53 | } 54 | }, 55 | "on a zset that doesn't contain the key": { 56 | topic: function (client) { 57 | client.zadd("zset-5", 1, "a"); 58 | client.zrem("zset-5", "b", this.callback); 59 | }, 60 | 'should return integer reply 0 to specify non-membership': function (err, reply) { 61 | assert.equal(reply, 0); 62 | } 63 | }, 64 | 'on a non-zset key': { 65 | topic: function (client) { 66 | client.rpush("non-zset-key-2", "a"); 67 | client.zrem("non-zset-key-2", "a", this.callback); 68 | }, 69 | 'should return an error': function (err, reply) { 70 | assert.instanceOf(err, Error); 71 | } 72 | } 73 | }), 74 | 75 | 'the command ZINCRBY': usingClient({ 76 | 'on a zset that contains the member': { 77 | topic: function (client) { 78 | client.zadd("zset-6", 1, "a"); 79 | client.zincrby("zset-6", 3, "a", this.callback); 80 | }, 81 | 'should return the new score as a double prec float': function (err, score) { 82 | assert.equal(score, 4.0); 83 | } 84 | }, 85 | "on a zset that doesn't contain the member": { 86 | topic: function (client) { 87 | client.zadd("zset-7", 1, "a"); 88 | client.zincrby("zset-7", 3.2, "b", this.callback); 89 | }, 90 | 'should return the score of the added member': function (err, score) { 91 | assert.equal(score, 3.2); 92 | } 93 | }, 94 | 'on a non-existing key': { 95 | topic: function (client) { 96 | client.zincrby("zset-8", 3.18, "z", this.callback); 97 | }, 98 | 'should return the score of the added member of the added zset': function (err, score) { 99 | assert.equal(score, 3.18); 100 | } 101 | }, 102 | 'decrementing a score': { 103 | topic: function (client) { 104 | client.zadd("zset-9", 5, "a"); 105 | client.zincrby("zset-9", -3, "a", this.callback); 106 | }, 107 | 'should return the new score': function (err, score) { 108 | assert.equal(score, 2); 109 | } 110 | } 111 | }), 112 | 113 | 'the command ZRANK': usingClient({ 114 | topic: function (client) { 115 | client.zadd("zset-10", 10, "a"); 116 | client.zadd("zset-10", 20, "b"); 117 | client.zadd("zset-10", 30, "c"); 118 | return client; 119 | }, 120 | 'for an element in the zset': { 121 | topic: function (client) { 122 | client.zrank("zset-10", "c", this.callback); 123 | }, 124 | 'should return the rank': function (err, rank) { 125 | assert.equal(rank, 2); // rank starts from 0-based index 126 | } 127 | }, 128 | 'for a non-existing element in the zset': { 129 | topic: function (client) { 130 | client.zrank("zset-10", "d", this.callback); 131 | }, 132 | 'should return null': function (err, nil) { 133 | assert.isNull(nil); 134 | } 135 | } 136 | }), 137 | 138 | 'the command ZREVANK': usingClient({ 139 | topic: function (client) { 140 | client.zadd("zset-11", 10, "a"); 141 | client.zadd("zset-11", 20, "b"); 142 | client.zadd("zset-11", 30, "c"); 143 | return client; 144 | }, 145 | 'for an element in the zset': { 146 | topic: function (client) { 147 | client.zrevrank("zset-11", "c", this.callback); 148 | }, 149 | 'should return the rank': function (err, rank) { 150 | assert.equal(rank, 0); // rank starts from 0-based index 151 | } 152 | }, 153 | 'for a non-existing element in the zset': { 154 | topic: function (client) { 155 | client.zrevrank("zset-11", "d", this.callback); 156 | }, 157 | 'should return null': function (err, nil) { 158 | assert.isNull(nil); 159 | } 160 | } 161 | }), 162 | 163 | // Symmetric with ZREVRANGE 164 | 'the command ZRANGE': usingClient({ 165 | topic: function (client) { 166 | client.zadd("zset-12", 10, "a"); 167 | client.zadd("zset-12", 20, "b"); 168 | client.zadd("zset-12", 30, "c"); 169 | client.zadd("zset-12", 40, "d"); 170 | return client; 171 | }, 172 | 173 | 'with in-range indexes': { 174 | topic: function (client) { 175 | client.zrange("zset-12", 0, -1, this.callback); 176 | }, 177 | 'should return the zset in sorted ascending score order': function (err, list) { 178 | assert.deepEqual(list, ["a", "b", "c", "d"]); 179 | } 180 | }, 181 | 'with the start > end': { 182 | topic: function (client) { 183 | client.zrange("zset-12", 4, 5, this.callback); 184 | }, 185 | 'should return an empty list': function (err, list) { 186 | assert.deepEqual(list, []); 187 | } 188 | }, 189 | "with end >= zset's length": { 190 | topic: function (client) { 191 | client.zrange("zset-12", 0, 4, this.callback); 192 | }, 193 | 'should return the entire zset sorted in ascending score order': function (err, list) { 194 | assert.deepEqual(list, ["a", "b", "c", "d"]); 195 | } 196 | }, 197 | 'with scores': { 198 | topic: function (client) { 199 | client.zrange("zset-12", 0, -1, "withscores", this.callback); 200 | }, 201 | 'should return an array of hashes with the scores': function (err, list) { 202 | assert.deepEqual(list, [{a: 10}, {b: 20}, {c: 30}, {d: 40}]); 203 | } 204 | } 205 | }), 206 | 207 | // TODO Make the command call more idiomatic 208 | // c.zrangebyscore(key, ">=0", "<=3", {offset: 0, count: 2, withscores: true}); 209 | // TODO Add tests for exclusive intervals and infinities 210 | 'the command ZRANGEBYSCORE': usingClient({ 211 | topic: function (client) { 212 | client.zadd("zset-13", 10, "a"); 213 | client.zadd("zset-13", 20, "b"); 214 | client.zadd("zset-13", 30, "c"); 215 | client.zadd("zset-13", 40, "d"); 216 | return client; 217 | }, 218 | 'with minimum number of parameters (key, min, max)': { 219 | topic: function (client) { 220 | client.zrangebyscore("zset-13", 10, 30, this.callback); 221 | }, 222 | 'should return only the items in the zset within the min and max inclusive': function (err, list) { 223 | assert.deepEqual(list, ["a", "b", "c"]); 224 | } 225 | }, 226 | 'with LIMIT': { 227 | topic: function (client) { 228 | client.zrangebyscore("zset-13", 10, 30, "LIMIT", 1, 2, this.callback); 229 | }, 230 | 'should return only the appropriate items': function (err, list) { 231 | assert.deepEqual(list, ["b", "c"]); 232 | } 233 | }, 234 | 'with scores': { 235 | topic: function (client) { 236 | client.zrangebyscore("zset-13", 10, 40, "LIMIT", 1, 3, "withscores", this.callback); 237 | }, 238 | 'should return an array of hashes with the scores': function (err, list) { 239 | assert.deepEqual(list, [{b: 20}, {c: 30}, {d: 40}]); 240 | } 241 | } 242 | }), 243 | 244 | 'the command ZCOUNT': usingClient({ 245 | topic: function (client) { 246 | client.zadd("zset-14", 10, "a"); 247 | client.zadd("zset-14", 20, "b"); 248 | client.zadd("zset-14", 30, "c"); 249 | client.zadd("zset-14", 40, "d"); 250 | client.zcount("zset-14", 20, 30, this.callback); 251 | }, 252 | 'should return the number of elements that match the score range': function (err, card) { 253 | assert.equal(card, 2); 254 | } 255 | }), 256 | 257 | 'the command ZCARD': usingClient({ 258 | 'for an existing zset': { 259 | topic: function (client) { 260 | client.zadd("zset-15", 10, "a"); 261 | client.zadd("zset-15", 20, "b"); 262 | client.zadd("zset-15", 30, "c"); 263 | client.zadd("zset-15", 40, "d"); 264 | client.zcard("zset-15", this.callback); 265 | }, 266 | 'should return the number of members in the zset': function (err, card) { 267 | assert.equal(card, 4); 268 | } 269 | }, 270 | 'for a non-existing key': { 271 | topic: function (client) { 272 | client.zcard("non-existing-key", this.callback); 273 | }, 274 | 'should return 0': function (err, result) { 275 | assert.equal(result, 0); 276 | } 277 | } 278 | }), 279 | 280 | 'the command ZSCORE': usingClient({ 281 | 'for an existing zset': { 282 | topic: function (client) { 283 | client.zadd("zset-16", 10, "a"); 284 | return client; 285 | }, 286 | 'containing the element': { 287 | topic: function (client) { 288 | client.zscore("zset-16", "a", this.callback); 289 | }, 290 | 'should return the score': function (err, score) { 291 | assert.equal(score, 10.0); 292 | } 293 | }, 294 | 'not containing the element': { 295 | topic: function (client) { 296 | client.zscore("zset-16", "b", this.callback); 297 | }, 298 | 'should return null': function (err, nil) { 299 | assert.isNull(nil); 300 | } 301 | } 302 | }, 303 | 'for a non-existing key': { 304 | topic: function (client) { 305 | client.zscore("non-existing-key", "a", this.callback); 306 | }, 307 | 'should return null': function (err, nil) { 308 | assert.isNull(nil); 309 | } 310 | } 311 | }), 312 | 313 | 'the command ZREMRANGEBYRANK': usingClient({ 314 | topic: function (client) { 315 | client.zadd("zset-17", 10, "a"); 316 | client.zadd("zset-17", 20, "b"); 317 | client.zadd("zset-17", 30, "c"); 318 | client.zadd("zset-17", 40, "d"); 319 | client.zremrangebyrank("zset-17", 0, 1, this.callback); 320 | }, 321 | 'should return the number of elements removed': function (err, num) { 322 | assert.equal(num, 2); 323 | } 324 | }), 325 | 326 | 'the command ZREMRANGEBYSCORE': usingClient({ 327 | topic: function (client) { 328 | client.zadd("zset-18", 10, "a"); 329 | client.zadd("zset-18", 20, "b"); 330 | client.zadd("zset-18", 30, "c"); 331 | client.zadd("zset-18", 40, "d"); 332 | client.zremrangebyscore("zset-18", 15, 25, this.callback); 333 | }, 334 | 'should return the number of elements removed': function (err, num) { 335 | assert.equal(num, 1); 336 | } 337 | }), 338 | 339 | 'the command ZUNIONSTORE': usingClient({ 340 | topic: function (client) { 341 | client.zadd("zset-19", 10, "a"); 342 | client.zadd("zset-19", 20, "b"); 343 | client.zadd("zset-19", 30, "c"); 344 | client.zadd("zset-19", 40, "d"); 345 | 346 | client.zadd("zset-20", 10, "c"); 347 | client.zadd("zset-20", 20, "d"); 348 | client.zadd("zset-20", 30, "e"); 349 | client.zadd("zset-20", 40, "f"); 350 | 351 | return client; 352 | }, 353 | 'with bare minimum parameters': { 354 | topic: function (client) { 355 | client.zunionstore("zunionstore-dest-1", ["zset-19", "zset-20"], this.callback); 356 | }, 357 | 'should return the number of elements in the sorted set': function (err, card) { 358 | assert.equal(card, 6); 359 | } 360 | }, 361 | 'with WEIGHTS': { 362 | topic: function (client) { 363 | client.zunionstore("zunionstore-dest-2", {"zset-19": 2, "zset-20": 3}, this.callback); 364 | }, 365 | 'should return the number of elements in the sorted set': function (err, card) { 366 | assert.equal(card, 6); 367 | } 368 | }, 369 | 'with AGGREGATE': { 370 | topic: function (client) { 371 | client.zunionstore("zunionstore-dest-3", ["zset-19", "zset-20"], "min", this.callback); 372 | }, 373 | 'should return the number of elements in the sorted set': function (err, card) { 374 | assert.equal(card, 6); 375 | } 376 | }, 377 | 'with WEIGHTS and AGGREGATE': { 378 | topic: function (client) { 379 | client.zunionstore("zunionstore-dest-4", {"zset-19": 2, "zset-20": 3}, "min", this.callback); 380 | }, 381 | 'should return the number of elements in the sorted set': function (err, card) { 382 | assert.equal(card, 6); 383 | } 384 | } 385 | 386 | }), 387 | 388 | 'the command ZINTERSTORE': usingClient({ 389 | topic: function (client) { 390 | client.zadd("zset-21", 10, "a"); 391 | client.zadd("zset-21", 20, "b"); 392 | client.zadd("zset-21", 30, "c"); 393 | client.zadd("zset-21", 40, "d"); 394 | 395 | client.zadd("zset-22", 10, "c"); 396 | client.zadd("zset-22", 20, "d"); 397 | client.zadd("zset-22", 30, "e"); 398 | client.zadd("zset-22", 40, "f"); 399 | 400 | return client; 401 | }, 402 | 'with bare minimum parameters': { 403 | topic: function (client) { 404 | client.zinterstore("zinterstore-dest-5", ["zset-21", "zset-22"], this.callback); 405 | }, 406 | 'should return the number of elements in the sorted set': function (err, card) { 407 | assert.equal(card, 2); 408 | } 409 | }, 410 | 'with WEIGHTS': { 411 | topic: function (client) { 412 | client.zinterstore("zinterstore-dest-6", {"zset-21": 2, "zset-22": 3}, this.callback); 413 | }, 414 | 'should return the number of elements in the sorted set': function (err, card) { 415 | assert.equal(card, 2); 416 | } 417 | }, 418 | 'with AGGREGATE': { 419 | topic: function (client) { 420 | client.zinterstore("zinterstore-dest-7", ["zset-21", "zset-22"], "min", this.callback); 421 | }, 422 | 'should return the number of elements in the sorted set': function (err, card) { 423 | assert.equal(card, 2); 424 | } 425 | }, 426 | 'with WEIGHTS and AGGREGATE': { 427 | topic: function (client) { 428 | client.zinterstore("zinterstore-dest-8", {"zset-21": 2, "zset-22": 3}, "min", this.callback); 429 | }, 430 | 'should return the number of elements in the sorted set': function (err, card) { 431 | assert.equal(card, 2); 432 | } 433 | } 434 | 435 | }) 436 | }).export(module, {}); 437 | --------------------------------------------------------------------------------