ioredis 4.14.4 → 4.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Changelog.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## [4.16.1](https://github.com/luin/ioredis/compare/v4.16.0...v4.16.1) (2020-03-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * abort incomplete pipelines upon reconnect ([#1084](https://github.com/luin/ioredis/issues/1084)) ([0013991](https://github.com/luin/ioredis/commit/0013991b7fbf239ffd74311266bb9e63e22b46cb)), closes [#965](https://github.com/luin/ioredis/issues/965)
7
+
8
+ # [4.16.0](https://github.com/luin/ioredis/compare/v4.15.1...v4.16.0) (2020-02-19)
9
+
10
+
11
+ ### Features
12
+
13
+ * ability force custom scripts to be readOnly and execute on slaves ([#1057](https://github.com/luin/ioredis/issues/1057)) ([a24c3ab](https://github.com/luin/ioredis/commit/a24c3abcf4013e74e25424d2f6b91a2ae0de12b5))
14
+
15
+ ## [4.15.1](https://github.com/luin/ioredis/compare/v4.15.0...v4.15.1) (2019-12-25)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * ignore empty hosts returned by CLUSTER SLOTS ([#1025](https://github.com/luin/ioredis/issues/1025)) ([d79a8ef](https://github.com/luin/ioredis/commit/d79a8ef40f5670af6962b598752dc5a7aa96722c))
21
+ * prevent exception when send custom command ([04cad7f](https://github.com/luin/ioredis/commit/04cad7fbf2db5e14a478e2eb1dc825346abe41dd))
22
+
23
+ # [4.15.0](https://github.com/luin/ioredis/compare/v4.14.4...v4.15.0) (2019-11-29)
24
+
25
+
26
+ ### Features
27
+
28
+ * support multiple fields for hset ([51b1478](https://github.com/luin/ioredis/commit/51b14786eef4c627c178de4967434e8d4a51ebe0))
29
+
1
30
  ## [4.14.4](https://github.com/luin/ioredis/compare/v4.14.3...v4.14.4) (2019-11-22)
2
31
 
3
32
 
package/README.md CHANGED
@@ -76,23 +76,32 @@ $ npm install ioredis
76
76
  ## Basic Usage
77
77
 
78
78
  ```javascript
79
- var Redis = require("ioredis");
80
- var redis = new Redis();
79
+ const Redis = require("ioredis");
80
+ const redis = new Redis(); // uses defaults unless given configuration object
81
81
 
82
- redis.set("foo", "bar");
82
+ // ioredis supports all Redis commands:
83
+ redis.set("foo", "bar"); // returns promise which resolves to string, "OK"
84
+
85
+ // the format is: redis[SOME_REDIS_COMMAND_IN_LOWERCASE](ARGUMENTS_ARE_JOINED_INTO_COMMAND_STRING)
86
+ // the js: ` redis.set("mykey", "Hello") ` is equivalent to the cli: ` redis> SET mykey "Hello" `
87
+
88
+ // ioredis supports the node.js callback style
83
89
  redis.get("foo", function(err, result) {
84
- console.log(result);
90
+ if (err) {
91
+ console.error(err);
92
+ } else {
93
+ console.log(result); // Promise resolves to "bar"
94
+ }
85
95
  });
86
- redis.del("foo");
87
96
 
88
- // Or using a promise if the last argument isn't a function
97
+ // Or ioredis returns a promise if the last argument isn't a function
89
98
  redis.get("foo").then(function(result) {
90
- console.log(result);
99
+ console.log(result); // Prints "bar"
91
100
  });
92
101
 
93
- // Arguments to commands are flattened, so the following are the same:
94
- redis.sadd("set", 1, 3, 5, 7);
95
- redis.sadd("set", [1, 3, 5, 7]);
102
+ // Most responses are strings, or arrays of strings
103
+ redis.zadd("sortedSet", 1, "one", 2, "dos", 4, "quatro", 3, "three")
104
+ redis.zrange("sortedSet", 0, 2, "WITHSCORES").then(res => console.log(res)); // Promise resolves to ["one", "1", "dos", "2", "three", "3"] as if the command was ` redis> ZRANGE sortedSet 0 2 WITHSCORES `
96
105
 
97
106
  // All arguments are passed directly to the redis server:
98
107
  redis.set("key", 100, "EX", 10);
@@ -750,7 +759,7 @@ ioredis **guarantees** that the node you connected to is always a master even af
750
759
 
751
760
  It's possible to connect to a slave instead of a master by specifying the option `role` with the value of `slave` and ioredis will try to connect to a random slave of the specified master, with the guarantee that the connected node is always a slave. If the current node is promoted to master due to a failover, ioredis will disconnect from it and ask the sentinels for another slave node to connect to.
752
761
 
753
- If you specify the option `preferredSlaves` along with `role: 'slave'` ioredis will attempt to use this value when selecting the slave from the pool of available slaves. The value of `preferredSlaves` should either be a function that accepts an array of avaiable slaves and returns a single result, or an array of slave values priorities by the lowest `prio` value first with a default value of `1`.
762
+ If you specify the option `preferredSlaves` along with `role: 'slave'` ioredis will attempt to use this value when selecting the slave from the pool of available slaves. The value of `preferredSlaves` should either be a function that accepts an array of available slaves and returns a single result, or an array of slave values priorities by the lowest `prio` value first with a default value of `1`.
754
763
 
755
764
  ```javascript
756
765
  // available slaves format
@@ -832,21 +841,21 @@ cluster.get("foo", function(err, res) {
832
841
  ioredis will try to reconnect to the startup nodes from scratch after the specified delay (in ms). Otherwise, an error of "None of startup nodes is available" will be returned.
833
842
  The default value of this option is:
834
843
 
835
- ```javascript
836
- function (times) {
837
- var delay = Math.min(100 + times * 2, 2000);
838
- return delay;
839
- }
840
- ```
844
+ ```javascript
845
+ function (times) {
846
+ var delay = Math.min(100 + times * 2, 2000);
847
+ return delay;
848
+ }
849
+ ```
841
850
 
842
- It's possible to modify the `startupNodes` property in order to switch to another set of nodes here:
851
+ It's possible to modify the `startupNodes` property in order to switch to another set of nodes here:
843
852
 
844
- ```javascript
845
- function (times) {
846
- this.startupNodes = [{ port: 6790, host: '127.0.0.1' }];
847
- return Math.min(100 + times * 2, 2000);
848
- }
849
- ```
853
+ ```javascript
854
+ function (times) {
855
+ this.startupNodes = [{ port: 6790, host: '127.0.0.1' }];
856
+ return Math.min(100 + times * 2, 2000);
857
+ }
858
+ ```
850
859
 
851
860
  - `dnsLookup`: Alternative DNS lookup function (`dns.lookup()` is used by default). It may be useful to override this in special cases, such as when AWS ElastiCache used with TLS enabled.
852
861
  - `enableOfflineQueue`: Similar to the `enableOfflineQueue` option of `Redis` class.
@@ -1047,7 +1056,7 @@ var cluster = new Redis.Cluster(
1047
1056
  tls: {}
1048
1057
  },
1049
1058
  }
1050
- };
1059
+ );
1051
1060
  ```
1052
1061
 
1053
1062
  <hr>
@@ -432,8 +432,9 @@ class Cluster extends events_1.EventEmitter {
432
432
  }
433
433
  let to = this.options.scaleReads;
434
434
  if (to !== "master") {
435
- const isCommandReadOnly = commands.exists(command.name) &&
436
- commands.hasFlag(command.name, "readonly");
435
+ const isCommandReadOnly = command.isReadOnly ||
436
+ (commands.exists(command.name) &&
437
+ commands.hasFlag(command.name, "readonly"));
437
438
  if (!isCommandReadOnly) {
438
439
  to = "master";
439
440
  }
@@ -637,6 +638,9 @@ class Cluster extends events_1.EventEmitter {
637
638
  const slotRangeEnd = items[1];
638
639
  const keys = [];
639
640
  for (let j = 2; j < items.length; j++) {
641
+ if (!items[j][0]) {
642
+ continue;
643
+ }
640
644
  items[j] = this.natMapper({ host: items[j][0], port: items[j][1] });
641
645
  items[j].readOnly = j !== 2;
642
646
  nodes.push(items[j]);
package/built/command.js CHANGED
@@ -45,6 +45,7 @@ class Command {
45
45
  this.name = name;
46
46
  this.transformed = false;
47
47
  this.isCustomCommand = false;
48
+ this.inTransaction = false;
48
49
  this.replyEncoding = options.replyEncoding;
49
50
  this.errorStack = options.errorStack;
50
51
  this.args = lodash_1.flatten(args);
@@ -53,6 +54,9 @@ class Command {
53
54
  if (options.keyPrefix) {
54
55
  this._iterateKeys(key => options.keyPrefix + key);
55
56
  }
57
+ if (options.readOnly) {
58
+ this.isReadOnly = true;
59
+ }
56
60
  }
57
61
  static getFlagMap() {
58
62
  if (!this.flagMap) {
@@ -293,6 +297,17 @@ Command.setReplyTransformer("hgetall", function (result) {
293
297
  }
294
298
  return result;
295
299
  });
300
+ Command.setArgumentTransformer("hset", function (args) {
301
+ if (args.length === 2) {
302
+ if (typeof Map !== "undefined" && args[1] instanceof Map) {
303
+ return [args[0]].concat(utils_1.convertMapToArray(args[1]));
304
+ }
305
+ if (typeof args[1] === "object" && args[1] !== null) {
306
+ return [args[0]].concat(utils_1.convertObjectToArray(args[1]));
307
+ }
308
+ }
309
+ return args;
310
+ });
296
311
  class MixedBuffers {
297
312
  constructor() {
298
313
  this.length = 0;
@@ -65,10 +65,11 @@ Commander.prototype.send_command = Commander.prototype.call;
65
65
  * @param {object} definition
66
66
  * @param {string} definition.lua - the lua code
67
67
  * @param {number} [definition.numberOfKeys=null] - the number of keys.
68
+ * @param {boolean} [definition.readOnly=false] - force this script to be readonly so it executes on slaves as well.
68
69
  * If omit, you have to pass the number of keys as the first argument every time you invoke the command
69
70
  */
70
71
  Commander.prototype.defineCommand = function (name, definition) {
71
- var script = new script_1.default(definition.lua, definition.numberOfKeys, this.options.keyPrefix);
72
+ var script = new script_1.default(definition.lua, definition.numberOfKeys, this.options.keyPrefix, definition.readOnly);
72
73
  this.scriptsSet[name] = script;
73
74
  this[name] = generateScriptingFunction(script, "utf8");
74
75
  this[name + "Buffer"] = generateScriptingFunction(script, null);
package/built/pipeline.js CHANGED
@@ -59,16 +59,9 @@ Pipeline.prototype.fillResult = function (value, position) {
59
59
  if (this.isCluster) {
60
60
  let retriable = true;
61
61
  let commonError;
62
- let inTransaction;
63
62
  for (let i = 0; i < this._result.length; ++i) {
64
63
  var error = this._result[i][0];
65
64
  var command = this._queue[i];
66
- if (command.name === "multi") {
67
- inTransaction = true;
68
- }
69
- else if (command.name === "exec") {
70
- inTransaction = false;
71
- }
72
65
  if (error) {
73
66
  if (command.name === "exec" &&
74
67
  error.message ===
@@ -87,7 +80,7 @@ Pipeline.prototype.fillResult = function (value, position) {
87
80
  break;
88
81
  }
89
82
  }
90
- else if (!inTransaction) {
83
+ else if (!command.inTransaction) {
91
84
  var isReadOnly = redis_commands_1.exists(command.name) && redis_commands_1.hasFlag(command.name, "readonly");
92
85
  if (!isReadOnly) {
93
86
  retriable = false;
@@ -99,7 +92,7 @@ Pipeline.prototype.fillResult = function (value, position) {
99
92
  var _this = this;
100
93
  var errv = commonError.message.split(" ");
101
94
  var queue = this._queue;
102
- inTransaction = false;
95
+ let inTransaction = false;
103
96
  this._queue = [];
104
97
  for (let i = 0; i < queue.length; ++i) {
105
98
  if (errv[0] === "ASK" &&
@@ -112,12 +105,7 @@ Pipeline.prototype.fillResult = function (value, position) {
112
105
  }
113
106
  queue[i].initPromise();
114
107
  this.sendCommand(queue[i]);
115
- if (queue[i].name === "multi") {
116
- inTransaction = true;
117
- }
118
- else if (queue[i].name === "exec") {
119
- inTransaction = false;
120
- }
108
+ inTransaction = queue[i].inTransaction;
121
109
  }
122
110
  let matched = true;
123
111
  if (typeof this.leftRedirections === "undefined") {
@@ -162,7 +150,11 @@ Pipeline.prototype.fillResult = function (value, position) {
162
150
  this.resolve(this._result.slice(0, this._result.length - ignoredCount));
163
151
  };
164
152
  Pipeline.prototype.sendCommand = function (command) {
153
+ if (this._transactions > 0) {
154
+ command.inTransaction = true;
155
+ }
165
156
  const position = this._queue.length;
157
+ command.pipelineIndex = position;
166
158
  command.promise
167
159
  .then(result => {
168
160
  this.fillResult([null, result], position);
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const redis_errors_1 = require("redis-errors");
3
4
  const command_1 = require("../command");
4
5
  const errors_1 = require("../errors");
5
6
  const utils_1 = require("../utils");
@@ -67,6 +68,59 @@ function connectHandler(self) {
67
68
  };
68
69
  }
69
70
  exports.connectHandler = connectHandler;
71
+ function abortError(command) {
72
+ const err = new redis_errors_1.AbortError("Command aborted due to connection close");
73
+ err.command = {
74
+ name: command.name,
75
+ args: command.args
76
+ };
77
+ return err;
78
+ }
79
+ // If a contiguous set of pipeline commands starts from index zero then they
80
+ // can be safely reattempted. If however we have a chain of pipelined commands
81
+ // starting at index 1 or more it means we received a partial response before
82
+ // the connection close and those pipelined commands must be aborted. For
83
+ // example, if the queue looks like this: [2, 3, 4, 0, 1, 2] then after
84
+ // aborting and purging we'll have a queue that looks like this: [0, 1, 2]
85
+ function abortIncompletePipelines(commandQueue) {
86
+ let expectedIndex = 0;
87
+ for (let i = 0; i < commandQueue.length;) {
88
+ const command = commandQueue.peekAt(i).command;
89
+ const pipelineIndex = command.pipelineIndex;
90
+ if (pipelineIndex === undefined || pipelineIndex === 0) {
91
+ expectedIndex = 0;
92
+ }
93
+ if (pipelineIndex !== undefined && pipelineIndex !== expectedIndex++) {
94
+ commandQueue.remove(i, 1);
95
+ command.reject(abortError(command));
96
+ continue;
97
+ }
98
+ i++;
99
+ }
100
+ }
101
+ // If only a partial transaction result was received before connection close,
102
+ // we have to abort any transaction fragments that may have ended up in the
103
+ // offline queue
104
+ function abortTransactionFragments(commandQueue) {
105
+ for (let i = 0; i < commandQueue.length;) {
106
+ const command = commandQueue.peekAt(i).command;
107
+ if (command.name === "multi") {
108
+ break;
109
+ }
110
+ if (command.name === "exec") {
111
+ commandQueue.remove(i, 1);
112
+ command.reject(abortError(command));
113
+ break;
114
+ }
115
+ if (command.inTransaction) {
116
+ commandQueue.remove(i, 1);
117
+ command.reject(abortError(command));
118
+ }
119
+ else {
120
+ i++;
121
+ }
122
+ }
123
+ }
70
124
  function closeHandler(self) {
71
125
  return function () {
72
126
  self.setStatus("close");
@@ -74,8 +128,12 @@ function closeHandler(self) {
74
128
  self.prevCondition = self.condition;
75
129
  }
76
130
  if (self.commandQueue.length) {
131
+ abortIncompletePipelines(self.commandQueue);
77
132
  self.prevCommandQueue = self.commandQueue;
78
133
  }
134
+ if (self.offlineQueue.length) {
135
+ abortTransactionFragments(self.offlineQueue);
136
+ }
79
137
  if (self.manuallyClosing) {
80
138
  self.manuallyClosing = false;
81
139
  debug("skip reconnecting since the connection is manually closed.");
@@ -581,6 +581,7 @@ Redis.prototype.sendCommand = function (command, stream) {
581
581
  var writable = this.status === "ready" ||
582
582
  (!stream &&
583
583
  this.status === "connect" &&
584
+ commands.exists(command.name) &&
584
585
  commands.hasFlag(command.name, "loading"));
585
586
  if (!this.stream) {
586
587
  writable = false;
package/built/script.js CHANGED
@@ -5,10 +5,11 @@ const promiseContainer_1 = require("./promiseContainer");
5
5
  const command_1 = require("./command");
6
6
  const standard_as_callback_1 = require("standard-as-callback");
7
7
  class Script {
8
- constructor(lua, numberOfKeys = null, keyPrefix = "") {
8
+ constructor(lua, numberOfKeys = null, keyPrefix = "", readOnly = false) {
9
9
  this.lua = lua;
10
10
  this.numberOfKeys = numberOfKeys;
11
11
  this.keyPrefix = keyPrefix;
12
+ this.readOnly = readOnly;
12
13
  this.sha = crypto_1.createHash("sha1")
13
14
  .update(lua)
14
15
  .digest("hex");
@@ -20,6 +21,9 @@ class Script {
20
21
  if (this.keyPrefix) {
21
22
  options.keyPrefix = this.keyPrefix;
22
23
  }
24
+ if (this.readOnly) {
25
+ options.readOnly = true;
26
+ }
23
27
  const evalsha = new command_1.default("evalsha", [this.sha].concat(args), options);
24
28
  evalsha.isCustomCommand = true;
25
29
  const result = container.sendCommand(evalsha);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ioredis",
3
- "version": "4.14.4",
3
+ "version": "4.16.1",
4
4
  "description": "A robust, performance-focused and full-featured Redis client for Node.js.",
5
5
  "main": "built/index.js",
6
6
  "files": [