Repository: luin/ioredis (ts)

Issue:
add `stringNumbers` option to return strings for 64-bit numbers
ref: https://github.com/NodeRedis/node_redis/commit/0c5947be5195c6d03f1472460620120d02760947

Pull request:
feat: add stringNumbers option to return numbers as JavaScript strings
Closes #273

Patch:
diff --git a/API.md b/API.md
index 2306679..ab0898e 100644
--- a/API.md
+++ b/API.md
@@ -1,4 +1,5 @@
 ## Classes
+
 <dl>
 <dt><a href="#Redis">Redis</a> ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code></dt>
 <dd></dd>
@@ -7,7 +8,9 @@
 <dt><a href="#Commander">Commander</a></dt>
 <dd></dd>
 </dl>
+
 ## Members
+
 <dl>
 <dt><a href="#defaultOptions">defaultOptions</a></dt>
 <dd><p>Default options</p>
@@ -16,26 +19,29 @@
 <dd><p>Default options</p>
 </dd>
 </dl>
+
 <a name="Redis"></a>
+
 ## Redis ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>
 **Kind**: global class  
 **Extends:** <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>, <code>[Commander](#Commander)</code>  
 
 * [Redis](#Redis) ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>
-  * [new Redis([port], [host], [options])](#new_Redis_new)
-  * _instance_
-    * [.connect(callback)](#Redis+connect) ⇒ <code>Promise</code>
-    * [.disconnect()](#Redis+disconnect)
-    * ~~[.end()](#Redis+end)~~
-    * [.duplicate()](#Redis+duplicate)
-    * [.monitor([callback])](#Redis+monitor)
-    * [.getBuiltinCommands()](#Commander+getBuiltinCommands) ⇒ <code>Array.&lt;string&gt;</code>
-    * [.createBuiltinCommand(commandName)](#Commander+createBuiltinCommand) ⇒ <code>object</code>
-    * [.defineCommand(name, definition)](#Commander+defineCommand)
-  * _static_
-    * ~~[.createClient()](#Redis.createClient)~~
+    * [new Redis([port], [host], [options])](#new_Redis_new)
+    * _instance_
+        * [.connect(callback)](#Redis+connect) ⇒ <code>Promise</code>
+        * [.disconnect()](#Redis+disconnect)
+        * ~~[.end()](#Redis+end)~~
+        * [.duplicate()](#Redis+duplicate)
+        * [.monitor([callback])](#Redis+monitor)
+        * [.getBuiltinCommands()](#Commander+getBuiltinCommands) ⇒ <code>Array.&lt;string&gt;</code>
+        * [.createBuiltinCommand(commandName)](#Commander+createBuiltinCommand) ⇒ <code>object</code>
+        * [.defineCommand(name, definition)](#Commander+defineCommand)
+    * _static_
+        * ~~[.createClient()](#Redis.createClient)~~
 
 <a name="new_Redis_new"></a>
+
 ### new Redis([port], [host], [options])
 Creates a Redis instance
 
@@ -63,6 +69,7 @@ Creates a Redis instance
 | [options.retryStrategy] | <code>function</code> |  | See "Quick Start" section |
 | [options.reconnectOnError] | <code>function</code> |  | See "Quick Start" section |
 | [options.readOnly] | <code>boolean</code> | <code>false</code> | Enable READONLY mode for the connection. Only available for cluster mode. |
+| [options.stringNumbers] | <code>boolean</code> | <code>false</code> | Force numbers to be always returned as JavaScript strings. This option is necessary when dealing with big numbers (exceed the [-2^53, +2^53] range). Notice that when this option is enabled, the JavaScript parser will be used even "hiredis" is specified because only JavaScript parser supports this feature for the time being. |
 
 **Example**  
 ```js
@@ -80,6 +87,7 @@ var urlRedis2 = new Redis('//localhost:6379');
 var authedRedis = new Redis(6380, '192.168.100.1', { password: 'password' });
 ```
 <a name="Redis+connect"></a>
+
 ### redis.connect(callback) ⇒ <code>Promise</code>
 Create a connection to Redis.
 This method will be invoked automatically when creating a new Redis instance.
@@ -92,6 +100,7 @@ This method will be invoked automatically when creating a new Redis instance.
 | callback | <code>function</code> | 
 
 <a name="Redis+disconnect"></a>
+
 ### redis.disconnect()
 Disconnect from Redis.
 
@@ -102,6 +111,7 @@ If you want to wait for the pending replies, use Redis#quit instead.
 **Kind**: instance method of <code>[Redis](#Redis)</code>  
 **Access:** public  
 <a name="Redis+end"></a>
+
 ### ~~redis.end()~~
 ***Deprecated***
 
@@ -109,6 +119,7 @@ Disconnect from Redis.
 
 **Kind**: instance method of <code>[Redis](#Redis)</code>  
 <a name="Redis+duplicate"></a>
+
 ### redis.duplicate()
 Create a new instance with the same options as the current one.
 
@@ -120,6 +131,7 @@ var redis = new Redis(6380);
 var anotherRedis = redis.duplicate();
 ```
 <a name="Redis+monitor"></a>
+
 ### redis.monitor([callback])
 Listen for all requests received by the server in real time.
 
@@ -152,6 +164,7 @@ redis.monitor().then(function (monitor) {
 });
 ```
 <a name="Commander+getBuiltinCommands"></a>
+
 ### redis.getBuiltinCommands() ⇒ <code>Array.&lt;string&gt;</code>
 Return supported builtin commands
 
@@ -159,6 +172,7 @@ Return supported builtin commands
 **Returns**: <code>Array.&lt;string&gt;</code> - command list  
 **Access:** public  
 <a name="Commander+createBuiltinCommand"></a>
+
 ### redis.createBuiltinCommand(commandName) ⇒ <code>object</code>
 Create a builtin command
 
@@ -171,6 +185,7 @@ Create a builtin command
 | commandName | <code>string</code> | command name |
 
 <a name="Commander+defineCommand"></a>
+
 ### redis.defineCommand(name, definition)
 Define a custom command using lua script
 
@@ -184,6 +199,7 @@ Define a custom command using lua script
 | [definition.numberOfKeys] | <code>number</code> | <code></code> | the number of keys. If omit, you have to pass the number of keys as the first argument every time you invoke the command |
 
 <a name="Redis.createClient"></a>
+
 ### ~~Redis.createClient()~~
 ***Deprecated***
 
@@ -191,21 +207,23 @@ Create a Redis instance
 
 **Kind**: static method of <code>[Redis](#Redis)</code>  
 <a name="Cluster"></a>
+
 ## Cluster ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>
 **Kind**: global class  
 **Extends:** <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>, <code>[Commander](#Commander)</code>  
 
 * [Cluster](#Cluster) ⇐ <code>[EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)</code>
-  * [new Cluster(startupNodes, options)](#new_Cluster_new)
-  * [.connect()](#Cluster+connect) ⇒ <code>Promise</code>
-  * [.disconnect()](#Cluster+disconnect)
-  * [.nodes([role])](#Cluster+nodes) ⇒ <code>[Array.&lt;Redis&gt;](#Redis)</code>
-  * [.getBuiltinCommands()](#Commander+getBuiltinCommands) ⇒ <code>Array.&lt;string&gt;</code>
-  * [.createBuiltinCommand(commandName)](#Commander+createBuiltinCommand) ⇒ <code>object</code>
-  * [.defineCommand(name, definition)](#Commander+defineCommand)
-  * *[.sendCommand()](#Commander+sendCommand)*
+    * [new Cluster(startupNodes, options)](#new_Cluster_new)
+    * [.connect()](#Cluster+connect) ⇒ <code>Promise</code>
+    * [.disconnect()](#Cluster+disconnect)
+    * [.nodes([role])](#Cluster+nodes) ⇒ <code>[Array.&lt;Redis&gt;](#Redis)</code>
+    * [.getBuiltinCommands()](#Commander+getBuiltinCommands) ⇒ <code>Array.&lt;string&gt;</code>
+    * [.createBuiltinCommand(commandName)](#Commander+createBuiltinCommand) ⇒ <code>object</code>
+    * [.defineCommand(name, definition)](#Commander+defineCommand)
+    * *[.sendCommand()](#Commander+sendCommand)*
 
 <a name="new_Cluster_new"></a>
+
 ### new Cluster(startupNodes, options)
 Creates a Redis Cluster instance
 
@@ -225,18 +243,21 @@ Creates a Redis Cluster instance
 | [options.redisOptions] | <code>Object</code> |  | Passed to the constructor of `Redis`. |
 
 <a name="Cluster+connect"></a>
+
 ### cluster.connect() ⇒ <code>Promise</code>
 Connect to a cluster
 
 **Kind**: instance method of <code>[Cluster](#Cluster)</code>  
 **Access:** public  
 <a name="Cluster+disconnect"></a>
+
 ### cluster.disconnect()
 Disconnect from every node in the cluster.
 
 **Kind**: instance method of <code>[Cluster](#Cluster)</code>  
 **Access:** public  
 <a name="Cluster+nodes"></a>
+
 ### cluster.nodes([role]) ⇒ <code>[Array.&lt;Redis&gt;](#Redis)</code>
 Get nodes with the specified role
 
@@ -249,6 +270,7 @@ Get nodes with the specified role
 | [role] | <code>string</code> | <code>&quot;all&quot;</code> | role, "master", "slave" or "all" |
 
 <a name="Commander+getBuiltinCommands"></a>
+
 ### cluster.getBuiltinCommands() ⇒ <code>Array.&lt;string&gt;</code>
 Return supported builtin commands
 
@@ -256,6 +278,7 @@ Return supported builtin commands
 **Returns**: <code>Array.&lt;string&gt;</code> - command list  
 **Access:** public  
 <a name="Commander+createBuiltinCommand"></a>
+
 ### cluster.createBuiltinCommand(commandName) ⇒ <code>object</code>
 Create a builtin command
 
@@ -268,6 +291,7 @@ Create a builtin command
 | commandName | <code>string</code> | command name |
 
 <a name="Commander+defineCommand"></a>
+
 ### cluster.defineCommand(name, definition)
 Define a custom command using lua script
 
@@ -281,6 +305,7 @@ Define a custom command using lua script
 | [definition.numberOfKeys] | <code>number</code> | <code></code> | the number of keys. If omit, you have to pass the number of keys as the first argument every time you invoke the command |
 
 <a name="Commander+sendCommand"></a>
+
 ### *cluster.sendCommand()*
 Send a command
 
@@ -288,17 +313,19 @@ Send a command
 **Overrides:** <code>[sendCommand](#Commander+sendCommand)</code>  
 **Access:** public  
 <a name="Commander"></a>
+
 ## Commander
 **Kind**: global class  
 
 * [Commander](#Commander)
-  * [new Commander()](#new_Commander_new)
-  * [.getBuiltinCommands()](#Commander+getBuiltinCommands) ⇒ <code>Array.&lt;string&gt;</code>
-  * [.createBuiltinCommand(commandName)](#Commander+createBuiltinCommand) ⇒ <code>object</code>
-  * [.defineCommand(name, definition)](#Commander+defineCommand)
-  * *[.sendCommand()](#Commander+sendCommand)*
+    * [new Commander()](#new_Commander_new)
+    * [.getBuiltinCommands()](#Commander+getBuiltinCommands) ⇒ <code>Array.&lt;string&gt;</code>
+    * [.createBuiltinCommand(commandName)](#Commander+createBuiltinCommand) ⇒ <code>object</code>
+    * [.defineCommand(name, definition)](#Commander+defineCommand)
+    * *[.sendCommand()](#Commander+sendCommand)*
 
 <a name="new_Commander_new"></a>
+
 ### new Commander()
 Commander
 
@@ -310,6 +337,7 @@ This is the base class of Redis, Redis.Cluster and Pipeline
 | [options.showFriendlyErrorStack] | <code>boolean</code> | <code>false</code> | Whether to show a friendly error stack. Will decrease the performance significantly. |
 
 <a name="Commander+getBuiltinCommands"></a>
+
 ### commander.getBuiltinCommands() ⇒ <code>Array.&lt;string&gt;</code>
 Return supported builtin commands
 
@@ -317,6 +345,7 @@ Return supported builtin commands
 **Returns**: <code>Array.&lt;string&gt;</code> - command list  
 **Access:** public  
 <a name="Commander+createBuiltinCommand"></a>
+
 ### commander.createBuiltinCommand(commandName) ⇒ <code>object</code>
 Create a builtin command
 
@@ -329,6 +358,7 @@ Create a builtin command
 | commandName | <code>string</code> | command name |
 
 <a name="Commander+defineCommand"></a>
+
 ### commander.defineCommand(name, definition)
 Define a custom command using lua script
 
@@ -342,18 +372,21 @@ Define a custom command using lua script
 | [definition.numberOfKeys] | <code>number</code> | <code></code> | the number of keys. If omit, you have to pass the number of keys as the first argument every time you invoke the command |
 
 <a name="Commander+sendCommand"></a>
+
 ### *commander.sendCommand()*
 Send a command
 
 **Kind**: instance abstract method of <code>[Commander](#Commander)</code>  
 **Access:** public  
 <a name="defaultOptions"></a>
+
 ## defaultOptions
 Default options
 
 **Kind**: global variable  
 **Access:** protected  
 <a name="defaultOptions"></a>
+
 ## defaultOptions
 Default options
 
diff --git a/lib/cluster/index.js b/lib/cluster/index.js
index dba62b5..926da18 100644
--- a/lib/cluster/index.js
+++ b/lib/cluster/index.js
@@ -57,7 +57,7 @@ function Cluster(startupNodes, options) {
 
   var _this = this;
   this.connectionPool.on('-node', function (redis) {
-    if (_this.subscriber === redis) {
+    if (_this.status !== 'disconnecting' && _this.subscriber === redis) {
       _this.selectSubscriber();
     }
     _this.emit('-node', redis);
@@ -193,6 +193,8 @@ Cluster.prototype.connect = function () {
  * @public
  */
 Cluster.prototype.disconnect = function (reconnect) {
+  this.setStatus('disconnecting');
+
   if (!reconnect) {
     this.manuallyClosing = true;
   }
diff --git a/lib/connectors/sentinel_connector.js b/lib/connectors/sentinel_connector.js
index 44a8cb3..686fd08 100644
--- a/lib/connectors/sentinel_connector.js
+++ b/lib/connectors/sentinel_connector.js
@@ -37,6 +37,7 @@ SentinelConnector.prototype.connect = function (callback) {
   }
 
   var _this = this;
+  var lastError;
   connectToNext();
 
   function connectToNext() {
@@ -50,7 +51,11 @@ SentinelConnector.prototype.connect = function (callback) {
       }
       if (typeof retryDelay !== 'number') {
         debug('All sentinels are unreachable and retry is disabled, emitting error...');
-        return callback(new Error('All sentinels are unreachable.'));
+        var error = 'All sentinels are unreachable.';
+        if (lastError) {
+          error += ' Last error: ' + lastError.message;
+        }
+        return callback(new Error(error));
       }
       debug('All sentinels are unreachable. Retrying from scratch after %d', retryDelay);
       setTimeout(connectToNext, retryDelay);
@@ -68,9 +73,11 @@ SentinelConnector.prototype.connect = function (callback) {
         callback(null, _this.stream);
       } else if (err) {
         debug('failed to connect to sentinel %s:%s because %s', endpoint.host, endpoint.port, err);
+        lastError = err;
         connectToNext();
       } else {
-        debug('connected to sentinel %s:%s successfully, but got a invalid reply: %s', endpoint.host, endpoint.port, resolved);
+        debug('connected to sentinel %s:%s successfully, but got a invalid reply: %s',
+          endpoint.host, endpoint.port, resolved);
         connectToNext();
       }
     });
diff --git a/lib/redis.js b/lib/redis.js
index 52e74c3..99bfd1f 100644
--- a/lib/redis.js
+++ b/lib/redis.js
@@ -74,6 +74,10 @@ var ScanStream = require('./scan_stream');
  * @param {function} [options.reconnectOnError] - See "Quick Start" section
  * @param {boolean} [options.readOnly=false] - Enable READONLY mode for the connection.
  * Only available for cluster mode.
+ * @param {boolean} [options.stringNumbers=false] - Force numbers to be always returned as JavaScript
+ * strings. This option is necessary when dealing with big numbers (exceed the [-2^53, +2^53] range).
+ * Notice that when this option is enabled, the JavaScript parser will be used even "hiredis" is specified
+ * because only JavaScript parser supports this feature for the time being.
  * @extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter)
  * @extends Commander
  * @example
@@ -169,7 +173,8 @@ Redis.defaultOptions = {
   lazyConnect: false,
   keyPrefix: '',
   reconnectOnError: null,
-  readOnly: false
+  readOnly: false,
+  stringNumbers: false
 };
 
 Redis.prototype.resetCommandQueue = function () {
@@ -307,7 +312,11 @@ Redis.prototype.disconnect = function (reconnect) {
     clearTimeout(this.reconnectTimeout);
     this.reconnectTimeout = null;
   }
-  this.connector.disconnect();
+  if (this.status === 'wait') {
+    eventHandler.closeHandler(this)();
+  } else {
+    this.connector.disconnect();
+  }
 };
 
 /**
diff --git a/lib/redis/parser.js b/lib/redis/parser.js
index 4087f0f..39c3ec6 100644
--- a/lib/redis/parser.js
+++ b/lib/redis/parser.js
@@ -19,6 +19,7 @@ exports.initParser = function () {
 
   this.replyParser = new Parser({
     name: this.options.parser,
+    stringNumbers: this.options.stringNumbers,
     returnBuffers: true,
     returnError: function (err) {
       _this.returnError(new ReplyError(err.message));
@@ -148,7 +149,7 @@ exports.returnReply = function (reply) {
     item = this.commandQueue.shift();
     if (!item) {
       return this.emit('error',
-        new Error('Command queue state error. If you can reproduce this, please report it.' + reply.toString()));
+        new Error('Command queue state error. If you can reproduce this, please report it. Last reply: ' + reply.toString()));
     }
     if (_.includes(Command.FLAGS.ENTER_SUBSCRIBER_MODE, item.command.name)) {
       this.condition.subscriber = new SubscriptionSet();
diff --git a/package.json b/package.json
index 4bba5e4..923153a 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "main": "index.js",
   "scripts": {
     "test": "NODE_ENV=test mocha",
-    "test:cov": "NODE_ENV=test DEBUG=ioredis:* node ./node_modules/istanbul/lib/cli.js cover --preserve-comments ./node_modules/mocha/bin/_mocha -- -R spec",
+    "test:cov": "NODE_ENV=test node ./node_modules/istanbul/lib/cli.js cover --preserve-comments ./node_modules/mocha/bin/_mocha -- -R spec",
     "generate-docs": "jsdoc2md lib/redis.js lib/cluster/index.js lib/commander.js > API.md",
     "build": "node tools/build > commands.js",
     "bench": "matcha benchmarks/*.js"


Test patch:
diff --git a/test/functional/cluster.js b/test/functional/cluster.js
index 8135759..d759dd5 100644
--- a/test/functional/cluster.js
+++ b/test/functional/cluster.js
@@ -1219,7 +1219,7 @@ describe('cluster', function () {
         expect(cluster.nodes('master')).to.have.lengthOf(2);
         expect(cluster.nodes('slave')).to.have.lengthOf(1);
 
-        cluster.on('-node', function () {
+        cluster.once('-node', function () {
           expect(cluster.nodes()).to.have.lengthOf(2);
           expect(cluster.nodes('all')).to.have.lengthOf(2);
           expect(cluster.nodes('master')).to.have.lengthOf(1);
diff --git a/test/functional/lazy_connect.js b/test/functional/lazy_connect.js
index 16522e5..90270aa 100644
--- a/test/functional/lazy_connect.js
+++ b/test/functional/lazy_connect.js
@@ -26,4 +26,12 @@ describe('lazy connect', function () {
       });
     });
   });
+
+  it('should be able to disconnect', function (done) {
+    var redis = new Redis({ lazyConnect: true });
+    redis.on('end', function () {
+      done();
+    });
+    redis.disconnect();
+  });
 });
diff --git a/test/functional/sentinel.js b/test/functional/sentinel.js
index 8bf690a..470c9a7 100644
--- a/test/functional/sentinel.js
+++ b/test/functional/sentinel.js
@@ -129,6 +129,31 @@ describe('sentinel', function () {
       });
     });
 
+    it('should reject when sentinel is rejected', function (done) {
+      var sentinel = new MockServer(27379, function (argv) {
+        if (argv[0] === 'sentinel' && argv[1] === 'get-master-addr-by-name') {
+          return new Error('just rejected');
+        }
+      });
+
+      var redis = new Redis({
+        sentinels: [
+          { host: '127.0.0.1', port: '27379' }
+        ],
+        name: 'master',
+        sentinelRetryStrategy: null,
+        lazyConnect: true
+      });
+
+      redis.connect().then(function () {
+        throw new Error('Expect `connect` to be thrown');
+      }).catch(function (err) {
+        expect(err.message).to.eql('All sentinels are unreachable. Last error: just rejected');
+        redis.disconnect();
+        sentinel.disconnect(done);
+      });
+    });
+
     it('should connect to the next sentinel if getting master failed', function (done) {
       var sentinel = new MockServer(27379, function (argv) {
         if (argv[0] === 'sentinel' && argv[1] === 'get-master-addr-by-name') {
diff --git a/test/functional/string_numbers.js b/test/functional/string_numbers.js
new file mode 100644
index 0000000..63fbe38
--- /dev/null
+++ b/test/functional/string_numbers.js
@@ -0,0 +1,52 @@
+'use strict';
+
+var MAX_NUMBER = 9007199254740991; // Number.MAX_SAFE_INTEGER
+
+describe('stringNumbers', function () {
+  context('enabled', function () {
+    it('returns numbers as strings', function (done) {
+      var redis = new Redis({
+        stringNumbers: true
+      });
+
+      var pending = 0;
+
+      redis.set('foo', MAX_NUMBER);
+      redis.incr('foo', check('9007199254740992'));
+      redis.incr('foo', check('9007199254740993'));
+      redis.incr('foo', check('9007199254740994'));
+
+      // also works for small interger
+      redis.set('foo', 123);
+      redis.incr('foo', check('124'));
+
+      // and floats
+      redis.set('foo', 123.23);
+      redis.incrbyfloat('foo', 1.2, check('124.43'));
+
+      function check(expected) {
+        pending += 1;
+        return function (err, res) {
+          expect(res).to.eql(expected);
+          if (!--pending) {
+            redis.disconnect();
+            done();
+          }
+        };
+      }
+    });
+  });
+
+  context('disabled', function () {
+    it('returns numbers', function (done) {
+      var redis = new Redis();
+
+      redis.set('foo', '123');
+      redis.incr('foo', function (err, res) {
+        expect(res).to.eql(124);
+        redis.disconnect();
+        done();
+      });
+    });
+  });
+});