Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0b8090b

Browse files
committedApr 29, 2016
v.2.6.0-2 pre-release
2 parents c2c57de + 03f1a60 commit 0b8090b

36 files changed

+1914
-616
lines changed
 

‎.github/ISSUE_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ to a small test case, but it's highly appreciated to have as much data as possib
99
Thank you!_
1010

1111
* **Version**: What node_redis and what redis version is the issue happening on?
12-
* **Platform**: What platform / version? (For example Node.js 0.10 or Node.js 5.7.0)
12+
* **Platform**: What platform / version? (For example Node.js 0.10 or Node.js 5.7.0 on Windows 7 / Ubuntu 15.10 / Azure)
1313
* **Description**: Description of your issue, stack traces from errors and code that reproduces the issue
1414

1515
[gitter]: https://gitter.im/NodeRedis/node_redis?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge

‎README.md

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ Install with:
1212

1313
npm install redis
1414

15-
## Usage
16-
17-
Simple example, included as `examples/simple.js`:
15+
## Usage Example
1816

1917
```js
2018
var redis = require("redis"),
@@ -51,6 +49,8 @@ This will display:
5149
mjr:~/work/node_redis (master)$
5250

5351
Note that the API is entirely asynchronous. To get data back from the server, you'll need to use a callback.
52+
From v.2.6 on the API supports camelCase and snack_case and all options / variables / events etc. can be used either way.
53+
It is recommended to use camelCase as this is the default for the Node.js landscape.
5454

5555
### Promises
5656

@@ -83,7 +83,7 @@ return client.multi().get('foo').execAsync().then(function(res) {
8383
Each Redis command is exposed as a function on the `client` object.
8484
All functions take either an `args` Array plus optional `callback` Function or
8585
a variable number of individual arguments followed by an optional callback.
86-
Here are examples how to use the api:
86+
Examples:
8787

8888
```js
8989
client.hmset(["key", "test keys 1", "test val 1", "test keys 2", "test val 2"], function (err, res) {});
@@ -111,8 +111,6 @@ client.get("missingkey", function(err, reply) {
111111

112112
For a list of Redis commands, see [Redis Command Reference](http://redis.io/commands)
113113

114-
The commands can be specified in uppercase or lowercase for convenience. `client.get()` is the same as `client.GET()`.
115-
116114
Minimal parsing is done on the replies. Commands that return a integer return JavaScript Numbers, arrays return JavaScript Array. `HGETALL` returns an Object keyed by the hash keys. All strings will either be returned as string or as buffer depending on your setting.
117115
Please be aware that sending null, undefined and Boolean values will result in the value coerced to a string!
118116

@@ -139,6 +137,7 @@ are passed an object containing `delay` (in ms) and `attempt` (the attempt #) at
139137
### "error"
140138

141139
`client` will emit `error` when encountering an error connecting to the Redis server or when any other in node_redis occurs.
140+
If you use a command without callback and encounter a ReplyError it is going to be emitted to the error listener.
142141

143142
So please attach the error listener to node_redis.
144143

@@ -182,7 +181,7 @@ __Tip:__ If the Redis server runs on the same machine as the client consider usi
182181
| host | 127.0.0.1 | IP address of the Redis server |
183182
| port | 6379 | Port of the Redis server |
184183
| path | null | The UNIX socket string of the Redis server |
185-
| url | null | The URL of the Redis server. Format: `[redis:]//[user][:password@][host][:port][/db-number][?db=db-number[&password=bar[&option=value]]]` (More info avaliable at [IANA](http://www.iana.org/assignments/uri-schemes/prov/redis)). |
184+
| url | null | The URL of the Redis server. Format: `[redis:]//[[user][:password@]][host][:port][/db-number][?db=db-number[&password=bar[&option=value]]]` (More info avaliable at [IANA](http://www.iana.org/assignments/uri-schemes/prov/redis)). |
186185
| parser | hiredis | If hiredis is not installed, automatic fallback to the built-in javascript parser |
187186
| string_numbers | null | Set to `true`, `node_redis` will return Redis number values as Strings instead of javascript Numbers. Useful if you need to handle big numbers (above `Number.MAX_SAFE_INTEGER === 2^53`). Hiredis is incapable of this behavior, so setting this option to `true` will result in the built-in javascript parser being used no matter the value of the `parser` option. |
188187
| return_buffers | false | If set to `true`, then all replies will be sent to callbacks as Buffers instead of Strings. |
@@ -295,6 +294,51 @@ client.get("foo_rand000000000000", function (err, reply) {
295294

296295
`client.end()` without the flush parameter set to true should NOT be used in production!
297296

297+
## Error handling (>= v.2.6)
298+
299+
All redis errors are returned as `ReplyError`.
300+
All unresolved commands that get rejected due to what ever reason return a `AbortError`.
301+
As subclass of the `AbortError` a `AggregateError` exists. This is emitted in case multiple unresolved commands without callback got rejected in debug_mode.
302+
They are all aggregated and a single error is emitted in that case.
303+
304+
Example:
305+
```js
306+
var redis = require('./');
307+
var assert = require('assert');
308+
var client = redis.createClient();
309+
310+
client.on('error', function (err) {
311+
assert(err instanceof Error);
312+
assert(err instanceof redis.AbortError);
313+
assert(err instanceof redis.AggregateError);
314+
assert.strictEqual(err.errors.length, 2); // The set and get got aggregated in here
315+
assert.strictEqual(err.code, 'NR_CLOSED');
316+
});
317+
client.set('foo', 123, 'bar', function (err, res) { // To many arguments
318+
assert(err instanceof redis.ReplyError); // => true
319+
assert.strictEqual(err.command, 'SET');
320+
assert.deepStrictEqual(err.args, ['foo', 123, 'bar']);
321+
322+
redis.debug_mode = true;
323+
client.set('foo', 'bar');
324+
client.get('foo');
325+
process.nextTick(function () {
326+
client.end(true); // Force closing the connection while the command did not yet return
327+
redis.debug_mode = false;
328+
});
329+
});
330+
331+
```
332+
333+
Every `ReplyError` contains the `command` name in all-caps and the arguments (`args`).
334+
335+
If node_redis emits a library error because of another error, the triggering error is added to the returned error as `origin` attribute.
336+
337+
___Error codes___
338+
339+
node_redis returns a `NR_CLOSED` error code if the clients connection dropped. If a command unresolved command got rejected a `UNERCTAIN_STATE` code is returned.
340+
A `CONNECTION_BROKEN` error code is used in case node_redis gives up to reconnect.
341+
298342
## client.unref()
299343

300344
Call `unref()` on the underlying socket connection to the Redis server, allowing the program to exit once no more commands are pending.
@@ -363,7 +407,7 @@ client.HMSET(key1, "0123456789", "abcdefghij", "some manner of key", "a type of
363407

364408
## Publish / Subscribe
365409

366-
Here is a simple example of the API for publish / subscribe. This program opens two
410+
Example of the publish / subscribe API. This program opens two
367411
client connections, subscribes to a channel on one of them, and publishes to that
368412
channel on the other:
369413

@@ -412,6 +456,16 @@ Client will emit `pmessage` for every message received that matches an active su
412456
Listeners are passed the original pattern used with `PSUBSCRIBE` as `pattern`, the sending channel
413457
name as `channel`, and the message as `message`.
414458

459+
### "message_buffer" (channel, message)
460+
461+
This is the same as the `message` event with the exception, that it is always going to emit a buffer.
462+
If you listen to the `message` event at the same time as the `message_buffer`, it is always going to emit a string.
463+
464+
### "pmessage_buffer" (pattern, channel, message)
465+
466+
This is the same as the `pmessage` event with the exception, that it is always going to emit a buffer.
467+
If you listen to the `pmessage` event at the same time as the `pmessage_buffer`, it is always going to emit a string.
468+
415469
### "subscribe" (channel, count)
416470

417471
Client will emit `subscribe` in response to a `SUBSCRIBE` command. Listeners are passed the
@@ -529,7 +583,7 @@ Redis. The interface in `node_redis` is to return an individual `Batch` object b
529583
The only difference between .batch and .multi is that no transaction is going to be used.
530584
Be aware that the errors are - just like in multi statements - in the result. Otherwise both, errors and results could be returned at the same time.
531585

532-
If you fire many commands at once this is going to **boost the execution speed by up to 400%** [sic!] compared to fireing the same commands in a loop without waiting for the result! See the benchmarks for further comparison. Please remember that all commands are kept in memory until they are fired.
586+
If you fire many commands at once this is going to boost the execution speed significantly compared to fireing the same commands in a loop without waiting for the result! See the benchmarks for further comparison. Please remember that all commands are kept in memory until they are fired.
533587

534588
## Monitor mode
535589

@@ -539,7 +593,7 @@ across all client connections, including from other client libraries and other c
539593
A `monitor` event is going to be emitted for every command fired from any client connected to the server including the monitoring client itself.
540594
The callback for the `monitor` event takes a timestamp from the Redis server, an array of command arguments and the raw monitoring string.
541595

542-
Here is a simple example:
596+
Example:
543597

544598
```js
545599
var client = require("redis").createClient();
@@ -599,9 +653,10 @@ the second word as first parameter:
599653
client.multi().script('load', 'return 1').exec(...);
600654
client.multi([['script', 'load', 'return 1']]).exec(...);
601655

602-
## client.duplicate([options])
656+
## client.duplicate([options][, callback])
603657

604658
Duplicate all current options and return a new redisClient instance. All options passed to the duplicate function are going to replace the original option.
659+
If you pass a callback, duplicate is going to wait until the client is ready and returns it in the callback. If an error occurs in the meanwhile, that is going to return an error instead in the callback.
605660

606661
## client.send_command(command_name[, [args][, callback]])
607662

@@ -615,27 +670,16 @@ All commands are sent as multi-bulk commands. `args` can either be an Array of a
615670

616671
Boolean tracking the state of the connection to the Redis server.
617672

618-
## client.command_queue.length
673+
## client.command_queue_length
619674

620675
The number of commands that have been sent to the Redis server but not yet replied to. You can use this to
621676
enforce some kind of maximum queue depth for commands while connected.
622677

623-
Don't mess with `client.command_queue` though unless you really know what you are doing.
624-
625-
## client.offline_queue.length
678+
## client.offline_queue_length
626679

627680
The number of commands that have been queued up for a future connection. You can use this to enforce
628681
some kind of maximum queue depth for pre-connection commands.
629682

630-
## client.retry_delay
631-
632-
Current delay in milliseconds before a connection retry will be attempted. This starts at `200`.
633-
634-
## client.retry_backoff
635-
636-
Multiplier for future retry timeouts. This should be larger than 1 to add more time between retries.
637-
Defaults to 1.7. The default initial connection retry is 200, so the second retry will be 340, followed by 578, etc.
638-
639683
### Commands with Optional and Keyword arguments
640684

641685
This applies to anything that uses an optional `[WITHSCORES]` or `[LIMIT offset count]` in the [redis.io/commands](http://redis.io/commands) documentation.

‎benchmarks/multi_bench.js

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function returnArg (name, def) {
2323
}
2424
var num_clients = returnArg('clients', 1);
2525
var run_time = returnArg('time', 2500); // ms
26+
var pipeline = returnArg('pipeline', 1); // number of concurrent commands
2627
var versions_logged = false;
2728
var client_options = {
2829
parser: returnArg('parser', 'hiredis'),
@@ -41,17 +42,18 @@ function lpad (input, len, chr) {
4142

4243
metrics.Histogram.prototype.print_line = function () {
4344
var obj = this.printObj();
44-
return lpad((obj.min / 1e6).toFixed(2), 6) + '/' + lpad((obj.max / 1e6).toFixed(2), 6) + '/' + lpad((obj.mean / 1e6).toFixed(2), 6);
45+
return lpad((obj.mean / 1e6).toFixed(2), 6) + '/' + lpad((obj.max / 1e6).toFixed(2), 6);
4546
};
4647

4748
function Test (args) {
4849
this.args = args;
50+
this.args.pipeline = +pipeline;
4951
this.callback = null;
5052
this.clients = [];
5153
this.clients_ready = 0;
5254
this.commands_sent = 0;
5355
this.commands_completed = 0;
54-
this.max_pipeline = this.args.pipeline || 50;
56+
this.max_pipeline = +pipeline;
5557
this.batch_pipeline = this.args.batch || 0;
5658
this.client_options = args.client_options || {};
5759
this.client_options.parser = client_options.parser;
@@ -206,7 +208,7 @@ Test.prototype.print_stats = function () {
206208
var duration = Date.now() - this.test_start;
207209
totalTime += duration;
208210

209-
console.log('min/max/avg: ' + this.command_latency.print_line() + ' ' + lpad(duration, 6) + 'ms total, ' +
211+
console.log('avg/max: ' + this.command_latency.print_line() + lpad(duration, 5) + 'ms total, ' +
210212
lpad(Math.round(this.commands_completed / (duration / 1000)), 7) + ' ops/sec');
211213
};
212214

@@ -217,55 +219,55 @@ large_buf = new Buffer(large_str);
217219
very_large_str = (new Array((4 * 1024 * 1024) + 1).join('-'));
218220
very_large_buf = new Buffer(very_large_str);
219221

220-
tests.push(new Test({descr: 'PING', command: 'ping', args: [], pipeline: 1}));
222+
tests.push(new Test({descr: 'PING', command: 'ping', args: []}));
221223
tests.push(new Test({descr: 'PING', command: 'ping', args: [], batch: 50}));
222224

223-
tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str], pipeline: 1}));
225+
tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str]}));
224226
tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str], batch: 50}));
225227

226-
tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf], pipeline: 1}));
228+
tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf]}));
227229
tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf], batch: 50}));
228230

229-
tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000'], pipeline: 1}));
231+
tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000']}));
230232
tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000'], batch: 50}));
231233

232-
tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], pipeline: 1, client_opts: { return_buffers: true} }));
234+
tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], client_opts: { return_buffers: true} }));
233235
tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], batch: 50, client_opts: { return_buffers: true} }));
234236

235-
tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str], pipeline: 1}));
237+
tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str]}));
236238
tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str], batch: 50}));
237239

238-
tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf], pipeline: 1}));
240+
tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf]}));
239241
tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf], batch: 50}));
240242

241-
tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001'], pipeline: 1}));
243+
tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001']}));
242244
tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001'], batch: 50}));
243245

244-
tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], pipeline: 1, client_opts: { return_buffers: true} }));
246+
tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], client_opts: { return_buffers: true} }));
245247
tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], batch: 50, client_opts: { return_buffers: true} }));
246248

247-
tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000'], pipeline: 1}));
249+
tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000']}));
248250
tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000'], batch: 50}));
249251

250-
tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str], pipeline: 1}));
252+
tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str]}));
251253
tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str], batch: 50}));
252254

253-
tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9'], pipeline: 1}));
255+
tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9']}));
254256
tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9'], batch: 50}));
255257

256-
tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99'], pipeline: 1}));
258+
tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99']}));
257259
tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99'], batch: 50}));
258260

259-
tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str], pipeline: 1}));
261+
tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str]}));
260262
tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str], batch: 20}));
261263

262-
tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf], pipeline: 1}));
264+
tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf]}));
263265
tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf], batch: 20}));
264266

265-
tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002'], pipeline: 1}));
267+
tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002']}));
266268
tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002'], batch: 20}));
267269

268-
tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], pipeline: 1, client_opts: { return_buffers: true} }));
270+
tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], client_opts: { return_buffers: true} }));
269271
tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], batch: 20, client_opts: { return_buffers: true} }));
270272

271273
function next () {

‎changelog.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,43 @@
11
Changelog
22
=========
33

4+
## v.2.6.0-2 - xx Apr, 2016
5+
6+
Features
7+
8+
- Added support for the new `CLIENT REPLY ON|OFF|SKIP` command (Redis v.3.2)
9+
- Added support for camelCase
10+
- The Node.js landscape default is to use camelCase. node_redis is a bit out of the box here
11+
but from now on it is possible to use both, just as you prefer!
12+
- If there's any documented variable missing as camelCased, please open a issue for it
13+
- Improve error handling significantly
14+
- Only emit an error if the error has not already been handled in a callback
15+
- Emit an error if a command would otherwise silently fail (no callback present)
16+
- Improved unspecific error messages e.g. "Connection gone from end / close event"
17+
- Added `args` to command errors to improve identification of the error
18+
- Added origin to errors if there's e.g. a connection error
19+
- Added ReplyError class. All Redis errors are from now on going to be of that class
20+
- Added AbortError class. A subclass of AbortError. All unresolved and by node_redis rejected commands are from now on of that class
21+
- Added AggregateError class. If a unresolved and by node_redis rejected command has no callback and
22+
this applies to more than a single command, the errors for the commands without callback are aggregated
23+
to a single error that is emitted in debug_mode in that case.
24+
- Added `message_buffer` / `pmessage_buffer` events. That event is always going to emit a buffer
25+
- Listening to the `message` event at the same time is always going to return the same message as string
26+
- Added callback option to the duplicate function
27+
- Added support for `__proto__` and other reserved keywords as hgetall field
28+
- Updated [redis-commands](https://github.com/NodeRedis/redis-commands) dependency ([changelog](https://github.com/NodeRedis/redis-commands/releases/tag/v.1.2.0))
29+
30+
Bugfixes
31+
32+
- Fixed v.2.5.0 auth command regression (under special circumstances a reconnect would not authenticate properly)
33+
- Fixed v.2.6.0-0 pub sub mode and quit command regressions:
34+
- Entering pub sub mode not working if a earlier called and still running command returned an error
35+
- Unsubscribe callback not called if unsubscribing from all channels and resubscribing right away
36+
- Quit command resulting in an error in some cases
37+
- Fixed special handled functions in batch and multi context not working the same as without (e.g. select and info)
38+
- Be aware that not all commands work in combination with transactions but they all work with batch
39+
- Fixed address always set to 127.0.0.1:6379 in case host / port is set in the `tls` options instead of the general options
40+
441
## v.2.6.0-1 - 01 Apr, 2016
542

643
A second pre-release with further fixes. This is likely going to be released as 2.6.0 stable without further changes.

‎index.js

Lines changed: 361 additions & 174 deletions
Large diffs are not rendered by default.

‎lib/command.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
// This Command constructor is ever so slightly faster than using an object literal, but more importantly, using
44
// a named constructor helps it show up meaningfully in the V8 CPU profiler and in heap snapshots.
5-
function Command (command, args, callback) {
5+
function Command (command, args, buffer_args, callback) {
66
this.command = command;
7-
this.args = args; // We only need the args for the offline commands => move them into another class. We need the number of args though for pub sub
8-
this.buffer_args = false;
7+
this.args = args;
8+
this.buffer_args = buffer_args;
99
this.callback = callback;
10-
this.sub_commands_left = args.length;
1110
}
1211

13-
function OfflineCommand (command, args, callback) {
12+
function OfflineCommand (command, args, callback, call_on_write) {
1413
this.command = command;
1514
this.args = args;
1615
this.callback = callback;
16+
this.call_on_write = call_on_write;
1717
}
1818

1919
module.exports = {

‎lib/createClient.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = function createClient (port_arg, host_arg, options) {
1212
host = host_arg;
1313
} else {
1414
if (options && host_arg) {
15-
throw new Error('Unknown type of connection in createClient()');
15+
throw new TypeError('Unknown type of connection in createClient()');
1616
}
1717
options = options || host_arg;
1818
}
@@ -44,20 +44,20 @@ module.exports = function createClient (port_arg, host_arg, options) {
4444
}
4545
if (parsed.search !== '') {
4646
var elem;
47-
for (elem in parsed.query) { // jshint ignore: line
47+
for (elem in parsed.query) {
4848
// If options are passed twice, only the parsed options will be used
4949
if (elem in options) {
5050
if (options[elem] === parsed.query[elem]) {
5151
console.warn('node_redis: WARNING: You passed the ' + elem + ' option twice!');
5252
} else {
53-
throw new Error('The ' + elem + ' option is added twice and does not match');
53+
throw new RangeError('The ' + elem + ' option is added twice and does not match');
5454
}
5555
}
5656
options[elem] = parsed.query[elem];
5757
}
5858
}
5959
} else if (parsed.hostname) {
60-
throw new Error('The redis url must begin with slashes "//" or contain slashes after the redis protocol');
60+
throw new RangeError('The redis url must begin with slashes "//" or contain slashes after the redis protocol');
6161
} else {
6262
options.path = port_arg;
6363
}
@@ -67,12 +67,12 @@ module.exports = function createClient (port_arg, host_arg, options) {
6767
options.host = options.host || host_arg;
6868

6969
if (port_arg && arguments.length !== 1) {
70-
throw new Error('To many arguments passed to createClient. Please only pass the options object');
70+
throw new TypeError('To many arguments passed to createClient. Please only pass the options object');
7171
}
7272
}
7373

7474
if (!options) {
75-
throw new Error('Unknown type of connection in createClient()');
75+
throw new TypeError('Unknown type of connection in createClient()');
7676
}
7777

7878
return options;

‎lib/customErrors.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
3+
var util = require('util');
4+
5+
function AbortError (obj) {
6+
Error.captureStackTrace(this, this.constructor);
7+
var message;
8+
Object.defineProperty(this, 'name', {
9+
get: function () {
10+
return this.constructor.name;
11+
}
12+
});
13+
Object.defineProperty(this, 'message', {
14+
get: function () {
15+
return message;
16+
},
17+
set: function (msg) {
18+
message = msg;
19+
}
20+
});
21+
for (var keys = Object.keys(obj), key = keys.pop(); key; key = keys.pop()) {
22+
this[key] = obj[key];
23+
}
24+
// Explicitly add the message
25+
// If the obj is a error itself, the message is not enumerable
26+
this.message = obj.message;
27+
}
28+
29+
function ReplyError (obj) {
30+
Error.captureStackTrace(this, this.constructor);
31+
var tmp;
32+
Object.defineProperty(this, 'name', {
33+
get: function () {
34+
return this.constructor.name;
35+
}
36+
});
37+
Object.defineProperty(this, 'message', {
38+
get: function () {
39+
return tmp;
40+
},
41+
set: function (msg) {
42+
tmp = msg;
43+
}
44+
});
45+
this.message = obj.message;
46+
}
47+
48+
function AggregateError (obj) {
49+
Error.captureStackTrace(this, this.constructor);
50+
var tmp;
51+
Object.defineProperty(this, 'name', {
52+
get: function () {
53+
return this.constructor.name;
54+
}
55+
});
56+
Object.defineProperty(this, 'message', {
57+
get: function () {
58+
return tmp;
59+
},
60+
set: function (msg) {
61+
tmp = msg;
62+
}
63+
});
64+
for (var keys = Object.keys(obj), key = keys.pop(); key; key = keys.pop()) {
65+
this[key] = obj[key];
66+
}
67+
this.message = obj.message;
68+
}
69+
70+
util.inherits(ReplyError, Error);
71+
util.inherits(AbortError, Error);
72+
util.inherits(AggregateError, AbortError);
73+
74+
module.exports = {
75+
ReplyError: ReplyError,
76+
AbortError: AbortError,
77+
AggregateError: AggregateError
78+
};

‎lib/debug.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
var index = require('../');
44

5-
function debug (msg) {
5+
function debug () {
66
if (index.debug_mode) {
7-
console.error(msg);
7+
console.error.apply(null, arguments);
88
}
99
}
1010

‎lib/extendedApi.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ All documented and exposed API belongs in here
1010
**********************************************/
1111

1212
// Redirect calls to the appropriate function and use to send arbitrary / not supported commands
13-
RedisClient.prototype.send_command = function (command, args, callback) {
13+
RedisClient.prototype.send_command = RedisClient.prototype.sendCommand = function (command, args, callback) {
1414
// Throw to fail early instead of relying in order in this case
1515
if (typeof command !== 'string') {
16-
throw new Error('Wrong input type "' + (command !== null && command !== undefined ? command.constructor.name : command) + '" for command name');
16+
throw new TypeError('Wrong input type "' + (command !== null && command !== undefined ? command.constructor.name : command) + '" for command name');
1717
}
1818
if (!Array.isArray(args)) {
1919
if (args === undefined || args === null) {
@@ -22,11 +22,11 @@ RedisClient.prototype.send_command = function (command, args, callback) {
2222
callback = args;
2323
args = [];
2424
} else {
25-
throw new Error('Wrong input type "' + args.constructor.name + '" for args');
25+
throw new TypeError('Wrong input type "' + args.constructor.name + '" for args');
2626
}
2727
}
2828
if (typeof callback !== 'function' && callback !== undefined) {
29-
throw new Error('Wrong input type "' + (callback !== null ? callback.constructor.name : 'null') + '" for callback function');
29+
throw new TypeError('Wrong input type "' + (callback !== null ? callback.constructor.name : 'null') + '" for callback function');
3030
}
3131

3232
// Using the raw multi command is only possible with this function
@@ -39,15 +39,18 @@ RedisClient.prototype.send_command = function (command, args, callback) {
3939
return this.internal_send_command(command, args, callback);
4040
}
4141
if (typeof callback === 'function') {
42-
args = args.concat([callback]);
42+
args = args.concat([callback]); // Prevent manipulating the input array
4343
}
4444
return this[command].apply(this, args);
4545
};
4646

4747
RedisClient.prototype.end = function (flush) {
4848
// Flush queue if wanted
4949
if (flush) {
50-
this.flush_and_error(new Error("The command can't be processed. The connection has already been closed."));
50+
this.flush_and_error({
51+
message: 'Connection forcefully ended and command aborted.',
52+
code: 'NR_CLOSED'
53+
});
5154
} else if (arguments.length === 0) {
5255
this.warn(
5356
'Using .end() without the flush parameter is deprecated and throws from v.3.0.0 on.\n' +
@@ -79,13 +82,30 @@ RedisClient.prototype.unref = function () {
7982
}
8083
};
8184

82-
RedisClient.prototype.duplicate = function (options) {
85+
RedisClient.prototype.duplicate = function (options, callback) {
86+
if (typeof options === 'function') {
87+
callback = options;
88+
options = null;
89+
}
8390
var existing_options = utils.clone(this.options);
8491
options = utils.clone(options);
85-
for (var elem in options) { // jshint ignore: line
92+
for (var elem in options) {
8693
existing_options[elem] = options[elem];
8794
}
8895
var client = new RedisClient(existing_options);
8996
client.selected_db = this.selected_db;
97+
if (typeof callback === 'function') {
98+
var ready_listener = function () {
99+
callback(null, client);
100+
client.removeAllListeners(error_listener);
101+
};
102+
var error_listener = function (err) {
103+
callback(err);
104+
client.end(true);
105+
};
106+
client.once('ready', ready_listener);
107+
client.once('error', error_listener);
108+
return;
109+
}
90110
return client;
91111
};

‎lib/individualCommands.js

Lines changed: 478 additions & 74 deletions
Large diffs are not rendered by default.

‎lib/multi.js

Lines changed: 43 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -20,47 +20,11 @@ function Multi (client, args) {
2020
}
2121
}
2222

23-
Multi.prototype.hmset = Multi.prototype.HMSET = function hmset () {
24-
var arr,
25-
len = 0,
26-
callback,
27-
i = 0;
28-
if (Array.isArray(arguments[0])) {
29-
arr = arguments[0];
30-
callback = arguments[1];
31-
} else if (Array.isArray(arguments[1])) {
32-
len = arguments[1].length;
33-
arr = new Array(len + 1);
34-
arr[0] = arguments[0];
35-
for (; i < len; i += 1) {
36-
arr[i + 1] = arguments[1][i];
37-
}
38-
callback = arguments[2];
39-
} else if (typeof arguments[1] === 'object' && (typeof arguments[2] === 'function' || typeof arguments[2] === 'undefined')) {
40-
arr = [arguments[0]];
41-
for (var field in arguments[1]) { // jshint ignore: line
42-
arr.push(field, arguments[1][field]);
43-
}
44-
callback = arguments[2];
45-
} else {
46-
len = arguments.length;
47-
// The later should not be the average use case
48-
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
49-
len--;
50-
callback = arguments[len];
51-
}
52-
arr = new Array(len);
53-
for (; i < len; i += 1) {
54-
arr[i] = arguments[i];
55-
}
56-
}
57-
this.queue.push(['hmset', arr, callback]);
58-
return this;
59-
};
60-
61-
function pipeline_transaction_command (self, command, args, index, cb) {
23+
function pipeline_transaction_command (self, command, args, index, cb, call_on_write) {
24+
// Queueing is done first, then the commands are executed
6225
self._client.send_command(command, args, function (err, reply) {
63-
if (err) {
26+
// Ignore the multi command. This is applied by node_redis and the user does not benefit by it
27+
if (err && index !== -1) {
6428
if (cb) {
6529
cb(err);
6630
}
@@ -70,7 +34,7 @@ function pipeline_transaction_command (self, command, args, index, cb) {
7034
});
7135
}
7236

73-
Multi.prototype.exec_atomic = function exec_atomic (callback) {
37+
Multi.prototype.exec_atomic = Multi.prototype.EXEC_ATOMIC = Multi.prototype.execAtomic = function exec_atomic (callback) {
7438
if (this.queue.length < 2) {
7539
return this.exec_batch(callback);
7640
}
@@ -81,13 +45,11 @@ function multi_callback (self, err, replies) {
8145
var i = 0, args;
8246

8347
if (err) {
84-
// The errors would be circular
85-
var connection_error = ['CONNECTION_BROKEN', 'UNCERTAIN_STATE'].indexOf(err.code) !== -1;
86-
err.errors = connection_error ? [] : self.errors;
48+
err.errors = self.errors;
8749
if (self.callback) {
8850
self.callback(err);
8951
// Exclude connection errors so that those errors won't be emitted twice
90-
} else if (!connection_error) {
52+
} else if (err.code !== 'CONNECTION_BROKEN') {
9153
self._client.emit('error', err);
9254
}
9355
return;
@@ -122,36 +84,45 @@ function multi_callback (self, err, replies) {
12284
}
12385

12486
Multi.prototype.exec_transaction = function exec_transaction (callback) {
87+
if (this.monitoring || this._client.monitoring) {
88+
var err = new RangeError(
89+
'Using transaction with a client that is in monitor mode does not work due to faulty return values of Redis.'
90+
);
91+
err.command = 'EXEC';
92+
err.code = 'EXECABORT';
93+
return utils.reply_in_order(this._client, callback, err);
94+
}
12595
var self = this;
12696
var len = self.queue.length;
12797
self.errors = [];
12898
self.callback = callback;
129-
self._client.cork(len + 2);
99+
self._client.cork();
130100
self.wants_buffers = new Array(len);
131-
pipeline_transaction_command(self, 'multi', []);
101+
pipeline_transaction_command(self, 'multi', [], -1);
132102
// Drain queue, callback will catch 'QUEUED' or error
133103
for (var index = 0; index < len; index++) {
134-
var args = self.queue.get(index);
135-
var command = args[0];
136-
var cb = args[2];
104+
// The commands may not be shifted off, since they are needed in the result handler
105+
var command_obj = self.queue.get(index);
106+
var command = command_obj[0];
107+
var cb = command_obj[2];
108+
var call_on_write = command_obj.length === 4 ? command_obj[3] : undefined;
137109
// Keep track of who wants buffer responses:
138110
if (self._client.options.detect_buffers) {
139111
self.wants_buffers[index] = false;
140-
for (var i = 0; i < args[1].length; i += 1) {
141-
if (args[1][i] instanceof Buffer) {
112+
for (var i = 0; i < command_obj[1].length; i += 1) {
113+
if (command_obj[1][i] instanceof Buffer) {
142114
self.wants_buffers[index] = true;
143115
break;
144116
}
145117
}
146118
}
147-
pipeline_transaction_command(self, command, args[1], index, cb);
119+
pipeline_transaction_command(self, command, command_obj[1], index, cb, call_on_write);
148120
}
149121

150122
self._client.internal_send_command('exec', [], function (err, replies) {
151123
multi_callback(self, err, replies);
152124
});
153125
self._client.uncork();
154-
self._client.writeDefault = self._client.writeStrings;
155126
return !self._client.should_buffer;
156127
};
157128

@@ -172,7 +143,18 @@ Multi.prototype.exec = Multi.prototype.EXEC = Multi.prototype.exec_batch = funct
172143
var self = this;
173144
var len = self.queue.length;
174145
var index = 0;
175-
var args;
146+
var command_obj;
147+
self._client.cork();
148+
if (!callback) {
149+
while (command_obj = self.queue.shift()) {
150+
self._client.internal_send_command(command_obj[0], command_obj[1], command_obj[2], (command_obj.length === 4 ? command_obj[3] : undefined));
151+
}
152+
self._client.uncork();
153+
return !self._client.should_buffer;
154+
} else if (len === 0) {
155+
utils.reply_in_order(self._client, callback, null, []);
156+
return !self._client.should_buffer;
157+
}
176158
var callback_without_own_cb = function (err, res) {
177159
if (err) {
178160
self.results.push(err);
@@ -191,31 +173,23 @@ Multi.prototype.exec = Multi.prototype.EXEC = Multi.prototype.exec_batch = funct
191173
callback(null, self.results);
192174
};
193175
};
194-
if (len === 0) {
195-
if (callback) {
196-
utils.reply_in_order(self._client, callback, null, []);
197-
}
198-
return true;
199-
}
200176
self.results = [];
201-
self._client.cork(len);
202-
while (args = self.queue.shift()) {
203-
var command = args[0];
177+
while (command_obj = self.queue.shift()) {
178+
var command = command_obj[0];
179+
var call_on_write = command_obj.length === 4 ? command_obj[3] : undefined;
204180
var cb;
205-
if (typeof args[2] === 'function') {
206-
cb = batch_callback(self, args[2], index);
181+
if (typeof command_obj[2] === 'function') {
182+
cb = batch_callback(self, command_obj[2], index);
207183
} else {
208184
cb = callback_without_own_cb;
209185
}
210186
if (typeof callback === 'function' && index === len - 1) {
211187
cb = last_callback(cb);
212188
}
213-
self._client.internal_send_command(command, args[1], cb);
189+
this._client.internal_send_command(command, command_obj[1], cb, call_on_write);
214190
index++;
215191
}
216-
self.queue = new Queue();
217192
self._client.uncork();
218-
self._client.writeDefault = self._client.writeStrings;
219193
return !self._client.should_buffer;
220194
};
221195

‎lib/rawObject.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use strict';
2+
3+
// Using a predefined object with this prototype is faster than calling `Object.create(null)` directly
4+
// This is needed to make sure `__proto__` and similar reserved words can be used
5+
function RawObject () {}
6+
RawObject.prototype = Object.create(null);
7+
8+
module.exports = RawObject;

‎lib/utils.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use strict';
22

3+
var RawObject = require('./rawObject');
4+
35
// hgetall converts its replies to an Object. If the reply is empty, null is returned.
4-
// These function are only called with internal data and have therefor always the same instanceof X
6+
// These function are only called with internal data and have therefore always the same instanceof X
57
function replyToObject (reply) {
68
// The reply might be a string or a buffer if this is called in a transaction (multi)
79
if (reply.length === 0 || !(reply instanceof Array)) {
810
return null;
911
}
10-
var obj = {};
12+
var obj = new RawObject();
1113
for (var i = 0; i < reply.length; i += 2) {
1214
obj[reply[i].toString('binary')] = reply[i + 1];
1315
}
@@ -39,8 +41,10 @@ function print (err, reply) {
3941
}
4042
}
4143

44+
var camelCase;
4245
// Deep clone arbitrary objects with arrays. Can't handle cyclic structures (results in a range error)
4346
// Any attribute with a non primitive value besides object and array will be passed by reference (e.g. Buffers, Maps, Functions)
47+
// All capital letters are going to be replaced with a lower case letter and a underscore infront of it
4448
function clone (obj) {
4549
var copy;
4650
if (Array.isArray(obj)) {
@@ -55,15 +59,27 @@ function clone (obj) {
5559
var elems = Object.keys(obj);
5660
var elem;
5761
while (elem = elems.pop()) {
58-
copy[elem] = clone(obj[elem]);
62+
// Accept camelCase options and convert them to snack_case
63+
var snack_case = elem.replace(/[A-Z][^A-Z]/g, '_$&').toLowerCase();
64+
// If camelCase is detected, pass it to the client, so all variables are going to be camelCased
65+
// There are no deep nested options objects yet, but let's handle this future proof
66+
if (snack_case !== elem.toLowerCase()) {
67+
camelCase = true;
68+
}
69+
copy[snack_case] = clone(obj[elem]);
5970
}
6071
return copy;
6172
}
6273
return obj;
6374
}
6475

6576
function convenienceClone (obj) {
66-
return clone(obj) || {};
77+
camelCase = false;
78+
obj = clone(obj) || {};
79+
if (camelCase) {
80+
obj.camel_case = true;
81+
}
82+
return obj;
6783
}
6884

6985
function callbackOrEmit (self, callback, err, res) {
@@ -74,8 +90,16 @@ function callbackOrEmit (self, callback, err, res) {
7490
}
7591
}
7692

77-
function replyInOrder (self, callback, err, res) {
78-
var command_obj = self.command_queue.peekBack() || self.offline_queue.peekBack();
93+
function replyInOrder (self, callback, err, res, queue) {
94+
// If the queue is explicitly passed, use that, otherwise fall back to the offline queue first,
95+
// as there might be commands in both queues at the same time
96+
var command_obj;
97+
/* istanbul ignore if: TODO: Remove this as soon as we test Redis 3.2 on travis */
98+
if (queue) {
99+
command_obj = queue.peekBack();
100+
} else {
101+
command_obj = self.offline_queue.peekBack() || self.command_queue.peekBack();
102+
}
79103
if (!command_obj) {
80104
process.nextTick(function () {
81105
callbackOrEmit(self, callback, err, res);

‎package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@
2121
"coverage": "nyc report --reporter=html",
2222
"benchmark": "node benchmarks/multi_bench.js",
2323
"test": "nyc --cache mocha ./test/*.js ./test/commands/*.js --timeout=8000",
24-
"pretest": "optional-dev-dependency hiredis",
2524
"posttest": "eslint . --fix"
2625
},
2726
"dependencies": {
2827
"double-ended-queue": "^2.1.0-0",
29-
"redis-commands": "^1.1.0",
30-
"redis-parser": "^1.2.0"
28+
"redis-commands": "^1.2.0",
29+
"redis-parser": "^1.3.0"
3130
},
3231
"engines": {
3332
"node": ">=0.10.0"
@@ -40,7 +39,6 @@
4039
"metrics": "^0.1.9",
4140
"mocha": "^2.3.2",
4241
"nyc": "^6.0.0",
43-
"optional-dev-dependency": "^1.1.0",
4442
"tcp-port-used": "^0.1.2",
4543
"uuid": "^2.0.1",
4644
"win-spawn": "^2.0.0"

‎test/auth.spec.js

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('client authentication', function () {
5353
client.auth(auth, function (err, res) {
5454
assert.strictEqual('retry worked', res);
5555
var now = Date.now();
56-
// Hint: setTimeout sometimes triggers early and therefor the value can be like one or two ms to early
56+
// Hint: setTimeout sometimes triggers early and therefore the value can be like one or two ms to early
5757
assert(now - time >= 98, 'Time should be above 100 ms (the reconnect time) and is ' + (now - time));
5858
assert(now - time < 225, 'Time should be below 255 ms (the reconnect should only take a bit above 100 ms) and is ' + (now - time));
5959
done();
@@ -160,24 +160,34 @@ describe('client authentication', function () {
160160
client.on('ready', done);
161161
});
162162

163-
it('reconnects with appropriate authentication', function (done) {
163+
it('reconnects with appropriate authentication while offline commands are present', function (done) {
164164
if (helper.redisProcess().spawnFailed()) this.skip();
165165

166166
client = redis.createClient.apply(null, args);
167167
client.auth(auth);
168168
client.on('ready', function () {
169-
if (this.times_connected === 1) {
170-
client.stream.destroy();
169+
if (this.times_connected < 3) {
170+
var interval = setInterval(function () {
171+
if (client.commandQueueLength !== 0) {
172+
return;
173+
}
174+
clearInterval(interval);
175+
interval = null;
176+
client.stream.destroy();
177+
client.set('foo', 'bar');
178+
client.get('foo'); // Errors would bubble
179+
assert.strictEqual(client.offlineQueueLength, 2);
180+
}, 1);
171181
} else {
172182
done();
173183
}
174184
});
175185
client.on('reconnecting', function (params) {
176-
assert.strictEqual(params.error.message, 'Stream connection closed');
186+
assert.strictEqual(params.error, null);
177187
});
178188
});
179189

180-
it('should return an error if the password is not of type string and a callback has been provided', function (done) {
190+
it('should return an error if the password is not correct and a callback has been provided', function (done) {
181191
if (helper.redisProcess().spawnFailed()) this.skip();
182192

183193
client = redis.createClient.apply(null, args);
@@ -192,7 +202,7 @@ describe('client authentication', function () {
192202
assert(async);
193203
});
194204

195-
it('should emit an error if the password is not of type string and no callback has been provided', function (done) {
205+
it('should emit an error if the password is not correct and no callback has been provided', function (done) {
196206
if (helper.redisProcess().spawnFailed()) this.skip();
197207

198208
client = redis.createClient.apply(null, args);
@@ -262,13 +272,13 @@ describe('client authentication', function () {
262272
var args = config.configureClient(parser, ip, {
263273
password: auth
264274
});
265-
client = redis.createClient.apply(redis.createClient, args);
275+
client = redis.createClient.apply(null, args);
266276
client.set('foo', 'bar');
267277
client.subscribe('somechannel', 'another channel', function (err, res) {
268278
client.once('ready', function () {
269279
assert.strictEqual(client.pub_sub_mode, 1);
270280
client.get('foo', function (err, res) {
271-
assert.strictEqual(err.message, 'ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context');
281+
assert(/ERR only \(P\)SUBSCRIBE \/ \(P\)UNSUBSCRIBE/.test(err.message));
272282
done();
273283
});
274284
});
@@ -281,6 +291,48 @@ describe('client authentication', function () {
281291
});
282292
});
283293
});
294+
295+
it('individual commands work properly with batch', function (done) {
296+
// quit => might return an error instead of "OK" in the exec callback... (if not connected)
297+
// auth => might return an error instead of "OK" in the exec callback... (if no password is required / still loading on Redis <= 2.4)
298+
// This could be fixed by checking the return value of the callback in the exec callback and
299+
// returning the manipulated [error, result] from the callback.
300+
// There should be a better solution though
301+
302+
var args = config.configureClient(parser, 'localhost', {
303+
noReadyCheck: true
304+
});
305+
client = redis.createClient.apply(null, args);
306+
assert.strictEqual(client.selected_db, undefined);
307+
var end = helper.callFuncAfter(done, 8);
308+
client.on('monitor', function () {
309+
end(); // Should be called for each command after monitor
310+
});
311+
client.batch()
312+
.auth(auth)
313+
.SELECT(5, function (err, res) {
314+
assert.strictEqual(client.selected_db, 5);
315+
assert.strictEqual(res, 'OK');
316+
assert.notDeepEqual(client.serverInfo.db5, { avg_ttl: 0, expires: 0, keys: 1 });
317+
})
318+
.monitor()
319+
.set('foo', 'bar', helper.isString('OK'))
320+
.INFO('stats', function (err, res) {
321+
assert.strictEqual(res.indexOf('# Stats\r\n'), 0);
322+
assert.strictEqual(client.serverInfo.sync_full, '0');
323+
})
324+
.get('foo', helper.isString('bar'))
325+
.subscribe(['foo', 'bar'])
326+
.unsubscribe('foo')
327+
.SUBSCRIBE('/foo', helper.isString('/foo'))
328+
.psubscribe('*')
329+
.quit(helper.isString('OK')) // this might be interesting
330+
.exec(function (err, res) {
331+
res[4] = res[4].substr(0, 9);
332+
assert.deepEqual(res, ['OK', 'OK', 'OK', 'OK', '# Stats\r\n', 'bar', 'bar', 'foo', '/foo', '*', 'OK']);
333+
end();
334+
});
335+
});
284336
});
285337
});
286338

‎test/batch.spec.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ describe("The 'batch' method", function () {
1212
describe('using ' + parser + ' and ' + ip, function () {
1313

1414
describe('when not connected', function () {
15-
// TODO: This is somewhat broken and should be fixed in v.3
16-
// The commands should return an error instead of returning an empty result
1715
var client;
1816

1917
beforeEach(function (done) {
@@ -24,7 +22,7 @@ describe("The 'batch' method", function () {
2422
client.on('end', done);
2523
});
2624

27-
it('returns an empty array', function (done) {
25+
it('returns an empty array for missing commands', function (done) {
2826
var batch = client.batch();
2927
batch.exec(function (err, res) {
3028
assert.strictEqual(err, null);
@@ -33,7 +31,17 @@ describe("The 'batch' method", function () {
3331
});
3432
});
3533

36-
it('returns an empty array if promisified', function () {
34+
it('returns an error for batch with commands', function (done) {
35+
var batch = client.batch();
36+
batch.set('foo', 'bar');
37+
batch.exec(function (err, res) {
38+
assert.strictEqual(err, null);
39+
assert.strictEqual(res[0].code, 'NR_CLOSED');
40+
done();
41+
});
42+
});
43+
44+
it('returns an empty array for missing commands if promisified', function () {
3745
return client.batch().execAsync().then(function (res) {
3846
assert.strictEqual(res.length, 0);
3947
});

‎test/commands/client.spec.js

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,89 @@ describe("The 'client' method", function () {
3030
});
3131

3232
it("lists connected clients when invoked with multi's chaining syntax", function (done) {
33-
client.multi().client('list').exec(function (err, results) {
34-
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
35-
return done();
36-
});
33+
client.multi().client('list', helper.isType.string()).exec(helper.match(pattern, done));
3734
});
3835

3936
it('lists connected clients when invoked with array syntax on client', function (done) {
40-
client.multi().client(['list']).exec(function (err, results) {
41-
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
42-
return done();
43-
});
37+
client.multi().client(['list']).exec(helper.match(pattern, done));
4438
});
4539

4640
it("lists connected clients when invoked with multi's array syntax", function (done) {
4741
client.multi([
4842
['client', 'list']
49-
]).exec(function (err, results) {
50-
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
51-
return done();
43+
]).exec(helper.match(pattern, done));
44+
});
45+
});
46+
47+
describe('reply', function () {
48+
describe('as normal command', function () {
49+
it('on', function (done) {
50+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
51+
assert.strictEqual(client.reply, 'ON');
52+
client.client('reply', 'on', helper.isString('OK'));
53+
assert.strictEqual(client.reply, 'ON');
54+
client.set('foo', 'bar', done);
55+
});
56+
57+
it('off', function (done) {
58+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
59+
assert.strictEqual(client.reply, 'ON');
60+
client.client(new Buffer('REPLY'), 'OFF', helper.isUndefined());
61+
assert.strictEqual(client.reply, 'OFF');
62+
client.set('foo', 'bar', helper.isUndefined(done));
63+
});
64+
65+
it('skip', function (done) {
66+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
67+
assert.strictEqual(client.reply, 'ON');
68+
client.client('REPLY', new Buffer('SKIP'), helper.isUndefined());
69+
assert.strictEqual(client.reply, 'SKIP_ONE_MORE');
70+
client.set('foo', 'bar', helper.isUndefined());
71+
client.get('foo', helper.isString('bar', done));
72+
});
73+
});
74+
75+
describe('in a batch context', function () {
76+
it('on', function (done) {
77+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
78+
var batch = client.batch();
79+
assert.strictEqual(client.reply, 'ON');
80+
batch.client('reply', 'on', helper.isString('OK'));
81+
assert.strictEqual(client.reply, 'ON');
82+
batch.set('foo', 'bar');
83+
batch.exec(function (err, res) {
84+
assert.deepEqual(res, ['OK', 'OK']);
85+
done(err);
86+
});
87+
});
88+
89+
it('off', function (done) {
90+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
91+
var batch = client.batch();
92+
assert.strictEqual(client.reply, 'ON');
93+
batch.set('hello', 'world');
94+
batch.client(new Buffer('REPLY'), new Buffer('OFF'), helper.isUndefined());
95+
batch.set('foo', 'bar', helper.isUndefined());
96+
batch.exec(function (err, res) {
97+
assert.strictEqual(client.reply, 'OFF');
98+
assert.deepEqual(res, ['OK', undefined, undefined]);
99+
done(err);
100+
});
101+
});
102+
103+
it('skip', function (done) {
104+
helper.serverVersionAtLeast.call(this, client, [3, 2, 0]);
105+
assert.strictEqual(client.reply, 'ON');
106+
client.batch()
107+
.set('hello', 'world')
108+
.client('REPLY', 'SKIP', helper.isUndefined())
109+
.set('foo', 'bar', helper.isUndefined())
110+
.get('foo')
111+
.exec(function (err, res) {
112+
assert.strictEqual(client.reply, 'ON');
113+
assert.deepEqual(res, ['OK', undefined, undefined, 'bar']);
114+
done(err);
115+
});
52116
});
53117
});
54118
});
@@ -57,7 +121,7 @@ describe("The 'client' method", function () {
57121
var client2;
58122

59123
beforeEach(function (done) {
60-
client2 = redis.createClient.apply(redis.createClient, args);
124+
client2 = redis.createClient.apply(null, args);
61125
client2.once('ready', function () {
62126
done();
63127
});
@@ -72,9 +136,9 @@ describe("The 'client' method", function () {
72136
// per chunk. So the execution order is only garanteed on each client
73137
var end = helper.callFuncAfter(done, 2);
74138

75-
client.client('setname', 'RUTH', helper.isString('OK'));
76-
client2.client('setname', 'RENEE', helper.isString('OK'));
77-
client2.client('setname', 'MARTIN', helper.isString('OK'));
139+
client.client('setname', 'RUTH');
140+
client2.client('setname', ['RENEE'], helper.isString('OK'));
141+
client2.client(['setname', 'MARTIN'], helper.isString('OK'));
78142
client2.client('getname', function (err, res) {
79143
assert.equal(res, 'MARTIN');
80144
end();

‎test/commands/dbsize.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("The 'dbsize' method", function () {
3131

3232
it('reports an error', function (done) {
3333
client.dbsize([], function (err, res) {
34-
assert(err.message.match(/The connection has already been closed/));
34+
assert(err.message.match(/The connection is already closed/));
3535
done();
3636
});
3737
});

‎test/commands/flushdb.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("The 'flushdb' method", function () {
3131

3232
it('reports an error', function (done) {
3333
client.flushdb(function (err, res) {
34-
assert(err.message.match(/The connection has already been closed/));
34+
assert(err.message.match(/The connection is already closed/));
3535
done();
3636
});
3737
});

‎test/commands/get.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ describe("The 'get' method", function () {
3131

3232
it('reports an error', function (done) {
3333
client.get(key, function (err, res) {
34-
assert(err.message.match(/The connection has already been closed/));
34+
assert(err.message.match(/The connection is already closed/));
3535
done();
3636
});
3737
});
3838

3939
it('reports an error promisified', function () {
4040
return client.getAsync(key).then(assert, function (err) {
41-
assert(err.message.match(/The connection has already been closed/));
41+
assert(err.message.match(/The connection is already closed/));
4242
});
4343
});
4444
});

‎test/commands/getset.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe("The 'getset' method", function () {
3232

3333
it('reports an error', function (done) {
3434
client.get(key, function (err, res) {
35-
assert(err.message.match(/The connection has already been closed/));
35+
assert(err.message.match(/The connection is already closed/));
3636
done();
3737
});
3838
});

‎test/commands/hgetall.spec.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,31 @@ describe("The 'hgetall' method", function () {
2222
});
2323

2424
it('handles simple keys and values', function (done) {
25-
client.hmset(['hosts', 'mjr', '1', 'another', '23', 'home', '1234'], helper.isString('OK'));
25+
client.hmset(['hosts', '__proto__', '1', 'another', '23', 'home', '1234'], helper.isString('OK'));
2626
client.HGETALL(['hosts'], function (err, obj) {
27-
assert.strictEqual(3, Object.keys(obj).length);
28-
assert.strictEqual('1', obj.mjr.toString());
27+
if (!/^v0\.10/.test(process.version)) {
28+
assert.strictEqual(3, Object.keys(obj).length);
29+
assert.strictEqual('1', obj.__proto__.toString()); // eslint-disable-line no-proto
30+
}
2931
assert.strictEqual('23', obj.another.toString());
3032
assert.strictEqual('1234', obj.home.toString());
31-
return done(err);
33+
done(err);
3234
});
3335
});
3436

3537
it('handles fetching keys set using an object', function (done) {
36-
client.HMSET('msg_test', { message: 'hello' }, helper.isString('OK'));
38+
client.batch().HMSET('msg_test', { message: 'hello' }, undefined).exec();
3739
client.hgetall('msg_test', function (err, obj) {
3840
assert.strictEqual(1, Object.keys(obj).length);
3941
assert.strictEqual(obj.message, 'hello');
40-
return done(err);
42+
done(err);
4143
});
4244
});
4345

4446
it('handles fetching a messing key', function (done) {
4547
client.hgetall('missing', function (err, obj) {
4648
assert.strictEqual(null, obj);
47-
return done(err);
49+
done(err);
4850
});
4951
});
5052
});

‎test/commands/hmset.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe("The 'hmset' method", function () {
3939
});
4040

4141
it('handles object-style syntax and the key being a number', function (done) {
42-
client.HMSET(231232, {'0123456789': 'abcdefghij', 'some manner of key': 'a type of value', 'otherTypes': 555}, helper.isString('OK'));
42+
client.HMSET(231232, {'0123456789': 'abcdefghij', 'some manner of key': 'a type of value', 'otherTypes': 555}, undefined);
4343
client.HGETALL(231232, function (err, obj) {
4444
assert.equal(obj['0123456789'], 'abcdefghij');
4545
assert.equal(obj['some manner of key'], 'a type of value');

‎test/commands/info.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ describe("The 'info' method", function () {
2323
client.end(true);
2424
});
2525

26-
it('update server_info after a info command', function (done) {
26+
it('update serverInfo after a info command', function (done) {
2727
client.set('foo', 'bar');
2828
client.info();
2929
client.select(2, function () {
30-
assert.strictEqual(client.server_info.db2, undefined);
30+
assert.strictEqual(client.serverInfo.db2, undefined);
3131
});
3232
client.set('foo', 'bar');
3333
client.info();
3434
setTimeout(function () {
35-
assert.strictEqual(typeof client.server_info.db2, 'object');
35+
assert.strictEqual(typeof client.serverInfo.db2, 'object');
3636
done();
3737
}, 30);
3838
});

‎test/commands/mset.spec.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe("The 'mset' method", function () {
3333

3434
it('reports an error', function (done) {
3535
client.mset(key, value, key2, value2, function (err, res) {
36-
assert(err.message.match(/The connection has already been closed/));
36+
assert(err.message.match(/The connection is already closed/));
3737
done();
3838
});
3939
});
@@ -96,7 +96,8 @@ describe("The 'mset' method", function () {
9696
// this behavior is different from the 'set' behavior.
9797
it('emits an error', function (done) {
9898
client.on('error', function (err) {
99-
assert.equal(err.message, "ERR wrong number of arguments for 'mset' command");
99+
assert.strictEqual(err.message, "ERR wrong number of arguments for 'mset' command");
100+
assert.strictEqual(err.name, 'ReplyError');
100101
done();
101102
});
102103

‎test/commands/select.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe("The 'select' method", function () {
2323

2424
it('returns an error if redis is not connected', function (done) {
2525
var buffering = client.select(1, function (err, res) {
26-
assert(err.message.match(/The connection has already been closed/));
26+
assert(err.message.match(/The connection is already closed/));
2727
done();
2828
});
2929
assert(typeof buffering === 'boolean');

‎test/commands/set.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("The 'set' method", function () {
3131

3232
it('reports an error', function (done) {
3333
client.set(key, value, function (err, res) {
34-
assert(err.message.match(/The connection has already been closed/));
34+
assert(err.message.match(/The connection is already closed/));
3535
done();
3636
});
3737
});

‎test/connection.spec.js

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('connection tests', function () {
2020
it('unofficially support for a private stream', function () {
2121
// While using a private stream, reconnection and other features are not going to work properly.
2222
// Besides that some functions also have to be monkey patched to be safe from errors in this case.
23-
// Therefor this is not officially supported!
23+
// Therefore this is not officially supported!
2424
var socket = new net.Socket();
2525
client = new redis.RedisClient({
2626
prefix: 'test'
@@ -53,7 +53,7 @@ describe('connection tests', function () {
5353
}
5454
});
5555
client.set('foo', 'bar', function (err, res) {
56-
assert.strictEqual(err.message, 'Redis connection gone from close event.');
56+
assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
5757
called = -1;
5858
});
5959
});
@@ -62,7 +62,7 @@ describe('connection tests', function () {
6262
var called = false;
6363
client = redis.createClient(9999);
6464
client.set('foo', 'bar', function (err, res) {
65-
assert.strictEqual(err.message, 'Redis connection gone from close event.');
65+
assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
6666
called = true;
6767
});
6868
var bool = client.quit(function (err, res) {
@@ -93,6 +93,27 @@ describe('connection tests', function () {
9393
assert.strictEqual(bool, false);
9494
});
9595

96+
it('calling quit while connected without offline queue should end the connection when all commands have finished', function (done) {
97+
var called = false;
98+
client = redis.createClient({
99+
enable_offline_queue: false
100+
});
101+
client.on('ready', function () {
102+
client.set('foo', 'bar', function (err, res) {
103+
assert.strictEqual(res, 'OK');
104+
called = true;
105+
});
106+
var bool = client.quit(function (err, res) {
107+
assert.strictEqual(res, 'OK');
108+
assert.strictEqual(err, null);
109+
assert(called);
110+
done();
111+
});
112+
// TODO: In v.3 the quit command would be fired right away, so bool should be true
113+
assert.strictEqual(bool, true);
114+
});
115+
});
116+
96117
it('do not quit before connected or a connection issue is detected', function (done) {
97118
client = redis.createClient();
98119
client.set('foo', 'bar', helper.isString('OK'));
@@ -132,12 +153,14 @@ describe('connection tests', function () {
132153

133154
describe('on lost connection', function () {
134155
it('emit an error after max retry attempts and do not try to reconnect afterwards', function (done) {
135-
var max_attempts = 3;
156+
var maxAttempts = 3;
136157
var options = {
137158
parser: parser,
138-
max_attempts: max_attempts
159+
maxAttempts: maxAttempts
139160
};
140161
client = redis.createClient(options);
162+
assert.strictEqual(client.retryBackoff, 1.7);
163+
assert.strictEqual(client.retryDelay, 200);
141164
assert.strictEqual(Object.keys(options).length, 2);
142165
var calls = 0;
143166

@@ -152,7 +175,7 @@ describe('connection tests', function () {
152175
client.on('error', function (err) {
153176
if (/Redis connection in broken state: maximum connection attempts.*?exceeded./.test(err.message)) {
154177
process.nextTick(function () { // End is called after the error got emitted
155-
assert.strictEqual(calls, max_attempts - 1);
178+
assert.strictEqual(calls, maxAttempts - 1);
156179
assert.strictEqual(client.emitted_end, true);
157180
assert.strictEqual(client.connected, false);
158181
assert.strictEqual(client.ready, false);
@@ -236,74 +259,89 @@ describe('connection tests', function () {
236259

237260
it('emits error once if reconnecting after command has been executed but not yet returned without callback', function (done) {
238261
client = redis.createClient.apply(null, args);
239-
client.on('error', function (err) {
240-
assert.strictEqual(err.code, 'UNCERTAIN_STATE');
241-
done();
242-
});
243262

244263
client.on('ready', function () {
245-
client.set('foo', 'bar');
264+
client.set('foo', 'bar', function (err) {
265+
assert.strictEqual(err.code, 'UNCERTAIN_STATE');
266+
done();
267+
});
246268
// Abort connection before the value returned
247269
client.stream.destroy();
248270
});
249271
});
250272

251-
it('retry_strategy used to reconnect with individual error', function (done) {
273+
it('retryStrategy used to reconnect with individual error', function (done) {
252274
var text = '';
253275
var unhookIntercept = intercept(function (data) {
254276
text += data;
255277
return '';
256278
});
257-
var end = helper.callFuncAfter(done, 2);
258279
client = redis.createClient({
259-
retry_strategy: function (options) {
260-
if (options.total_retry_time > 150) {
280+
retryStrategy: function (options) {
281+
if (options.totalRetryTime > 150) {
261282
client.set('foo', 'bar', function (err, res) {
262-
assert.strictEqual(err.message, 'Connection timeout');
263-
end();
283+
assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
284+
assert.strictEqual(err.origin.message, 'Connection timeout');
285+
done();
264286
});
265287
// Pass a individual error message to the error handler
266288
return new Error('Connection timeout');
267289
}
268290
return Math.min(options.attempt * 25, 200);
269291
},
270-
max_attempts: 5,
271-
retry_max_delay: 123,
292+
maxAttempts: 5,
293+
retryMaxDelay: 123,
272294
port: 9999
273295
});
274-
275-
client.on('error', function (err) {
276-
unhookIntercept();
296+
process.nextTick(function () {
277297
assert.strictEqual(
278298
text,
279299
'node_redis: WARNING: You activated the retry_strategy and max_attempts at the same time. This is not possible and max_attempts will be ignored.\n' +
280300
'node_redis: WARNING: You activated the retry_strategy and retry_max_delay at the same time. This is not possible and retry_max_delay will be ignored.\n'
281301
);
282-
assert.strictEqual(err.message, 'Connection timeout');
283-
assert(!err.code);
284-
end();
302+
unhookIntercept();
285303
});
286304
});
287305

288306
it('retry_strategy used to reconnect', function (done) {
289-
var end = helper.callFuncAfter(done, 2);
290307
client = redis.createClient({
291308
retry_strategy: function (options) {
292309
if (options.total_retry_time > 150) {
293310
client.set('foo', 'bar', function (err, res) {
294-
assert.strictEqual(err.code, 'ECONNREFUSED');
295-
end();
311+
assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
312+
assert.strictEqual(err.code, 'NR_CLOSED');
313+
assert.strictEqual(err.origin.code, 'ECONNREFUSED');
314+
done();
296315
});
297316
return false;
298317
}
299318
return Math.min(options.attempt * 25, 200);
300319
},
301320
port: 9999
302321
});
322+
});
303323

324+
it('retryStrategy used to reconnect with defaults', function (done) {
325+
var unhookIntercept = intercept(function () {
326+
return '';
327+
});
328+
redis.debugMode = true;
329+
client = redis.createClient({
330+
retryStrategy: function (options) {
331+
client.set('foo', 'bar');
332+
assert(redis.debugMode);
333+
return null;
334+
}
335+
});
336+
setTimeout(function () {
337+
client.stream.destroy();
338+
}, 50);
304339
client.on('error', function (err) {
305-
assert.strictEqual(err.code, 'ECONNREFUSED');
306-
end();
340+
assert.strictEqual(err.code, 'NR_CLOSED');
341+
assert.strictEqual(err.message, 'Stream connection ended and command aborted.');
342+
unhookIntercept();
343+
redis.debugMode = false;
344+
done();
307345
});
308346
});
309347
});
@@ -344,7 +382,7 @@ describe('connection tests', function () {
344382
});
345383
});
346384

347-
it('use the system socket timeout if the connect_timeout has not been provided', function () {
385+
it('use the system socket timeout if the connect_timeout has not been provided', function (done) {
348386
client = redis.createClient({
349387
parser: parser,
350388
host: '2001:db8::ff00:42:8329' // auto detect ip v6
@@ -353,6 +391,7 @@ describe('connection tests', function () {
353391
assert.strictEqual(client.connection_options.family, 6);
354392
process.nextTick(function () {
355393
assert.strictEqual(client.stream.listeners('timeout').length, 0);
394+
done();
356395
});
357396
});
358397

‎test/helper.js

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ if (!process.env.REDIS_TESTS_STARTED) {
2929
});
3030
}
3131

32+
function arrayHelper (results) {
33+
if (results instanceof Array) {
34+
assert.strictEqual(results.length, 1, 'The array length may only be one element');
35+
return results[0];
36+
}
37+
return results;
38+
}
39+
3240
module.exports = {
3341
redisProcess: function () {
3442
return rp;
@@ -52,27 +60,38 @@ module.exports = {
5260
},
5361
isNumber: function (expected, done) {
5462
return function (err, results) {
55-
assert.strictEqual(null, err, 'expected ' + expected + ', got error: ' + err);
56-
assert.strictEqual(expected, results, expected + ' !== ' + results);
63+
assert.strictEqual(err, null, 'expected ' + expected + ', got error: ' + err);
64+
results = arrayHelper(results);
65+
assert.strictEqual(results, expected, expected + ' !== ' + results);
5766
assert.strictEqual(typeof results, 'number', 'expected a number, got ' + typeof results);
5867
if (done) done();
5968
};
6069
},
6170
isString: function (str, done) {
6271
str = '' + str; // Make sure it's a string
6372
return function (err, results) {
64-
assert.strictEqual(null, err, "expected string '" + str + "', got error: " + err);
73+
assert.strictEqual(err, null, "expected string '" + str + "', got error: " + err);
74+
results = arrayHelper(results);
6575
if (Buffer.isBuffer(results)) { // If options are passed to return either strings or buffers...
6676
results = results.toString();
6777
}
68-
assert.strictEqual(str, results, str + ' does not match ' + results);
78+
assert.strictEqual(results, str, str + ' does not match ' + results);
6979
if (done) done();
7080
};
7181
},
7282
isNull: function (done) {
7383
return function (err, results) {
74-
assert.strictEqual(null, err, 'expected null, got error: ' + err);
75-
assert.strictEqual(null, results, results + ' is not null');
84+
assert.strictEqual(err, null, 'expected null, got error: ' + err);
85+
results = arrayHelper(results);
86+
assert.strictEqual(results, null, results + ' is not null');
87+
if (done) done();
88+
};
89+
},
90+
isUndefined: function (done) {
91+
return function (err, results) {
92+
assert.strictEqual(err, null, 'expected null, got error: ' + err);
93+
results = arrayHelper(results);
94+
assert.strictEqual(results, undefined, results + ' is not undefined');
7695
if (done) done();
7796
};
7897
},
@@ -91,27 +110,39 @@ module.exports = {
91110
isType: {
92111
number: function (done) {
93112
return function (err, results) {
94-
assert.strictEqual(null, err, 'expected any number, got error: ' + err);
113+
assert.strictEqual(err, null, 'expected any number, got error: ' + err);
95114
assert.strictEqual(typeof results, 'number', results + ' is not a number');
96115
if (done) done();
97116
};
98117
},
118+
string: function (done) {
119+
return function (err, results) {
120+
assert.strictEqual(err, null, 'expected any string, got error: ' + err);
121+
assert.strictEqual(typeof results, 'string', results + ' is not a string');
122+
if (done) done();
123+
};
124+
},
99125
positiveNumber: function (done) {
100126
return function (err, results) {
101-
assert.strictEqual(null, err, 'expected positive number, got error: ' + err);
102-
assert.strictEqual(true, (results > 0), results + ' is not a positive number');
127+
assert.strictEqual(err, null, 'expected positive number, got error: ' + err);
128+
assert(results > 0, results + ' is not a positive number');
103129
if (done) done();
104130
};
105131
}
106132
},
107133
match: function (pattern, done) {
108134
return function (err, results) {
109-
assert.strictEqual(null, err, 'expected ' + pattern.toString() + ', got error: ' + err);
135+
assert.strictEqual(err, null, 'expected ' + pattern.toString() + ', got error: ' + err);
136+
results = arrayHelper(results);
110137
assert(pattern.test(results), "expected string '" + results + "' to match " + pattern.toString());
111138
if (done) done();
112139
};
113140
},
114141
serverVersionAtLeast: function (connection, desired_version) {
142+
// Wait until a connection has established (otherwise a timeout is going to be triggered at some point)
143+
if (Object.keys(connection.server_info).length === 0) {
144+
throw new Error('Version check not possible as the client is not yet ready or did not expose the version');
145+
}
115146
// Return true if the server version >= desired_version
116147
var version = connection.server_info.versions;
117148
for (var i = 0; i < 3; i++) {
@@ -132,10 +163,11 @@ module.exports = {
132163
}
133164
var parsers = ['javascript'];
134165
var protocols = ['IPv4'];
135-
try {
136-
require('hiredis');
137-
parsers.push('hiredis');
138-
} catch (e) {/* ignore eslint */}
166+
// The js parser works the same as the hiredis parser, just activate this if you want to be on the safe side
167+
// try {
168+
// require('hiredis');
169+
// parsers.push('hiredis');
170+
// } catch (e) {/* ignore eslint */}
139171
if (process.platform !== 'win32') {
140172
protocols.push('IPv6', '/tmp/redis.sock');
141173
}

‎test/multi.spec.js

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
var assert = require('assert');
44
var config = require('./lib/config');
55
var helper = require('./helper');
6+
var utils = require('../lib/utils');
67
var redis = config.redis;
78
var zlib = require('zlib');
89
var client;
@@ -110,31 +111,78 @@ describe("The 'multi' method", function () {
110111
it('reports an error', function (done) {
111112
var multi = client.multi();
112113
var notBuffering = multi.exec(function (err, res) {
113-
assert(err.message.match(/The connection has already been closed/));
114+
assert(err.message.match(/The connection is already closed/));
114115
done();
115116
});
116117
assert.strictEqual(notBuffering, false);
117118
});
118119

119120
it('reports an error if promisified', function () {
120121
return client.multi().execAsync().catch(function (err) {
121-
assert(err.message.match(/The connection has already been closed/));
122+
assert(err.message.match(/The connection is already closed/));
122123
});
123124
});
124125
});
125126

126127
describe('when connected', function () {
127128

128-
beforeEach(function (done) {
129+
beforeEach(function () {
129130
client = redis.createClient.apply(null, args);
130-
client.once('connect', done);
131+
});
132+
133+
describe('monitor and transactions do not work together', function () {
134+
135+
it('results in a execabort', function (done) {
136+
// Check that transactions in combination with monitor result in an error
137+
client.monitor(function (e) {
138+
client.on('error', function (err) {
139+
assert.strictEqual(err.code, 'EXECABORT');
140+
done();
141+
});
142+
var multi = client.multi();
143+
multi.set('hello', 'world');
144+
multi.exec();
145+
});
146+
});
147+
148+
it('results in a execabort #2', function (done) {
149+
// Check that using monitor with a transactions results in an error
150+
client.multi().set('foo', 'bar').monitor().exec(function (err, res) {
151+
assert.strictEqual(err.code, 'EXECABORT');
152+
done();
153+
});
154+
});
155+
156+
it('sanity check', function (done) {
157+
// Remove the listener and add it back again after the error
158+
var mochaListener = helper.removeMochaListener();
159+
process.on('uncaughtException', function (err) {
160+
helper.removeMochaListener();
161+
process.on('uncaughtException', mochaListener);
162+
done();
163+
});
164+
// Check if Redis still has the error
165+
client.monitor();
166+
client.send_command('multi');
167+
client.send_command('set', ['foo', 'bar']);
168+
client.send_command('get', ['foo']);
169+
client.send_command('exec', function (err, res) {
170+
// res[0] is going to be the monitor result of set
171+
// res[1] is going to be the result of the set command
172+
assert(utils.monitor_regex.test(res[0]));
173+
assert.strictEqual(res[1], 'OK');
174+
assert.strictEqual(res.length, 2);
175+
client.end(false);
176+
});
177+
});
131178
});
132179

133180
it('executes a pipelined multi properly in combination with the offline queue', function (done) {
134181
var multi1 = client.multi();
135182
multi1.set('m1', '123');
136183
multi1.get('m1');
137184
multi1.exec(done);
185+
assert.strictEqual(client.offline_queue.length, 4);
138186
});
139187

140188
it('executes a pipelined multi properly after a reconnect in combination with the offline queue', function (done) {
@@ -180,7 +228,8 @@ describe("The 'multi' method", function () {
180228

181229
client.multi([['set', 'foo', 'bar'], ['get', 'foo']]).exec(function (err, res) {
182230
assert(/Redis connection in broken state/.test(err.message));
183-
assert.strictEqual(err.errors.length, 0);
231+
assert.strictEqual(err.errors.length, 2);
232+
assert.strictEqual(err.errors[0].args.length, 2);
184233
});
185234
});
186235

@@ -255,12 +304,12 @@ describe("The 'multi' method", function () {
255304
multi2.set('m2', '456');
256305
multi1.set('m1', '123');
257306
multi1.get('m1');
258-
multi2.get('m2');
307+
multi2.get('m1');
259308
multi2.ping();
260309

261310
multi1.exec(end);
262311
multi2.exec(function (err, res) {
263-
assert.strictEqual(res[1], '456');
312+
assert.strictEqual(res[1], '123');
264313
end();
265314
});
266315
});
@@ -571,7 +620,7 @@ describe("The 'multi' method", function () {
571620
test = true;
572621
};
573622
multi.set('baz', 'binary');
574-
multi.exec_atomic();
623+
multi.EXEC_ATOMIC();
575624
assert(test);
576625
});
577626

@@ -612,16 +661,56 @@ describe("The 'multi' method", function () {
612661
});
613662

614663
it('emits error once if reconnecting after multi has been executed but not yet returned without callback', function (done) {
664+
// NOTE: If uncork is called async by postponing it to the next tick, this behavior is going to change.
665+
// The command won't be processed anymore two errors are returned instead of one
615666
client.on('error', function (err) {
616667
assert.strictEqual(err.code, 'UNCERTAIN_STATE');
617-
done();
668+
client.get('foo', function (err, res) {
669+
assert.strictEqual(res, 'bar');
670+
done();
671+
});
618672
});
619673

674+
// The commands should still be fired, no matter that the socket is destroyed on the same tick
620675
client.multi().set('foo', 'bar').get('foo').exec();
621676
// Abort connection before the value returned
622677
client.stream.destroy();
623678
});
624679

680+
it('indivdual commands work properly with multi', function (done) {
681+
// Neither of the following work properly in a transactions:
682+
// (This is due to Redis not returning the reply as expected / resulting in undefined behavior)
683+
// (Likely there are more commands that do not work with a transaction)
684+
//
685+
// auth => can't be called after a multi command
686+
// monitor => results in faulty return values e.g. multi().monitor().set('foo', 'bar').get('foo')
687+
// returns ['OK, 'OK', 'monitor reply'] instead of ['OK', 'OK', 'bar']
688+
// quit => ends the connection before the exec
689+
// client reply skip|off => results in weird return values. Not sure what exactly happens
690+
// subscribe => enters subscribe mode and this does not work in combination with exec (the same for psubscribe, unsubscribe...)
691+
//
692+
693+
assert.strictEqual(client.selected_db, undefined);
694+
var multi = client.multi();
695+
multi.select(5, function (err, res) {
696+
assert.strictEqual(client.selected_db, 5);
697+
assert.strictEqual(res, 'OK');
698+
assert.notDeepEqual(client.server_info.db5, { avg_ttl: 0, expires: 0, keys: 1 });
699+
});
700+
// multi.client('reply', 'on', helper.isString('OK')); // Redis v.3.2
701+
multi.set('foo', 'bar', helper.isString('OK'));
702+
multi.info(function (err, res) {
703+
assert.strictEqual(res.indexOf('# Server\r\nredis_version:'), 0);
704+
assert.deepEqual(client.server_info.db5, { avg_ttl: 0, expires: 0, keys: 1 });
705+
});
706+
multi.get('foo', helper.isString('bar'));
707+
multi.exec(function (err, res) {
708+
res[2] = res[2].substr(0, 10);
709+
assert.deepEqual(res, ['OK', 'OK', '# Server\r\n', 'bar']);
710+
done();
711+
});
712+
});
713+
625714
});
626715
});
627716
});

‎test/node_redis.spec.js

Lines changed: 219 additions & 51 deletions
Large diffs are not rendered by default.

‎test/pubsub.spec.js

Lines changed: 99 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ describe('publish/subscribe', function () {
1919
beforeEach(function (done) {
2020
var end = helper.callFuncAfter(done, 2);
2121

22-
pub = redis.createClient.apply(redis.createClient, args);
23-
sub = redis.createClient.apply(redis.createClient, args);
22+
pub = redis.createClient.apply(null, args);
23+
sub = redis.createClient.apply(null, args);
2424
pub.once('connect', function () {
2525
pub.flushdb(function () {
2626
end();
@@ -79,6 +79,7 @@ describe('publish/subscribe', function () {
7979

8080
it('does not fire subscribe events after reconnecting', function (done) {
8181
var i = 0;
82+
var end = helper.callFuncAfter(done, 2);
8283
sub.on('subscribe', function (chnl, count) {
8384
assert.strictEqual(typeof count, 'number');
8485
assert.strictEqual(++i, count);
@@ -91,9 +92,10 @@ describe('publish/subscribe', function () {
9192
sub.unsubscribe(function (err, res) { // Do not pass a channel here!
9293
assert.strictEqual(sub.pub_sub_mode, 2);
9394
assert.deepEqual(sub.subscription_set, {});
95+
end();
9496
});
9597
sub.set('foo', 'bar', helper.isString('OK'));
96-
sub.subscribe(channel2, done);
98+
sub.subscribe(channel2, end);
9799
});
98100
});
99101

@@ -181,74 +183,46 @@ describe('publish/subscribe', function () {
181183
sub.subscribe('chan9');
182184
sub.unsubscribe('chan9');
183185
pub.publish('chan8', 'something');
184-
sub.subscribe('chan9', function () {
185-
return done();
186-
});
186+
sub.subscribe('chan9', done);
187187
});
188188

189189
it('handles SUB_UNSUB_MSG_SUB 2', function (done) {
190-
sub.psubscribe('abc*');
190+
sub.psubscribe('abc*', helper.isString('abc*'));
191191
sub.subscribe('xyz');
192192
sub.unsubscribe('xyz');
193193
pub.publish('abcd', 'something');
194-
sub.subscribe('xyz', function () {
195-
return done();
196-
});
194+
sub.subscribe('xyz', done);
197195
});
198196

199197
it('emits end event if quit is called from within subscribe', function (done) {
200-
sub.on('end', function () {
201-
return done();
202-
});
198+
sub.on('end', done);
203199
sub.on('subscribe', function (chnl, count) {
204200
sub.quit();
205201
});
206202
sub.subscribe(channel);
207203
});
208204

209-
it('handles SUBSCRIBE_CLOSE_RESUBSCRIBE', function (done) {
205+
it('subscribe; close; resubscribe with prototype inherited property names', function (done) {
210206
var count = 0;
211-
/* Create two clients. c1 subscribes to two channels, c2 will publish to them.
212-
c2 publishes the first message.
213-
c1 gets the message and drops its connection. It must resubscribe itself.
214-
When it resubscribes, c2 publishes the second message, on the same channel
215-
c1 gets the message and drops its connection. It must resubscribe itself, again.
216-
When it resubscribes, c2 publishes the third message, on the second channel
217-
c1 gets the message and drops its connection. When it reconnects, the test ends.
218-
*/
207+
var channels = ['__proto__', 'channel 2'];
208+
var msg = ['hi from channel __proto__', 'hi from channel 2'];
209+
219210
sub.on('message', function (channel, message) {
220-
if (channel === 'chan1') {
221-
assert.strictEqual(message, 'hi on channel 1');
222-
sub.stream.end();
223-
} else if (channel === 'chan2') {
224-
assert.strictEqual(message, 'hi on channel 2');
225-
sub.stream.end();
226-
} else {
227-
sub.quit();
228-
pub.quit();
229-
assert.fail('test failed');
230-
}
211+
var n = Math.max(count - 1, 0);
212+
assert.strictEqual(channel, channels[n]);
213+
assert.strictEqual(message, msg[n]);
214+
if (count === 2) return done();
215+
sub.stream.end();
231216
});
232217

233-
sub.subscribe('chan1', 'chan2');
218+
sub.subscribe(channels);
234219

235220
sub.on('ready', function (err, results) {
221+
pub.publish(channels[count], msg[count]);
236222
count++;
237-
if (count === 1) {
238-
pub.publish('chan1', 'hi on channel 1');
239-
return;
240-
} else if (count === 2) {
241-
pub.publish('chan2', 'hi on channel 2');
242-
} else {
243-
sub.quit(function () {
244-
pub.quit(function () {
245-
return done();
246-
});
247-
});
248-
}
249223
});
250224

251-
pub.publish('chan1', 'hi on channel 1');
225+
pub.publish(channels[count], msg[count]);
252226
});
253227
});
254228

@@ -258,6 +232,10 @@ describe('publish/subscribe', function () {
258232
var end = helper.callFuncAfter(done, 2);
259233
sub.select(3);
260234
sub.set('foo', 'bar');
235+
sub.set('failure', helper.isError()); // Triggering a warning while subscribing should work
236+
sub.mget('foo', 'bar', 'baz', 'hello', 'world', function (err, res) {
237+
assert.deepEqual(res, ['bar', null, null, null, null]);
238+
});
261239
sub.subscribe('somechannel', 'another channel', function (err, res) {
262240
end();
263241
sub.stream.destroy();
@@ -302,7 +280,7 @@ describe('publish/subscribe', function () {
302280

303281
it('should only resubscribe to channels not unsubscribed earlier on a reconnect', function (done) {
304282
sub.subscribe('/foo', '/bar');
305-
sub.unsubscribe('/bar', function () {
283+
sub.batch().unsubscribe(['/bar'], function () {
306284
pub.pubsub('channels', function (err, res) {
307285
assert.deepEqual(res, ['/foo']);
308286
sub.stream.destroy();
@@ -313,7 +291,7 @@ describe('publish/subscribe', function () {
313291
});
314292
});
315293
});
316-
});
294+
}).exec();
317295
});
318296

319297
it('unsubscribes, subscribes, unsubscribes... single and multiple entries mixed. Withouth callbacks', function (done) {
@@ -512,7 +490,7 @@ describe('publish/subscribe', function () {
512490
return_buffers: true
513491
});
514492
sub2.on('ready', function () {
515-
sub2.psubscribe('*');
493+
sub2.batch().psubscribe('*', helper.isString('*')).exec();
516494
sub2.subscribe('/foo');
517495
sub2.on('pmessage', function (pattern, channel, message) {
518496
assert.strictEqual(pattern.inspect(), new Buffer('*').inspect());
@@ -523,9 +501,59 @@ describe('publish/subscribe', function () {
523501
pub.pubsub('numsub', '/foo', function (err, res) {
524502
assert.deepEqual(res, ['/foo', 2]);
525503
});
504+
// sub2 is counted twice as it subscribed with psubscribe and subscribe
526505
pub.publish('/foo', 'hello world', helper.isNumber(3));
527506
});
528507
});
508+
509+
it('allows to listen to pmessageBuffer and pmessage', function (done) {
510+
var batch = sub.batch();
511+
var end = helper.callFuncAfter(done, 6);
512+
assert.strictEqual(sub.message_buffers, false);
513+
batch.psubscribe('*');
514+
batch.subscribe('/foo');
515+
batch.unsubscribe('/foo');
516+
batch.unsubscribe(helper.isNull());
517+
batch.subscribe(['/foo'], helper.isString('/foo'));
518+
batch.exec();
519+
assert.strictEqual(sub.shouldBuffer, false);
520+
sub.on('pmessageBuffer', function (pattern, channel, message) {
521+
assert.strictEqual(pattern.inspect(), new Buffer('*').inspect());
522+
assert.strictEqual(channel.inspect(), new Buffer('/foo').inspect());
523+
sub.quit(end);
524+
});
525+
// Either message_buffers or buffers has to be true, but not both at the same time
526+
assert.notStrictEqual(sub.message_buffers, sub.buffers);
527+
sub.on('pmessage', function (pattern, channel, message) {
528+
assert.strictEqual(pattern, '*');
529+
assert.strictEqual(channel, '/foo');
530+
assert.strictEqual(message, 'hello world');
531+
end();
532+
});
533+
sub.on('message', function (channel, message) {
534+
assert.strictEqual(channel, '/foo');
535+
assert.strictEqual(message, 'hello world');
536+
end();
537+
});
538+
setTimeout(function () {
539+
pub.pubsub('numsub', '/foo', function (err, res) {
540+
// There's one subscriber to this channel
541+
assert.deepEqual(res, ['/foo', 1]);
542+
end();
543+
});
544+
pub.pubsub('channels', function (err, res) {
545+
// There's exactly one channel that is listened too
546+
assert.deepEqual(res, ['/foo']);
547+
end();
548+
});
549+
pub.pubsub('numpat', function (err, res) {
550+
// One pattern is active
551+
assert.strictEqual(res, 1);
552+
end();
553+
});
554+
pub.publish('/foo', 'hello world', helper.isNumber(2));
555+
}, 50);
556+
});
529557
});
530558

531559
describe('punsubscribe', function () {
@@ -534,10 +562,7 @@ describe('publish/subscribe', function () {
534562
});
535563

536564
it('executes callback when punsubscribe is called and there are no subscriptions', function (done) {
537-
pub.punsubscribe(function (err, results) {
538-
assert.strictEqual(null, results);
539-
done(err);
540-
});
565+
pub.batch().punsubscribe(helper.isNull()).exec(done);
541566
});
542567
});
543568

@@ -551,7 +576,7 @@ describe('publish/subscribe', function () {
551576
});
552577
// Get is forbidden
553578
sub.get('foo', function (err, res) {
554-
assert.strictEqual(err.message, 'ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context');
579+
assert(/^ERR only \(P\)SUBSCRIBE \/ \(P\)UNSUBSCRIBE/.test(err.message));
555580
assert.strictEqual(err.command, 'GET');
556581
});
557582
// Quit is allowed
@@ -561,7 +586,7 @@ describe('publish/subscribe', function () {
561586
it('emit error if only pub sub commands are allowed without callback', function (done) {
562587
sub.subscribe('channel');
563588
sub.on('error', function (err) {
564-
assert.strictEqual(err.message, 'ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context');
589+
assert(/^ERR only \(P\)SUBSCRIBE \/ \(P\)UNSUBSCRIBE/.test(err.message));
565590
assert.strictEqual(err.command, 'GET');
566591
done();
567592
});
@@ -614,6 +639,24 @@ describe('publish/subscribe', function () {
614639
});
615640
});
616641

642+
it('arguments variants', function (done) {
643+
sub.batch()
644+
.info(['stats'])
645+
.info()
646+
.client('KILL', ['type', 'pubsub'])
647+
.client('KILL', ['type', 'pubsub'], function () {})
648+
.unsubscribe()
649+
.psubscribe(['pattern:*'])
650+
.punsubscribe('unkown*')
651+
.punsubscribe(['pattern:*'])
652+
.exec(function (err, res) {
653+
sub.client('kill', ['type', 'pubsub']);
654+
sub.psubscribe('*');
655+
sub.punsubscribe('pa*');
656+
sub.punsubscribe(['a', '*'], done);
657+
});
658+
});
659+
617660
afterEach(function () {
618661
// Explicitly ignore still running commands
619662
pub.end(false);

‎test/return_buffers.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ describe('return_buffers', function () {
252252
var subConnected;
253253

254254
pub = redis.createClient.apply(redis.createClient, basicArgs);
255-
sub = redis.createClient.apply(redis.createClient, args);
255+
sub = redis.createClient.apply(null, args);
256256
pub.once('connect', function () {
257257
pub.flushdb(function () {
258258
pubConnected = true;

‎test/tls.spec.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ describe('TLS connection tests', function () {
6060
tls: tls_options
6161
});
6262
var time = 0;
63+
assert.strictEqual(client.address, '127.0.0.1:' + tls_port);
6364

6465
client.once('ready', function () {
6566
helper.killConnection(client);
@@ -87,18 +88,20 @@ describe('TLS connection tests', function () {
8788

8889
describe('when not connected', function () {
8990

90-
it('connect with host and port provided in the options object', function (done) {
91+
it('connect with host and port provided in the tls object', function (done) {
9192
if (skip) this.skip();
93+
var tls = utils.clone(tls_options);
94+
tls.port = tls_port;
95+
tls.host = 'localhost';
9296
client = redis.createClient({
93-
host: 'localhost',
9497
connect_timeout: 1000,
95-
port: tls_port,
96-
tls: tls_options
98+
tls: tls
9799
});
98100

99101
// verify connection is using TCP, not UNIX socket
100102
assert.strictEqual(client.connection_options.host, 'localhost');
101103
assert.strictEqual(client.connection_options.port, tls_port);
104+
assert.strictEqual(client.address, 'localhost:' + tls_port);
102105
assert(client.stream.encrypted);
103106

104107
client.set('foo', 'bar');
@@ -115,6 +118,7 @@ describe('TLS connection tests', function () {
115118
port: tls_port,
116119
tls: faulty_cert
117120
});
121+
assert.strictEqual(client.address, 'localhost:' + tls_port);
118122
client.on('error', function (err) {
119123
assert(/DEPTH_ZERO_SELF_SIGNED_CERT/.test(err.code || err.message), err);
120124
client.end(true);

‎test/utils.spec.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ describe('utils.js', function () {
1111
it('ignore the object prototype and clone a nested array / object', function () {
1212
var obj = {
1313
a: [null, 'foo', ['bar'], {
14-
"I'm special": true
14+
"i'm special": true
1515
}],
1616
number: 5,
1717
fn: function noop () {}
@@ -22,13 +22,28 @@ describe('utils.js', function () {
2222
assert(typeof clone.fn === 'function');
2323
});
2424

25-
it('replace faulty values with an empty object as return value', function () {
25+
it('replace falsy values with an empty object as return value', function () {
2626
var a = utils.clone();
2727
var b = utils.clone(null);
2828
assert.strictEqual(Object.keys(a).length, 0);
2929
assert.strictEqual(Object.keys(b).length, 0);
3030
});
3131

32+
it('transform camelCase options to snack_case and add the camel_case option', function () {
33+
var a = utils.clone({
34+
optionOneTwo: true,
35+
retryStrategy: false,
36+
nested: {
37+
onlyContainCamelCaseOnce: true
38+
}
39+
});
40+
assert.strictEqual(Object.keys(a).length, 4);
41+
assert.strictEqual(a.option_one_two, true);
42+
assert.strictEqual(a.retry_strategy, false);
43+
assert.strictEqual(a.camel_case, true);
44+
assert.strictEqual(Object.keys(a.nested).length, 1);
45+
});
46+
3247
it('throws on circular data', function () {
3348
try {
3449
var a = {};
@@ -92,7 +107,7 @@ describe('utils.js', function () {
92107
emitted = false;
93108
});
94109

95-
it('no elements in either queue. Reply in the next tick', function (done) {
110+
it('no elements in either queue. Reply in the next tick with callback', function (done) {
96111
var called = false;
97112
utils.reply_in_order(clientMock, function () {
98113
called = true;
@@ -101,7 +116,7 @@ describe('utils.js', function () {
101116
assert(!called);
102117
});
103118

104-
it('no elements in either queue. Reply in the next tick', function (done) {
119+
it('no elements in either queue. Reply in the next tick without callback', function (done) {
105120
assert(!emitted);
106121
utils.reply_in_order(clientMock, null, new Error('tada'));
107122
assert(!emitted);
@@ -138,16 +153,21 @@ describe('utils.js', function () {
138153
}
139154
});
140155

141-
it('elements in the offline queue. Reply after the offline queue is empty and respect the command_obj', function (done) {
142-
clientMock.command_queue.push(create_command_obj(), {});
143-
utils.reply_in_order(clientMock, function () {
156+
it('elements in the offline queue and the command_queue. Reply all other commands got handled respect the command_obj', function (done) {
157+
clientMock.command_queue.push(create_command_obj(), create_command_obj());
158+
clientMock.offline_queue.push(create_command_obj(), {});
159+
utils.reply_in_order(clientMock, function (err, res) {
144160
assert.strictEqual(clientMock.command_queue.length, 0);
161+
assert.strictEqual(clientMock.offline_queue.length, 0);
145162
assert(!emitted);
146-
assert.strictEqual(res_count, 1);
163+
assert.strictEqual(res_count, 3);
147164
done();
148165
}, null, null);
166+
while (clientMock.offline_queue.length) {
167+
clientMock.command_queue.push(clientMock.offline_queue.shift());
168+
}
149169
while (clientMock.command_queue.length) {
150-
clientMock.command_queue.shift().callback(null, 'bar');
170+
clientMock.command_queue.shift().callback(null, 'hello world');
151171
}
152172
});
153173
});

0 commit comments

Comments
 (0)
Please sign in to comment.