From f92b2b6668e6895ebe68c39487ab951faafe1a6c Mon Sep 17 00:00:00 2001 From: shaharmor Date: Mon, 18 Jun 2018 18:10:47 +0300 Subject: [PATCH 01/12] refresh slots automatically every slotsRefreshInterval (default 5s) --- README.md | 1 + lib/cluster/index.js | 24 ++++++++++++++++++++++-- test/functional/cluster.js | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 831f9c19..9e68f5d9 100644 --- a/README.md +++ b/README.md @@ -744,6 +744,7 @@ but a few so that if one is unreachable the client will try the next one, and th will resend the commands rejected with `TRYAGAIN` error after the specified time (in ms). * `redisOptions`: Default options passed to the constructor of `Redis` when connecting to a node. * `slotsRefreshTimeout`: Milliseconds before a timeout occurs while refreshing slots from the cluster (default `1000`) + * `slotsRefreshInterval`: Milliseconds between every automatic slots refresh (default `5000`) ### Read-write splitting diff --git a/lib/cluster/index.js b/lib/cluster/index.js index c268c888..483d978d 100644 --- a/lib/cluster/index.js +++ b/lib/cluster/index.js @@ -37,6 +37,7 @@ var DelayQueue = require('./delay_queue'); * if `retryDelayOnTryAgain` is valid delay time. * @param {number} [options.slotsRefreshTimeout=1000] - The milliseconds before a timeout occurs while refreshing * slots from the cluster. + * @param {number} [options.slotsRefreshInterval=5000] - The milliseconds between every automatic slots refresh. * @param {Object} [options.redisOptions] - Passed to the constructor of `Redis`. * @extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) * @extends Commander @@ -106,7 +107,8 @@ Cluster.defaultOptions = { retryDelayOnFailover: 100, retryDelayOnClusterDown: 100, retryDelayOnTryAgain: 100, - slotsRefreshTimeout: 1000 + slotsRefreshTimeout: 1000, + slotsRefreshInterval: 5000 }; util.inherits(Cluster, EventEmitter); @@ -116,6 +118,15 @@ Cluster.prototype.resetOfflineQueue = function () { this.offlineQueue = new Deque(); }; +Cluster.prototype.resetNodesRefreshInterval = function () { + if (this.slotsTimer) { + return; + } + this.slotsTimer = setInterval(function() { + this.refreshSlotsCache(); + }.bind(this), this.options.slotsRefreshInterval); +}; + /** * Connect to a cluster * @@ -127,6 +138,7 @@ Cluster.prototype.connect = function () { this.setStatus('ready'); this.retryAttempts = 0; this.executeOfflineCommands(); + this.resetNodesRefreshInterval(); } return new Promise(function (resolve, reject) { @@ -219,6 +231,10 @@ Cluster.prototype.disconnect = function (reconnect) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } + if (this.slotsTimer) { + clearInterval(this.slotsTimer); + this.slotsTimer = null; + } if (status === 'wait') { this.setStatus('close'); @@ -245,6 +261,10 @@ Cluster.prototype.quit = function (callback) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = null; } + if (this.slotsTimer) { + clearInterval(this.slotsTimer); + this.slotsTimer = null; + } if (status === 'wait') { var ret = Promise.resolve('OK').nodeify(callback); @@ -346,7 +366,7 @@ Cluster.prototype.setStatus = function (status) { /** * Refresh the slot cache * - * @param {function} callback + * @param {function} [callback] * @private */ Cluster.prototype.refreshSlotsCache = function (callback) { diff --git a/test/functional/cluster.js b/test/functional/cluster.js index e2ef6fed..c85f0717 100644 --- a/test/functional/cluster.js +++ b/test/functional/cluster.js @@ -199,6 +199,41 @@ describe('cluster', function () { } }); + it('should discover other nodes automatically every slotsRefreshInterval', function (done) { + var times = 0; + var argvHandler = function (argv) { + if (argv[0] === 'cluster' && argv[1] === 'slots') { + times++; + if (times === 1) { + return [ + [0, 5460, ['127.0.0.1', 30001]], + [5461, 10922, ['127.0.0.1', 30001]], + [10923, 16383, ['127.0.0.1', 30001]] + ]; + } + + return [ + [0, 5460, ['127.0.0.1', 30001]], + [5461, 10922, ['127.0.0.1', 30001]], + [10923, 16383, ['127.0.0.1', 30002]] + ]; + } + }; + var node1 = new MockServer(30001, argvHandler); + var node2 = new MockServer(30002, argvHandler); + + node1.once('connect', function() { + node2.once('connect', function () { + cluster.disconnect(); + disconnect([node1, node2], done); + }); + }); + + var cluster = new Redis.Cluster([ + { host: '127.0.0.1', port: '30001' } + ], { slotsRefreshInterval: 100, redisOptions: { lazyConnect: false } }); + }); + it('should send command to the correct node', function (done) { var node1 = new MockServer(30001, function (argv) { if (argv[0] === 'cluster' && argv[1] === 'slots') { From 2950b79e432b2d025dc1e3e5be167599b28fda5f Mon Sep 17 00:00:00 2001 From: luin Date: Sat, 23 Jun 2018 11:16:11 +0800 Subject: [PATCH 02/12] fix: solves vulnerabilities dependencies --- .travis.yml | 8 +- package-lock.json | 728 +++----------------------------- package.json | 7 +- test/functional/lazy_connect.js | 1 + 4 files changed, 71 insertions(+), 673 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7cfdcb95..17273100 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,9 +19,13 @@ script: env: - CXX=g++-4.8 INSTALL_HIREDIS="yes" - INSTALL_HIREDIS="no" +- CC_TEST_REPORTER_ID="4cee2f60edbf31acac6ddff823f0b93e2e9882c3e5c55130049e0fd878549f84" before_script: - if [ "$INSTALL_HIREDIS" = "yes" ]; then npm install hiredis; fi +- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter +- chmod +x ./cc-test-reporter +- ./cc-test-reporter before-build addons: apt: @@ -29,8 +33,6 @@ addons: - ubuntu-toolchain-r-test packages: - g++-4.8 - code_climate: - repo_token: 4cee2f60edbf31acac6ddff823f0b93e2e9882c3e5c55130049e0fd878549f84 after_success: -- if [ "$INSTALL_HIREDIS" = "yes" ]; then cat ./coverage/lcov.info | ./node_modules/.bin/codeclimate-test-reporter; fi +- if [ "$INSTALL_HIREDIS" = "yes" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi diff --git a/package-lock.json b/package-lock.json index 0f7df5c3..92f7e61a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,18 +36,6 @@ "array-back": "2.0.0" } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, "argparse": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", @@ -76,18 +64,6 @@ "typical": "2.6.1" } }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true - }, "assertion-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", @@ -100,24 +76,6 @@ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", - "dev": true - }, "babylon": { "version": "7.0.0-beta.19", "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz", @@ -130,30 +88,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, "bluebird": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -165,9 +104,9 @@ } }, "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "cache-point": { @@ -188,12 +127,6 @@ "dev": true, "optional": true }, - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true - }, "catharsis": { "version": "0.8.9", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz", @@ -225,19 +158,6 @@ "type-detect": "1.0.0" } }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -264,18 +184,6 @@ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.8.tgz", "integrity": "sha1-dlRVYIWmUzCTKi6LWXb44tCz5BQ=" }, - "codeclimate-test-reporter": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/codeclimate-test-reporter/-/codeclimate-test-reporter-0.5.0.tgz", - "integrity": "sha1-k/oGscGOQRc0kSjcTjiq0IBDgo4=", - "dev": true, - "requires": { - "async": "1.5.2", - "commander": "2.9.0", - "lcov-parse": "0.0.10", - "request": "2.79.0" - } - }, "collect-all": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-all/-/collect-all-1.0.3.tgz", @@ -286,15 +194,6 @@ "stream-via": "1.0.4" } }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, "command-line-args": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.0.2.tgz", @@ -334,13 +233,10 @@ } }, "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": "1.0.1" - } + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true }, "common-sequence": { "version": "1.0.2", @@ -377,21 +273,6 @@ "integrity": "sha1-XblXOdbCEqy+e29lahG5QLqmiUY=", "dev": true }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, "cz-conventional-changelog": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-2.0.0.tgz", @@ -406,23 +287,6 @@ "word-wrap": "1.2.3" } }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -456,9 +320,9 @@ } }, "deep-extend": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.0.tgz", - "integrity": "sha1-bvSgmwX5iw41jW2T1Mo8rsZnKAM=", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "dev": true }, "deep-is": { @@ -467,21 +331,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, "denque": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/denque/-/denque-1.2.1.tgz", "integrity": "sha512-Ak/DUA1K1wMpamAfz3BYXHdeN6Bmbw6CC48QCMbn8DL8idfxEGIdVNjCwpkdTcT34uRY16/+faA6RzwXh9t6mw==" }, "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, "dmd": { @@ -533,16 +391,6 @@ } } }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -580,18 +428,6 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -648,23 +484,6 @@ "resolved": "https://registry.npmjs.org/flexbuffer/-/flexbuffer-0.0.6.tgz", "integrity": "sha1-A5/fI/iCPkQMOPMnfm/vEXQhWzA=" }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.16" - } - }, "formatio": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz", @@ -686,38 +505,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "1.0.2" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, "glob": { "version": "5.0.15", "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", @@ -737,16 +524,10 @@ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, "growl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, "handlebars": { @@ -772,61 +553,17 @@ } } }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "commander": "2.9.0", - "is-my-json-valid": "2.16.0", - "pinkie-promise": "2.0.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, "has-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "http-signature": { + "he": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true }, "inflight": { "version": "1.0.6", @@ -850,42 +587,12 @@ "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", "dev": true }, - "is-my-json-valid": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", - "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", - "dev": true, - "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, "istanbul": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", @@ -946,13 +653,6 @@ "xmlcreate": "1.0.2" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, "jsdoc": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.4.tgz", @@ -965,7 +665,7 @@ "escape-string-regexp": "1.0.5", "js2xmlparser": "3.0.0", "klaw": "2.0.0", - "marked": "0.3.6", + "marked": "~0.3.6", "mkdirp": "0.5.1", "requizzle": "0.2.1", "strip-json-comments": "2.0.1", @@ -1002,7 +702,7 @@ "escape-string-regexp": "1.0.5", "js2xmlparser": "3.0.0", "klaw": "2.0.0", - "marked": "0.3.6", + "marked": "~0.3.6", "mkdirp": "0.5.1", "requizzle": "0.2.1", "strip-json-comments": "2.0.1", @@ -1041,50 +741,6 @@ "walk-back": "3.0.0" } }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -1110,12 +766,6 @@ "dev": true, "optional": true }, - "lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", - "dev": true - }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -1126,53 +776,6 @@ "type-check": "0.3.2" } }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "3.0.1", - "lodash.keys": "3.1.2" - }, - "dependencies": { - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" - } - } - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basecreate": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, "lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", @@ -1199,17 +802,6 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, - "lodash.create": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true, - "requires": { - "lodash._baseassign": "3.2.0", - "lodash._basecreate": "3.0.3", - "lodash._isiterateecall": "3.0.9" - } - }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -1230,18 +822,6 @@ "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, "lodash.isempty": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", @@ -1313,26 +893,11 @@ "dev": true }, "marked": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", - "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", "dev": true }, - "mime-db": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.29.0.tgz", - "integrity": "sha1-SNJtI1WJZRcErFkWygYAGRQmaHg=", - "dev": true - }, - "mime-types": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.16.tgz", - "integrity": "sha1-K4WKUuXs1RbbiXrCvodIeDBpjiM=", - "dev": true, - "requires": { - "mime-db": "1.29.0" - } - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1372,54 +937,36 @@ "dev": true }, "mocha": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.0.tgz", - "integrity": "sha512-pIU2PJjrPYvYRqVpjXzj76qltO9uBYI7woYAMoxbSefsa+vqAfptjoeevd6bUgwD0mPIO+hv9f7ltvsNreL2PA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", "dev": true, "requires": { - "browser-stdout": "1.3.0", - "commander": "2.9.0", - "debug": "2.6.8", - "diff": "3.2.0", + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", "escape-string-regexp": "1.0.5", - "glob": "7.1.1", - "growl": "1.9.2", - "json3": "3.3.2", - "lodash.create": "3.1.1", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "3.1.2" + "supports-color": "5.4.0" }, "dependencies": { - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "supports-color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "has-flag": "1.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } } } @@ -1438,12 +985,6 @@ "abbrev": "1.0.9" } }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true - }, "object-get": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/object-get/-/object-get-2.1.0.tgz", @@ -1512,39 +1053,12 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "2.0.4" - } - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", - "dev": true - }, "redis-commands": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz", @@ -1633,34 +1147,6 @@ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, - "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", - "dev": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.11.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "2.0.6", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.16", - "oauth-sign": "0.8.2", - "qs": "6.3.2", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.4.3", - "uuid": "3.1.0" - } - }, "requizzle": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz", @@ -1724,15 +1210,6 @@ "util": "0.10.3" } }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, "sort-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-2.0.0.tgz", @@ -1771,30 +1248,6 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "dev": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, "stream-connect": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-connect/-/stream-connect-1.0.2.tgz", @@ -1821,21 +1274,6 @@ "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", "dev": true }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -1843,10 +1281,21 @@ "dev": true }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + } + } }, "table-layout": { "version": "0.4.2", @@ -1855,7 +1304,7 @@ "dev": true, "requires": { "array-back": "2.0.0", - "deep-extend": "0.5.0", + "deep-extend": "~0.5.0", "lodash.padend": "4.6.1", "typical": "2.6.1", "wordwrapjs": "3.0.0" @@ -1883,28 +1332,6 @@ "typical": "2.6.1" } }, - "tough-cookie": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", - "dev": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -1994,31 +1421,6 @@ } } }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, "walk-back": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-3.0.0.tgz", @@ -2075,12 +1477,6 @@ "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", "dev": true }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, "yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", diff --git a/package.json b/package.json index 2f784adb..a08e3278 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "lib/" ], "scripts": { - "test": "NODE_ENV=test mocha", - "test:cov": "NODE_ENV=test node ./node_modules/istanbul/lib/cli.js cover --preserve-comments ./node_modules/mocha/bin/_mocha -- -R spec", + "test": "NODE_ENV=test mocha --exit", + "test:cov": "NODE_ENV=test node ./node_modules/istanbul/lib/cli.js cover --preserve-comments ./node_modules/mocha/bin/_mocha -- -R spec --exit", "generate-docs": "jsdoc2md lib/redis.js lib/cluster/index.js lib/commander.js > API.md", "bench": "matcha benchmarks/*.js" }, @@ -52,12 +52,11 @@ }, "devDependencies": { "chai": "^3.5.0", - "codeclimate-test-reporter": "^0.5.0", "cz-conventional-changelog": "^2.0.0", "istanbul": "^0.4.2", "jsdoc": "^3.4.0", "jsdoc-to-markdown": "^4.0.1", - "mocha": "^3.1.1", + "mocha": "^5.2.0", "server-destroy": "^1.0.1", "sinon": "^1.17.3" }, diff --git a/test/functional/lazy_connect.js b/test/functional/lazy_connect.js index 29035c6a..c16e3626 100644 --- a/test/functional/lazy_connect.js +++ b/test/functional/lazy_connect.js @@ -81,6 +81,7 @@ describe('lazy connect', function () { stub(Redis.Cluster.prototype, 'connect', function () { Redis.Cluster.prototype.connect.restore(); done(); + return Promise.resolve(); }); }); }); From 861eaf386f850e6105eed5e58bca499f12d3d4e7 Mon Sep 17 00:00:00 2001 From: luin Date: Sat, 23 Jun 2018 20:47:49 +0800 Subject: [PATCH 03/12] feat: drop support for < node v6 --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 17273100..05bdb898 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,6 @@ language: node_js node_js: -- '0.11' -- '0.12' -- '4' -- '5' - '6' - '8' - '9' From da60b8bf2355134f2751210a9e5cf16e1226d067 Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 24 Jun 2018 01:34:52 +0800 Subject: [PATCH 04/12] feat: use native Promise instead of Bluebird, and allow users to switch back. BREAKING CHANGE: This change makes all the code that rely on the features provided by Bluebird not working anymore. For example, `redis.get('foo').timeout(500)` now should be failed since the native Promise doesn't support the `timeout` method. You can switch back to the Bluebird implementation by setting `Redis.Promise`: ``` const Redis = require('ioredis') Redis.Promise = require('bluebird') const redis = new Redis() // Use bluebird assert.equal(redis.get().constructor, require('bluebird')) // You can change the Promise implementation at any time: Redis.Promise = global.Promise assert.equal(redis.get().constructor, global.Promise) ``` --- README.md | 20 +++++++- index.js | 10 ++++ lib/cluster/connection_pool.js | 2 +- lib/cluster/index.js | 23 ++++++--- lib/command.js | 10 ++-- lib/commander.js | 13 +++-- lib/connectors/connector.js | 2 +- lib/pipeline.js | 11 +++-- lib/promise_container.js | 19 ++++++++ lib/redis.js | 30 +++++++----- lib/script.js | 24 +++++---- lib/transaction.js | 9 ++-- lib/utils/lodash.js | 4 +- package-lock.json | 51 ++++++++------------ package.json | 8 ++- test/functional/promise.js | 20 ++++++++ test/functional/show_friendly_error_stack.js | 17 +++---- test/functional/watch-exec.js | 11 ++--- test/unit/cluster.js | 1 - test/unit/redis.js | 2 - 20 files changed, 185 insertions(+), 102 deletions(-) create mode 100644 lib/promise_container.js create mode 100644 test/functional/promise.js diff --git a/README.md b/README.md index 9e68f5d9..e49791ec 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ used in the world's biggest online commerce company [Alibaba](http://www.alibaba 0. Full-featured. It supports [Cluster](http://redis.io/topics/cluster-tutorial), [Sentinel](http://redis.io/topics/sentinel), [Pipelining](http://redis.io/topics/pipelining) and of course [Lua scripting](http://redis.io/commands/eval) & [Pub/Sub](http://redis.io/topics/pubsub) (with the support of binary messages). 0. High performance. -0. Delightful API. It works with Node callbacks and [Bluebird promises](https://github.com/petkaantonov/bluebird). +0. Delightful API. It works with Node callbacks and Native promises. 0. Transformation of command arguments and replies. 0. Transparent key prefixing. 0. Abstraction for Lua scripting, allowing you to define custom commands. @@ -944,6 +944,24 @@ var redis = new Redis(); redis.set('foo'); ``` +# Plugging in your own Promises Library +If you're an advanced user, you may want to plug in your own promise library like [bluebird](https://www.npmjs.com/package/bluebird). Just set Redis.Promise to your favorite ES6-style promise constructor and ioredis will use it. + +```javascript +const Redis = require('ioredis') +Redis.Promise = require('bluebird') + +const redis = new Redis() + +// Use bluebird +assert.equal(redis.get().constructor, require('bluebird')) + +// You can change the Promise implementation at any time: +Redis.Promise = global.Promise +assert.equal(redis.get().constructor, global.Promise) +``` + + # Running tests Start a Redis server on 127.0.0.1:6379, and then: diff --git a/index.js b/index.js index cfefe678..2860bd60 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,16 @@ exports.Promise = require('bluebird'); exports.Cluster = require('./lib/cluster'); exports.Command = require('./lib/command'); +var PromiseContainer = require('./lib/promise_container'); +Object.defineProperty(exports, 'Promise', { + get: function() { + return PromiseContainer.get(); + }, + set: function(lib) { + PromiseContainer.set(lib); + } +}); + exports.print = function (err, reply) { if (err) { console.log('Error: ' + err); diff --git a/lib/cluster/connection_pool.js b/lib/cluster/connection_pool.js index b30cf637..82c5ba5f 100644 --- a/lib/cluster/connection_pool.js +++ b/lib/cluster/connection_pool.js @@ -36,7 +36,7 @@ ConnectionPool.prototype.findOrCreate = function (node, readOnly) { readOnly = Boolean(readOnly); if (this.specifiedOptions[node.key]) { - _.assign(node, this.specifiedOptions[node.key]); + Object.assign(node, this.specifiedOptions[node.key]); } else { this.specifiedOptions[node.key] = node; } diff --git a/lib/cluster/index.js b/lib/cluster/index.js index 02f49d9d..473e8699 100644 --- a/lib/cluster/index.js +++ b/lib/cluster/index.js @@ -1,6 +1,5 @@ 'use strict'; -var Promise = require('bluebird'); var Deque = require('denque'); var Redis = require('../redis'); var utils = require('../utils'); @@ -12,8 +11,10 @@ var ScanStream = require('../scan_stream'); var Commander = require('../commander'); var Command = require('../command'); var commands = require('redis-commands'); +var asCallback = require('standard-as-callback'); var ConnectionPool = require('./connection_pool'); var DelayQueue = require('./delay_queue'); +var PromiseContainer = require('../promise_container'); /** * Creates a Redis Cluster instance @@ -112,7 +113,7 @@ Cluster.defaultOptions = { }; util.inherits(Cluster, EventEmitter); -_.assign(Cluster.prototype, Commander.prototype); +Object.assign(Cluster.prototype, Commander.prototype); Cluster.prototype.resetOfflineQueue = function () { this.offlineQueue = new Deque(); @@ -141,6 +142,7 @@ Cluster.prototype.connect = function () { this.resetNodesRefreshInterval(); } + var Promise = PromiseContainer.get(); return new Promise(function (resolve, reject) { if (this.status === 'connecting' || this.status === 'connect' || this.status === 'ready') { reject(new Error('Redis is already connecting/connected')); @@ -271,8 +273,10 @@ Cluster.prototype.quit = function (callback) { clearInterval(this.slotsTimer); this.slotsTimer = null; } + + var Promise = PromiseContainer.get(); if (status === 'wait') { - var ret = Promise.resolve('OK').nodeify(callback); + var ret = asCallback(Promise.resolve('OK'), callback) // use setImmediate to make sure "close" event // being emitted after quit() is returned @@ -283,11 +287,14 @@ Cluster.prototype.quit = function (callback) { return ret; } - return Promise.all(this.nodes().map(function (node) { - return node.quit(); - })).then(function () { - return 'OK'; - }).nodeify(callback); + return asCallback( + Promise.all(this.nodes().map(function (node) { + return node.quit(); + })).then(function () { + return 'OK'; + }), + callback + ); }; /** diff --git a/lib/command.js b/lib/command.js index 8bfaf317..5662d79d 100644 --- a/lib/command.js +++ b/lib/command.js @@ -1,11 +1,12 @@ 'use strict'; var _ = require('./utils/lodash'); -var Promise = require('bluebird'); var fbuffer = require('flexbuffer'); var utils = require('./utils'); var commands = require('redis-commands'); var calculateSlot = require('cluster-key-slot'); +var PromiseContainer = require('./promise_container'); +var asCallback = require('standard-as-callback'); /** * Command instance @@ -60,7 +61,8 @@ function Command(name, args, options, callback) { Command.prototype.initPromise = function () { var _this = this; - this.promise = new Promise(function (resolve, reject) { + var Promise = PromiseContainer.get(); + var promise = new Promise(function (resolve, reject) { if (!_this.transformed) { _this.transformed = true; var transformer = Command._transformer.argument[_this.name]; @@ -78,7 +80,9 @@ Command.prototype.initPromise = function () { } else { _this.reject = reject; } - }).nodeify(this.callback); + }); + + this.promise = asCallback(promise, this.callback); }; Command.prototype.getSlot = function () { diff --git a/lib/commander.js b/lib/commander.js index cc8dfa3b..60e36f81 100644 --- a/lib/commander.js +++ b/lib/commander.js @@ -3,7 +3,8 @@ var _ = require('./utils/lodash'); var Command = require('./command'); var Script = require('./script'); -var Promise = require('bluebird'); +var PromiseContainer = require('./promise_container'); +var asCallback = require('standard-as-callback'); var DROP_BUFFER_SUPPORT_ERROR = '*Buffer methods are not available ' + 'because "dropBufferSupport" option is enabled.' + @@ -114,7 +115,10 @@ function generateFunction(_commandName, _encoding) { var options; if (this.options.dropBufferSupport) { if (!_encoding) { - return Promise.reject(new Error(DROP_BUFFER_SUPPORT_ERROR)).nodeify(callback); + return asCallback( + PromiseContainer.get().reject(new Error(DROP_BUFFER_SUPPORT_ERROR)), + callback + ) } options = { replyEncoding: null }; } else { @@ -150,7 +154,10 @@ function generateScriptingFunction(_script, _encoding) { var options; if (this.options.dropBufferSupport) { if (!_encoding) { - return Promise.reject(new Error(DROP_BUFFER_SUPPORT_ERROR)).nodeify(callback); + return asCallback( + PromiseContainer.get().reject(new Error(DROP_BUFFER_SUPPORT_ERROR)), + callback + ) } options = { replyEncoding: null }; } else { diff --git a/lib/connectors/connector.js b/lib/connectors/connector.js index 7c746c3f..d7bb6bb4 100644 --- a/lib/connectors/connector.js +++ b/lib/connectors/connector.js @@ -29,7 +29,7 @@ Connector.prototype.connect = function (callback) { connectionOptions = _.pick(this.options, ['port', 'host', 'family']); } if (this.options.tls) { - _.assign(connectionOptions, this.options.tls); + Object.assign(connectionOptions, this.options.tls); } var _this = this; diff --git a/lib/pipeline.js b/lib/pipeline.js index 6fac8374..10be7c66 100644 --- a/lib/pipeline.js +++ b/lib/pipeline.js @@ -3,10 +3,11 @@ var _ = require('./utils/lodash'); var Commander = require('./commander'); var Command = require('./command'); var fbuffer = require('flexbuffer'); -var Promise = require('bluebird'); var util = require('util'); +var asCallback = require('standard-as-callback'); var commands = require('redis-commands'); var calculateSlot = require('cluster-key-slot'); +var PromiseContainer = require('./promise_container'); function Pipeline(redis) { Commander.call(this); @@ -20,13 +21,14 @@ function Pipeline(redis) { this._shaToScript = {}; var _this = this; - _.keys(redis.scriptsSet).forEach(function (name) { + Object.keys(redis.scriptsSet).forEach(function (name) { var script = redis.scriptsSet[name]; _this._shaToScript[script.sha] = script; _this[name] = redis[name]; _this[name + 'Buffer'] = redis[name + 'Buffer']; }); + var Promise = PromiseContainer.get() this.promise = new Promise(function (resolve, reject) { _this.resolve = resolve; _this.reject = reject; @@ -39,7 +41,7 @@ function Pipeline(redis) { }); } -_.assign(Pipeline.prototype, Commander.prototype); +Object.assign(Pipeline.prototype, Commander.prototype); Pipeline.prototype.fillResult = function (value, position) { var i; @@ -213,7 +215,7 @@ Pipeline.prototype.exec = function (callback) { } if (!this.nodeifiedPromise) { this.nodeifiedPromise = true; - this.promise.nodeify(callback); + asCallback(this.promise, callback); } if (_.isEmpty(this._queue)) { this.resolve([]); @@ -273,6 +275,7 @@ Pipeline.prototype.exec = function (callback) { pending.push(scripts[i]); } } + var Promise = PromiseContainer.get() return Promise.all(pending.map(function (script) { return _this.redis.script('load', script.lua); })); diff --git a/lib/promise_container.js b/lib/promise_container.js new file mode 100644 index 00000000..57b00dba --- /dev/null +++ b/lib/promise_container.js @@ -0,0 +1,19 @@ +exports.isPromise = function (obj) { + return !!obj && + (typeof obj === 'object' || typeof obj === 'function') && + typeof obj.then === 'function' +} + +let promise = global.Promise + +exports.get = function () { + return promise +} + +exports.set = function (lib) { + if (typeof lib !== 'function') { + throw new Error(`Provided Promise must be a function, got ${lib}`) + } + + promise = lib +} diff --git a/lib/redis.js b/lib/redis.js index 8870becd..332fb992 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -3,17 +3,18 @@ var _ = require('./utils/lodash'); var util = require('util'); var EventEmitter = require('events').EventEmitter; -var Promise = require('bluebird'); var Deque = require('denque'); var Command = require('./command'); var Commander = require('./commander'); var utils = require('./utils'); +var asCallback = require('standard-as-callback'); var eventHandler = require('./redis/event_handler'); var debug = require('./utils/debug')('ioredis:redis'); var Connector = require('./connectors/connector'); var SentinelConnector = require('./connectors/sentinel_connector'); var ScanStream = require('./scan_stream'); var commands = require('redis-commands'); +var PromiseContainer = require('./promise_container'); /** * Creates a Redis instance @@ -131,7 +132,7 @@ function Redis() { } util.inherits(Redis, EventEmitter); -_.assign(Redis.prototype, Commander.prototype); +Object.assign(Redis.prototype, Commander.prototype); /** * Create a Redis instance @@ -248,7 +249,8 @@ Redis.prototype.setStatus = function (status, arg) { * @public */ Redis.prototype.connect = function (callback) { - return new Promise(function (resolve, reject) { + var Promise = PromiseContainer.get(); + var promise = new Promise(function (resolve, reject) { if (this.status === 'connecting' || this.status === 'connect' || this.status === 'ready') { reject(new Error('Redis is already connecting/connected')); return; @@ -315,7 +317,9 @@ Redis.prototype.connect = function (callback) { }, function (type, err) { _this.silentEmit(type, err); }); - }.bind(this)).nodeify(callback); + }.bind(this)) + + return asCallback(promise, callback) }; /** @@ -362,7 +366,7 @@ Redis.prototype.end = function () { * @public */ Redis.prototype.duplicate = function (override) { - return new Redis(_.assign(_.cloneDeep(this.options), override || {})); + return new Redis(Object.assign(_.cloneDeep(this.options), override || {})); }; /** @@ -511,11 +515,15 @@ Redis.prototype.monitor = function (callback) { lazyConnect: false }); - return new Promise(function (resolve) { - monitorInstance.once('monitoring', function () { - resolve(monitorInstance); - }); - }).nodeify(callback); + var Promise = PromiseContainer.get(); + return asCallback( + new Promise(function (resolve) { + monitorInstance.once('monitoring', function () { + resolve(monitorInstance); + }); + }), + callback + ); }; require('./transaction').addTransactionSupport(Redis.prototype); @@ -636,6 +644,6 @@ Redis.prototype.sendCommand = function (command, stream) { }; }); -_.assign(Redis.prototype, require('./redis/parser')); +Object.assign(Redis.prototype, require('./redis/parser')); module.exports = Redis; diff --git a/lib/script.js b/lib/script.js index 6b85d582..bf4ec091 100644 --- a/lib/script.js +++ b/lib/script.js @@ -2,7 +2,8 @@ var Command = require('./command'); var crypto = require('crypto'); -var Promise = require('bluebird'); +var PromiseContainer = require('./promise_container') +var asCallback = require('standard-as-callback'); function Script(lua, numberOfKeys, keyPrefix) { this.lua = lua; @@ -22,19 +23,24 @@ Script.prototype.execute = function (container, args, options, callback) { var evalsha = new Command('evalsha', [this.sha].concat(args), options); evalsha.isCustomCommand = true; var result = container.sendCommand(evalsha); - if (result instanceof Promise) { + if (PromiseContainer.isPromise(result)) { var _this = this; - return result.catch(function (err) { - if (err.toString().indexOf('NOSCRIPT') === -1) { - throw err; - } - return container.sendCommand(new Command('eval', [_this.lua].concat(args), options)); - }).nodeify(callback); + return asCallback( + result.catch(function (err) { + if (err.toString().indexOf('NOSCRIPT') === -1) { + throw err; + } + return container.sendCommand( + new Command('eval', [_this.lua].concat(args), options) + ); + }), + callback + ) } // result is not a Promise--probably returned from a pipeline chain; however, // we still need the callback to fire when the script is evaluated - evalsha.promise.nodeify(callback); + asCallback(evalsha.promise, callback) return result; }; diff --git a/lib/transaction.js b/lib/transaction.js index 8e688a4c..74eb1333 100644 --- a/lib/transaction.js +++ b/lib/transaction.js @@ -2,6 +2,7 @@ var Pipeline = require('./pipeline'); var utils = require('./utils'); +var asCallback = require('standard-as-callback'); exports.addTransactionSupport = function (redis) { redis.pipeline = function (commands) { @@ -32,7 +33,7 @@ exports.addTransactionSupport = function (redis) { exec.call(pipeline); } var promise = exec.call(pipeline); - return promise.then(function (result) { + return asCallback(promise.then(function (result) { var execResult = result[result.length - 1]; if (execResult[0]) { execResult[0].previousErrors = []; @@ -44,7 +45,7 @@ exports.addTransactionSupport = function (redis) { throw execResult[0]; } return utils.wrapMultiResult(execResult[1]); - }).nodeify(callback); + }), callback) }; var execBuffer = pipeline.execBuffer; @@ -59,11 +60,11 @@ exports.addTransactionSupport = function (redis) { var exec = redis.exec; redis.exec = function (callback) { - return exec.call(this).then(function (results) { + return asCallback(exec.call(this).then(function (results) { if (Array.isArray(results)) { results = utils.wrapMultiResult(results); } return results; - }).nodeify(callback); + }), callback); }; }; diff --git a/lib/utils/lodash.js b/lib/utils/lodash.js index 1e7dabca..abed9971 100644 --- a/lib/utils/lodash.js +++ b/lib/utils/lodash.js @@ -2,15 +2,13 @@ exports.forEach = require('lodash.foreach'); exports.pick = require('lodash.pick'); -exports.assign = require('lodash.assign'); exports.defaults = require('lodash.defaults'); -exports.noop = require('lodash.noop'); +exports.noop = function () {}; exports.difference = require('lodash.difference'); exports.clone = require('lodash.clone'); exports.sample = require('lodash.sample'); exports.flatten = require('lodash.flatten'); exports.bind = require('lodash.bind'); -exports.keys = require('lodash.keys'); exports.isEmpty = require('lodash.isempty'); exports.values = require('lodash.values'); exports.shuffle = require('lodash.shuffle'); diff --git a/package-lock.json b/package-lock.json index 92f7e61a..0bde6e88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,9 +89,10 @@ "dev": true }, "bluebird": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", - "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true }, "brace-expansion": { "version": "1.1.8", @@ -660,12 +661,12 @@ "dev": true, "requires": { "babylon": "7.0.0-beta.19", - "bluebird": "3.5.0", + "bluebird": "3.5.1", "catharsis": "0.8.9", "escape-string-regexp": "1.0.5", "js2xmlparser": "3.0.0", "klaw": "2.0.0", - "marked": "~0.3.6", + "marked": "0.3.19", "mkdirp": "0.5.1", "requizzle": "0.2.1", "strip-json-comments": "2.0.1", @@ -697,12 +698,12 @@ "dev": true, "requires": { "babylon": "7.0.0-beta.19", - "bluebird": "3.5.0", + "bluebird": "3.5.1", "catharsis": "0.8.9", "escape-string-regexp": "1.0.5", "js2xmlparser": "3.0.0", "klaw": "2.0.0", - "marked": "~0.3.6", + "marked": "0.3.19", "mkdirp": "0.5.1", "requizzle": "0.2.1", "strip-json-comments": "2.0.1", @@ -776,11 +777,6 @@ "type-check": "0.3.2" } }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" - }, "lodash.bind": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", @@ -827,22 +823,12 @@ "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" }, - "lodash.keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.2.0.tgz", - "integrity": "sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU=" - }, "lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=", "dev": true }, - "lodash.noop": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz", - "integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw=" - }, "lodash.omit": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", @@ -961,12 +947,12 @@ "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" } } } @@ -1248,6 +1234,11 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "standard-as-callback": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-1.0.0.tgz", + "integrity": "sha512-v5T3aR0T22yS33dBT0UYwZwwunbnjF/XSACF5hhq26u0zvhUxjTNYr89bdDGR9l78SAcWr5KMwn443QdxEUIag==" + }, "stream-connect": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-connect/-/stream-connect-1.0.2.tgz", @@ -1286,7 +1277,7 @@ "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "3.0.0" }, "dependencies": { "has-flag": { @@ -1304,7 +1295,7 @@ "dev": true, "requires": { "array-back": "2.0.0", - "deep-extend": "~0.5.0", + "deep-extend": "0.5.1", "lodash.padend": "4.6.1", "typical": "2.6.1", "wordwrapjs": "3.0.0" diff --git a/package.json b/package.json index a08e3278..20895ebe 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,10 @@ "author": "luin (http://zihua.li)", "license": "MIT", "dependencies": { - "bluebird": "^3.3.4", "cluster-key-slot": "^1.0.6", "debug": "^3.1.0", "denque": "^1.1.0", "flexbuffer": "0.0.6", - "lodash.assign": "^4.2.0", "lodash.bind": "^4.2.1", "lodash.clone": "^4.5.0", "lodash.clonedeep": "^4.5.0", @@ -40,17 +38,17 @@ "lodash.flatten": "^4.4.0", "lodash.foreach": "^4.5.0", "lodash.isempty": "^4.4.0", - "lodash.keys": "^4.2.0", - "lodash.noop": "^3.0.1", "lodash.partial": "^4.2.1", "lodash.pick": "^4.4.0", "lodash.sample": "^4.2.1", "lodash.shuffle": "^4.2.0", "lodash.values": "^4.3.0", "redis-commands": "^1.2.0", - "redis-parser": "^2.4.0" + "redis-parser": "^2.4.0", + "standard-as-callback": "^1.0.0" }, "devDependencies": { + "bluebird": "^3.5.1", "chai": "^3.5.0", "cz-conventional-changelog": "^2.0.0", "istanbul": "^0.4.2", diff --git a/test/functional/promise.js b/test/functional/promise.js new file mode 100644 index 00000000..e55f372d --- /dev/null +++ b/test/functional/promise.js @@ -0,0 +1,20 @@ +var bluebirdPromise = require('bluebird'); +var nativePromise = global.Promise; + +describe('Promise', function () { + it('uses native promise by default', function () { + var redis = new Redis(); + expect(redis.get('foo').constructor).to.eql(nativePromise) + }); + + it('can switch to a custom Promise implementation', function () { + var origin = Redis.Promise; + Redis.Promise = bluebirdPromise + + var redis = new Redis(); + expect(redis.get('foo').constructor).to.eql(bluebirdPromise) + + Redis.Promise = origin + expect(redis.get('foo').constructor).to.eql(origin) + }); +}); diff --git a/test/functional/show_friendly_error_stack.js b/test/functional/show_friendly_error_stack.js index 2218f51a..25e60f6e 100644 --- a/test/functional/show_friendly_error_stack.js +++ b/test/functional/show_friendly_error_stack.js @@ -1,17 +1,16 @@ 'use strict'; +var path = require('path'); +var scriptName = path.basename(__filename); + describe('showFriendlyErrorStack', function () { it('should show friendly error stack', function (done) { var redis = new Redis({ showFriendlyErrorStack: true }); - stub(process.stderr, 'write', function (data) { - var errors = data.split('\n'); - if (errors[0].indexOf('Unhandled') !== -1) { - expect(errors[0].indexOf('ReplyError')).not.eql(-1); - expect(errors[1].indexOf('show_friendly_error_stack.js')).not.eql(-1); - process.stderr.write.restore(); - done(); - } + redis.set('foo').catch(function (err) { + var errors = err.stack.split('\n'); + expect(errors[0].indexOf('ReplyError')).not.eql(-1); + expect(errors[1].indexOf(scriptName)).not.eql(-1); + done(); }); - redis.set('foo'); }); }); diff --git a/test/functional/watch-exec.js b/test/functional/watch-exec.js index 275a5932..83136b63 100644 --- a/test/functional/watch-exec.js +++ b/test/functional/watch-exec.js @@ -1,10 +1,9 @@ 'use strict'; describe('watch-exec', function () { - - it('should support watch/exec transactions', function (done) { + it('should support watch/exec transactions', function () { var redis1 = new Redis(); - redis1.watch('watchkey') + return redis1.watch('watchkey') .then(function() { return redis1.multi().set('watchkey', '1').exec(); }) @@ -12,13 +11,12 @@ describe('watch-exec', function () { expect(result.length).to.eql(1); expect(result[0]).to.eql([null, 'OK']); }) - .nodeify(done); }); - it('should support watch/exec transaction rollback', function (done) { + it('should support watch/exec transaction rollback', function () { var redis1 = new Redis(); var redis2 = new Redis(); - redis1.watch('watchkey') + return redis1.watch('watchkey') .then(function() { return redis2.set('watchkey', '2'); }) @@ -28,7 +26,6 @@ describe('watch-exec', function () { .then(function(result) { expect(result).to.be.null; }) - .nodeify(done); }); }); diff --git a/test/unit/cluster.js b/test/unit/cluster.js index b939b63e..bb82a9ef 100644 --- a/test/unit/cluster.js +++ b/test/unit/cluster.js @@ -1,7 +1,6 @@ 'use strict'; var Cluster = require('../../lib/cluster'); -var Promise = require('bluebird'); describe('cluster', function () { beforeEach(function () { diff --git a/test/unit/redis.js b/test/unit/redis.js index ed3dfe8e..b6717349 100644 --- a/test/unit/redis.js +++ b/test/unit/redis.js @@ -1,7 +1,5 @@ 'use strict'; -var Promise = require('bluebird'); - describe('Redis', function () { describe('constructor', function () { it('should parse options correctly', function () { From 68578a8cbc685b6bc689ce15e686e997c67c6817 Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 24 Jun 2018 01:38:41 +0800 Subject: [PATCH 05/12] feat: disallow call `Redis()` as a function BREAKING CHANGE: The `new` keyword is required explicitly. So `Redis(/* options */)` will not work, use `new Redis(/* options */)` instead. --- lib/redis.js | 9 ++------- test/unit/redis.js | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/redis.js b/lib/redis.js index 332fb992..c4263698 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -91,7 +91,6 @@ var PromiseContainer = require('./promise_container'); * var Redis = require('ioredis'); * * var redis = new Redis(); - * // or: var redis = Redis(); * * var redisOnPort6380 = new Redis(6380); * var anotherRedis = new Redis(6380, '192.168.100.1'); @@ -103,10 +102,6 @@ var PromiseContainer = require('./promise_container'); * ``` */ function Redis() { - if (!(this instanceof Redis)) { - return new Redis(arguments[0], arguments[1], arguments[2]); - } - this.parseOptions(arguments[0], arguments[1], arguments[2]); EventEmitter.call(this); @@ -139,8 +134,8 @@ Object.assign(Redis.prototype, Commander.prototype); * * @deprecated */ -Redis.createClient = function () { - return Redis.apply(this, arguments); +Redis.createClient = function (...args) { + return new Redis(...args); }; /** diff --git a/test/unit/redis.js b/test/unit/redis.js index b6717349..55fb883d 100644 --- a/test/unit/redis.js +++ b/test/unit/redis.js @@ -82,8 +82,8 @@ describe('Redis', function () { } Redis.prototype.connect.restore(); - function getOption() { - var redis = Redis.apply(null, arguments); + function getOption(...args) { + var redis = new Redis(...args); return redis.options; } }); From 4e91a48d309cc3870b7becdc2e6f3419586a9eb3 Mon Sep 17 00:00:00 2001 From: luin Date: Sun, 24 Jun 2018 02:00:12 +0800 Subject: [PATCH 06/12] chore: upgrade node engine requirement --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 20895ebe..98c9c99f 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "lodash.shuffle": "^4.2.0", "lodash.values": "^4.3.0", "redis-commands": "^1.2.0", - "redis-parser": "^2.4.0", + "redis-parser": "^2.6.0", "standard-as-callback": "^1.0.0" }, "devDependencies": { @@ -59,8 +59,7 @@ "sinon": "^1.17.3" }, "engines": { - "node": ">= 0.10.16", - "iojs": ">= 1.0.0" + "node": ">=4" }, "config": { "commitizen": { From 1babc13b6b9634eaf4bbe6fea1ab28ddfeffba33 Mon Sep 17 00:00:00 2001 From: luin Date: Mon, 25 Jun 2018 01:53:20 +0800 Subject: [PATCH 07/12] feat: add maxRetriesPerRequest option to limit the retries attempts per command #634, #61 BREAKING CHANGE: The maxRetriesPerRequest is set to 20 instead of null (same behavior as ioredis v3) by default. So when a redis server is down, pending commands won't wait forever until the connection become alive, instead, they only wait about 10s (depends on the retryStrategy option) --- README.md | 10 ++++ lib/errors/MaxRetriesPerRequestError.js | 10 ++++ lib/errors/index.js | 1 + lib/redis.js | 3 +- lib/redis/event_handler.js | 16 +++++++ test/functional/maxRetriesPerRequest.js | 64 +++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 lib/errors/MaxRetriesPerRequestError.js create mode 100644 lib/errors/index.js create mode 100644 test/functional/maxRetriesPerRequest.js diff --git a/README.md b/README.md index e49791ec..09aa47fd 100644 --- a/README.md +++ b/README.md @@ -546,6 +546,16 @@ This behavior can be disabled by setting the `autoResubscribe` option to `false` And if the previous connection has some unfulfilled commands (most likely blocking commands such as `brpop` and `blpop`), the client will resend them when reconnected. This behavior can be disabled by setting the `autoResendUnfulfilledCommands` option to `false`. +By default, all pending commands will be flushed with an error every 20 retry attempts. That makes sure commands won't wait forever when the connection is down. You can change this behavior by setting `maxRetriesPerRequest`: + +```javascript +var redis = new Redis({ + maxRetriesPerRequest: 1 +}); +``` + +Set maxRetriesPerRequest to `null` to disable this behavior, and every command will wait forever until the connection is alive again (which is the default behavior before ioredis v4). + ### Reconnect on error Besides auto-reconnect when the connection is closed, ioredis supports reconnecting on the specified errors by the `reconnectOnError` option. Here's an example that will reconnect when receiving `READONLY` error: diff --git a/lib/errors/MaxRetriesPerRequestError.js b/lib/errors/MaxRetriesPerRequestError.js new file mode 100644 index 00000000..6e92e3be --- /dev/null +++ b/lib/errors/MaxRetriesPerRequestError.js @@ -0,0 +1,10 @@ +module.exports = class MaxRetriesPerRequestError extends Error { + constructor (maxRetriesPerRequest) { + var message = `Reached the max retries per request limit (which is ${maxRetriesPerRequest}). Refer to "maxRetriesPerRequest" option for details.`; + + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +}; + diff --git a/lib/errors/index.js b/lib/errors/index.js new file mode 100644 index 00000000..6a342fd4 --- /dev/null +++ b/lib/errors/index.js @@ -0,0 +1 @@ +exports.MaxRetriesPerRequestError = require('./MaxRetriesPerRequestError') diff --git a/lib/redis.js b/lib/redis.js index c4263698..fc5addd5 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -176,7 +176,8 @@ Redis.defaultOptions = { keyPrefix: '', reconnectOnError: null, readOnly: false, - stringNumbers: false + stringNumbers: false, + maxRetriesPerRequest: 20 }; Redis.prototype.resetCommandQueue = function () { diff --git a/lib/redis/event_handler.js b/lib/redis/event_handler.js index 4345eff7..77805594 100644 --- a/lib/redis/event_handler.js +++ b/lib/redis/event_handler.js @@ -4,6 +4,7 @@ var debug = require('../utils/debug')('ioredis:connection'); var Command = require('../command'); var utils = require('../utils'); var _ = require('../utils/lodash'); +var {MaxRetriesPerRequestError} = require('../errors') exports.connectHandler = function (self) { return function () { @@ -94,6 +95,21 @@ exports.closeHandler = function (self) { self.reconnectTimeout = null; self.connect().catch(_.noop); }, retryDelay); + + var {maxRetriesPerRequest} = self.options; + if (typeof maxRetriesPerRequest === 'number') { + if (maxRetriesPerRequest < 0) { + debug('maxRetriesPerRequest is negative, ignoring...') + } else { + var remainder = self.retryAttempts % (maxRetriesPerRequest + 1); + if (remainder === 0) { + debug('reach maxRetriesPerRequest limitation, flushing command queue...'); + self.flushQueue( + new MaxRetriesPerRequestError(maxRetriesPerRequest) + ); + } + } + } }; function close() { diff --git a/test/functional/maxRetriesPerRequest.js b/test/functional/maxRetriesPerRequest.js new file mode 100644 index 00000000..0e99e769 --- /dev/null +++ b/test/functional/maxRetriesPerRequest.js @@ -0,0 +1,64 @@ +'use strict'; + +var {MaxRetriesPerRequestError} = require('../../lib/errors') + +describe('maxRetriesPerRequest', function () { + it('throw the correct error when reached the limit', function (done) { + var redis = new Redis(9999, { + retryStrategy() { + return 1 + } + }); + redis.get('foo', (err) => { + expect(err).instanceOf(MaxRetriesPerRequestError) + done() + }) + }) + + it('defaults to max 20 retries', function (done) { + var redis = new Redis(9999, { + retryStrategy() { + return 1 + } + }); + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(21) + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(42) + done() + }) + }) + }); + + it('can be changed', function (done) { + var redis = new Redis(9999, { + maxRetriesPerRequest: 1, + retryStrategy() { + return 1 + } + }); + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(2) + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(4) + done() + }) + }) + }); + + it('allows 0', function (done) { + var redis = new Redis(9999, { + maxRetriesPerRequest: 0, + retryStrategy() { + return 1 + } + }); + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(1) + redis.get('foo', () => { + expect(redis.retryAttempts).to.eql(2) + done() + }) + }) + }); +}); From 7517a7322e6cb1320b9e6926759ec741f5f9a426 Mon Sep 17 00:00:00 2001 From: Shahar Mor Date: Thu, 28 Jun 2018 23:48:05 +0300 Subject: [PATCH 08/12] feat: wait for ready state before resolving cluster.connect() BREAKING CHANGE: `Cluster#connect()` will be resolved when the connection status become `ready` instead of `connect`. --- lib/cluster/index.js | 52 ++++++++++++++++-------------- test/functional/cluster/connect.js | 29 +++++++++++++++++ 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/lib/cluster/index.js b/lib/cluster/index.js index 473e8699..7b63f67b 100644 --- a/lib/cluster/index.js +++ b/lib/cluster/index.js @@ -135,13 +135,6 @@ Cluster.prototype.resetNodesRefreshInterval = function () { * @public */ Cluster.prototype.connect = function () { - function readyHandler() { - this.setStatus('ready'); - this.retryAttempts = 0; - this.executeOfflineCommands(); - this.resetNodesRefreshInterval(); - } - var Promise = PromiseContainer.get(); return new Promise(function (resolve, reject) { if (this.status === 'connecting' || this.status === 'connect' || this.status === 'ready') { @@ -156,6 +149,14 @@ Cluster.prototype.connect = function () { this.connectionPool.reset(this.startupNodes); + function readyHandler() { + this.setStatus('ready'); + this.retryAttempts = 0; + this.executeOfflineCommands(); + this.resetNodesRefreshInterval(); + resolve(); + } + var closeListener; var refreshListener = function () { this.removeListener('close', closeListener); @@ -164,7 +165,7 @@ Cluster.prototype.connect = function () { if (this.options.enableReadyCheck) { this._readyCheck(function (err, fail) { if (err || fail) { - debug('Ready check failed (%s). Reconnecting...', err || fail) + debug('Ready check failed (%s). Reconnecting...', err || fail); if (this.status === 'connect') { this.disconnect(true); } @@ -175,7 +176,6 @@ Cluster.prototype.connect = function () { } else { readyHandler.call(this); } - resolve(); }; closeListener = function () { @@ -276,7 +276,7 @@ Cluster.prototype.quit = function (callback) { var Promise = PromiseContainer.get(); if (status === 'wait') { - var ret = asCallback(Promise.resolve('OK'), callback) + var ret = asCallback(Promise.resolve('OK'), callback); // use setImmediate to make sure "close" event // being emitted after quit() is returned @@ -530,9 +530,9 @@ Cluster.prototype.sendCommand = function (command, stream, node) { if (typeof to === 'function') { var nodes = nodeKeys - .map(function (key) { - return _this.connectionPool.nodes.all[key]; - }); + .map(function (key) { + return _this.connectionPool.nodes.all[key]; + }); redis = to(nodes, command); if (Array.isArray(redis)) { redis = utils.sample(redis); @@ -603,7 +603,11 @@ Cluster.prototype.handleError = function (error, ttl, handlers) { timeout: this.options.retryDelayOnClusterDown, callback: this.refreshSlotsCache.bind(this) }); - } else if (error.message === utils.CONNECTION_CLOSED_ERROR_MSG && this.options.retryDelayOnFailover > 0 && this.status === 'ready') { + } else if ( + error.message === utils.CONNECTION_CLOSED_ERROR_MSG && + this.options.retryDelayOnFailover > 0 && + this.status === 'ready' + ) { this.delayQueue.push('failover', handlers.connectionClosed, { timeout: this.options.retryDelayOnFailover, callback: this.refreshSlotsCache.bind(this) @@ -683,16 +687,16 @@ Cluster.prototype._readyCheck = function (callback) { }; ['sscan', 'hscan', 'zscan', 'sscanBuffer', 'hscanBuffer', 'zscanBuffer'] -.forEach(function (command) { - Cluster.prototype[command + 'Stream'] = function (key, options) { - return new ScanStream(_.defaults({ - objectMode: true, - key: key, - redis: this, - command: command - }, options)); - }; -}); + .forEach(function (command) { + Cluster.prototype[command + 'Stream'] = function (key, options) { + return new ScanStream(_.defaults({ + objectMode: true, + key: key, + redis: this, + command: command + }, options)); + }; + }); require('../transaction').addTransactionSupport(Cluster.prototype); diff --git a/test/functional/cluster/connect.js b/test/functional/cluster/connect.js index a2368e62..f20da4e2 100644 --- a/test/functional/cluster/connect.js +++ b/test/functional/cluster/connect.js @@ -75,6 +75,34 @@ describe('cluster:connect', function () { }); }); + it('should wait for ready state before resolving', function (done) { + var slotTable = [ + [0, 16383, ['127.0.0.1', 30001]] + ]; + var argvHandler = function (argv) { + if (argv[0] === 'info') { + // return 'role:master' + } + if (argv[0] === 'cluster' && argv[1] === 'slots') { + return slotTable; + } + if (argv[0] === 'cluster' && argv[1] === 'info') { + return 'cluster_state:ok'; + } + }; + var node = new MockServer(30001, argvHandler); + + var cluster = new Redis.Cluster([ + { host: '127.0.0.1', port: '30001' } + ], { lazyConnect: true }); + + cluster.connect().then(function () { + expect(cluster.status).to.eql('ready'); + cluster.disconnect(); + disconnect([node], done); + }); + }); + it('should support url schema', function (done) { var node = new MockServer(30001); @@ -249,6 +277,7 @@ describe('cluster:connect', function () { expect(err.message).to.eql(errorMessage); checkDone(); }); + function checkDone() { if (!--pending) { cluster.disconnect(); From f0c600be8ee5edaa1f24d64ff512f507dab04741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=90=E9=AA=85?= Date: Sat, 30 Jun 2018 11:29:06 +0800 Subject: [PATCH 09/12] feat: `Redis#connect()` will be resolved when status is ready (#648) BREAKING CHANGE: `Redis#connect()` will be resolved when status is ready instead of `connect`: ``` const redis = new Redis({ lazyConnect: true }) redis.connect().then(() => { assert(redis.status === 'ready') }) ``` --- API.md | 12 +++++++---- lib/redis.js | 32 ++++++++++++++++------------ test/functional/connection.js | 40 ++++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/API.md b/API.md index f77ff538..8d8072dc 100644 --- a/API.md +++ b/API.md @@ -18,7 +18,7 @@ * [Redis](#Redis) ⇐ [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) * [new Redis([port], [host], [options])](#new_Redis_new) * _instance_ - * [.connect([callback])](#Redis+connect) ⇒ Promise + * [.connect([callback])](#Redis+connect) ⇒ Promise.<void> * [.disconnect()](#Redis+disconnect) * ~~[.end()](#Redis+end)~~ * [.duplicate()](#Redis+duplicate) @@ -68,7 +68,6 @@ Creates a Redis instance var Redis = require('ioredis'); var redis = new Redis(); -// or: var redis = Redis(); var redisOnPort6380 = new Redis(6380); var anotherRedis = new Redis(6380, '192.168.100.1'); @@ -80,9 +79,13 @@ var authedRedis = new Redis(6380, '192.168.100.1', { password: 'password' }); ``` -### redis.connect([callback]) ⇒ Promise +### redis.connect([callback]) ⇒ Promise.<void> Create a connection to Redis. -This method will be invoked automatically when creating a new Redis instance. +This method will be invoked automatically when creating a new Redis instance +unless `lazyConnect: true` is passed. + +When calling this method manually, a Promise is returned, which will +be resolved when the connection status is ready. **Kind**: instance method of [Redis](#Redis) **Access**: public @@ -234,6 +237,7 @@ Creates a Redis Cluster instance | [options.retryDelayOnClusterDown] | number | 100 | When a CLUSTERDOWN error is received, client will retry if `retryDelayOnClusterDown` is valid delay time. | | [options.retryDelayOnTryAgain] | number | 100 | When a TRYAGAIN error is received, client will retry if `retryDelayOnTryAgain` is valid delay time. | | [options.slotsRefreshTimeout] | number | 1000 | The milliseconds before a timeout occurs while refreshing slots from the cluster. | +| [options.slotsRefreshInterval] | number | 5000 | The milliseconds between every automatic slots refresh. | | [options.redisOptions] | Object | | Passed to the constructor of `Redis`. | diff --git a/lib/redis.js b/lib/redis.js index fc5addd5..c9cf2c06 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -239,9 +239,13 @@ Redis.prototype.setStatus = function (status, arg) { /** * Create a connection to Redis. - * This method will be invoked automatically when creating a new Redis instance. + * This method will be invoked automatically when creating a new Redis instance + * unless `lazyConnect: true` is passed. + * + * When calling this method manually, a Promise is returned, which will + * be resolved when the connection status is ready. * @param {function} [callback] - * @return {Promise} + * @return {Promise} * @public */ Redis.prototype.connect = function (callback) { @@ -253,9 +257,11 @@ Redis.prototype.connect = function (callback) { } this.setStatus('connecting'); + const {options} = this; + this.condition = { - select: this.options.db, - auth: this.options.password, + select: options.db, + auth: options.password, subscriber: false }; @@ -268,11 +274,11 @@ Redis.prototype.connect = function (callback) { _this.setStatus('end'); return; } - var CONNECT_EVENT = _this.options.tls ? 'secureConnect' : 'connect'; + var CONNECT_EVENT = options.tls ? 'secureConnect' : 'connect'; _this.stream = stream; - if (typeof _this.options.keepAlive === 'number') { - stream.setKeepAlive(true, _this.options.keepAlive); + if (typeof options.keepAlive === 'number') { + stream.setKeepAlive(true, options.keepAlive); } stream.once(CONNECT_EVENT, eventHandler.connectHandler(_this)); @@ -280,8 +286,8 @@ Redis.prototype.connect = function (callback) { stream.once('close', eventHandler.closeHandler(_this)); stream.on('data', eventHandler.dataHandler(_this)); - if (_this.options.connectTimeout) { - stream.setTimeout(_this.options.connectTimeout, function () { + if (options.connectTimeout) { + stream.setTimeout(options.connectTimeout, function () { stream.setTimeout(0); stream.destroy(); @@ -296,19 +302,19 @@ Redis.prototype.connect = function (callback) { }); } - if (_this.options.noDelay) { + if (options.noDelay) { stream.setNoDelay(true); } - var connectionConnectHandler = function () { + var connectionReadyHandler = function () { _this.removeListener('close', connectionCloseHandler); resolve(); }; var connectionCloseHandler = function () { - _this.removeListener(CONNECT_EVENT, connectionConnectHandler); + _this.removeListener('ready', connectionReadyHandler); reject(new Error(utils.CONNECTION_CLOSED_ERROR_MSG)); }; - _this.once(CONNECT_EVENT, connectionConnectHandler); + _this.once('ready', connectionReadyHandler); _this.once('close', connectionCloseHandler); }, function (type, err) { _this.silentEmit(type, err); diff --git a/test/functional/connection.js b/test/functional/connection.js index 9301d77d..fb033d03 100644 --- a/test/functional/connection.js +++ b/test/functional/connection.js @@ -96,7 +96,9 @@ describe('connection', function () { it('should stop reconnecting when disconnected', function (done) { var redis = new Redis(8999, { - retryStrategy: function () { return 0; } + retryStrategy: function () { + return 0; + } }); redis.on('close', function () { @@ -116,6 +118,42 @@ describe('connection', function () { done(); }); }); + + it('should resolve when the status become ready', function (done) { + var redis = new Redis({ lazyConnect: true }); + redis.connect().then(function () { + expect(redis.status).to.eql('ready'); + done(); + }); + }); + + it('should reject when closed (reconnecting)', function (done) { + var redis = new Redis({ + port: 8989, + lazyConnect: true, + retryStrategy: function () { + return 0; + } + }); + + redis.connect().catch(function () { + expect(redis.status).to.eql('reconnecting'); + done(); + }); + }); + + it('should reject when closed (end)', function (done) { + var redis = new Redis({ + port: 8989, + lazyConnect: true, + retryStrategy: false + }); + + redis.connect().catch(function () { + expect(redis.status).to.eql('end'); + done(); + }); + }); }); describe('retryStrategy', function () { From 8a87b957cfbd64f61e463f35fa6ca051ffed62af Mon Sep 17 00:00:00 2001 From: luin Date: Sat, 30 Jun 2018 12:58:28 +0800 Subject: [PATCH 10/12] docs(README): add tips for scanning with async task --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 09aa47fd..3fcae202 100644 --- a/README.md +++ b/README.md @@ -482,7 +482,8 @@ var redis = new Redis(); var stream = redis.scanStream(); var keys = []; stream.on('data', function (resultKeys) { - // `resultKeys` is an array of strings representing key names + // `resultKeys` is an array of strings representing key names. + // Note that resultKeys may contain 0 keys. for (var i = 0; i < resultKeys.length; i++) { keys.push(resultKeys[i]); } @@ -517,6 +518,26 @@ var stream = redis.hscanStream('myhash', { You can learn more from the [Redis documentation](http://redis.io/commands/scan). +**Useful Tips** +It's pretty common that doing an async task in the `data` handler. We'd like the scanning process to be paused until the async task to be finished. `Stream#pause()` and `Stream.resume()` do the trick. For example if we want to migrate data in Redis to MySQL: + +```javascript +var stream = redis.scanStream(); +stream.on('data', function (resultKeys) { + // Pause the stream from scanning more keys until we've migrated the current keys. + stream.pause(); + + Promise.all(resultKeys.map(migrateKeyToMySQL)).then(() => { + // Resume the stream here. + stream.resume(); + }); +}); + +stream.on('end', function () { + console.log('done migration'); +}); +``` + ## Auto-reconnect By default, ioredis will try to reconnect when the connection to Redis is lost except when the connection is closed manually by `redis.disconnect()` or `redis.quit()`. From 8e7c6f170db16b7f2aae2dcbd18661e4beb9eae3 Mon Sep 17 00:00:00 2001 From: luin Date: Sat, 30 Jun 2018 13:34:00 +0800 Subject: [PATCH 11/12] fix: Deprecated `Redis()` in favor of `new Redis()` --- lib/redis.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/redis.js b/lib/redis.js index c9cf2c06..08e491d1 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -102,6 +102,11 @@ var PromiseContainer = require('./promise_container'); * ``` */ function Redis() { + if (!(this instanceof Redis)) { + console.error(new Error('Calling `Redis()` like a function is deprecated. Using `new Redis()` instead.').stack.replace('Error', 'Warning')); + return new Redis(arguments[0], arguments[1], arguments[2]); + } + this.parseOptions(arguments[0], arguments[1], arguments[2]); EventEmitter.call(this); From cfe425843d297b9aadfd43d24d1e54befdbf1b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=90=E9=AA=85?= Date: Sat, 30 Jun 2018 22:53:02 +0800 Subject: [PATCH 12/12] fix(Cluster): issues when setting enableOfflineQueue to false (#649) Offline queue should be enabled on the internal cluster nodes so that we don't need to wait for the `ready` event before sending commands to the node. --- lib/cluster/connection_pool.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/cluster/connection_pool.js b/lib/cluster/connection_pool.js index 82c5ba5f..d06cf83d 100644 --- a/lib/cluster/connection_pool.js +++ b/lib/cluster/connection_pool.js @@ -46,7 +46,7 @@ ConnectionPool.prototype.findOrCreate = function (node, readOnly) { redis = this.nodes.all[node.key]; if (redis.options.readOnly !== readOnly) { redis.options.readOnly = readOnly; - debug('Change role of %s to %s', node.key, readOnly ? 'slave' : 'master') + debug('Change role of %s to %s', node.key, readOnly ? 'slave' : 'master'); redis[readOnly ? 'readonly' : 'readwrite']().catch(_.noop); if (readOnly) { delete this.nodes.master[node.key]; @@ -59,7 +59,14 @@ ConnectionPool.prototype.findOrCreate = function (node, readOnly) { } else { debug('Connecting to %s as %s', node.key, readOnly ? 'slave' : 'master'); redis = new Redis(_.defaults({ + // Never try to reconnect when a node is lose, + // instead, waiting for a `MOVED` error and + // fetch the slots again. retryStrategy: null, + // Offline queue should be enabled so that + // we don't need to wait for the `ready` event + // before sending commands to the node. + enableOfflineQueue: true, readOnly: readOnly }, node, this.redisOptions, { lazyConnect: true })); this.nodes.all[node.key] = redis;