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 +29 -0
- package/README.md +34 -25
- package/built/cluster/index.js +6 -2
- package/built/command.js +15 -0
- package/built/commander.js +2 -1
- package/built/pipeline.js +7 -15
- package/built/redis/event_handler.js +58 -0
- package/built/redis/index.js +1 -0
- package/built/script.js +5 -1
- package/package.json +1 -1
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
|
-
|
|
80
|
-
|
|
79
|
+
const Redis = require("ioredis");
|
|
80
|
+
const redis = new Redis(); // uses defaults unless given configuration object
|
|
81
81
|
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
94
|
-
redis.
|
|
95
|
-
redis.
|
|
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
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
844
|
+
```javascript
|
|
845
|
+
function (times) {
|
|
846
|
+
var delay = Math.min(100 + times * 2, 2000);
|
|
847
|
+
return delay;
|
|
848
|
+
}
|
|
849
|
+
```
|
|
841
850
|
|
|
842
|
-
|
|
851
|
+
It's possible to modify the `startupNodes` property in order to switch to another set of nodes here:
|
|
843
852
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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>
|
package/built/cluster/index.js
CHANGED
|
@@ -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 =
|
|
436
|
-
commands.
|
|
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;
|
package/built/commander.js
CHANGED
|
@@ -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
|
-
|
|
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.");
|
package/built/redis/index.js
CHANGED
|
@@ -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);
|