ioredis 5.5.0 → 5.6.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/README.md CHANGED
@@ -46,7 +46,7 @@ used in the world's biggest online commerce company [Alibaba](http://www.alibaba
46
46
  | Version | Branch | Node.js Version | Redis Version |
47
47
  | -------------- | ------ | --------------- | --------------- |
48
48
  | 5.x.x (latest) | main | >= 12 | 2.6.12 ~ latest |
49
- | 4.x.x | v4 | >= 6 | 2.6.12 ~ 7 |
49
+ | 4.x.x | v4 | >= 8 | 2.6.12 ~ 7 |
50
50
 
51
51
  Refer to [CHANGELOG.md](CHANGELOG.md) for features and bug fixes introduced in v5.
52
52
 
@@ -1196,6 +1196,38 @@ sub.subscribe("news", () => {
1196
1196
  });
1197
1197
  ```
1198
1198
 
1199
+ ### Sharded Pub/Sub
1200
+
1201
+ For sharded Pub/Sub, use the `spublish` and `ssubscribe` commands instead of the traditional `publish` and `subscribe`. With the old commands, the Redis cluster handles message propagation behind the scenes, allowing you to publish or subscribe to any node without considering sharding. However, this approach has scalability limitations that are addressed with sharded Pub/Sub. Here’s what you need to know:
1202
+
1203
+ 1. Instead of a single subscriber connection, there is now one subscriber connection per shard. Because of the potential overhead, you can enable or disable the use of the cluster subscriber group with the `shardedSubscribers` option. By default, this option is set to `false`, meaning sharded subscriptions are disabled. You should enable this option when establishing your cluster connection before using `ssubscribe`.
1204
+ 2. All channel names that you pass to a single `ssubscribe` need to map to the same hash slot. You can call `ssubscribe` multiple times on the same cluster client instance to subscribe to channels across slots. The cluster's subscriber group takes care of forwarding the `ssubscribe` command to the shard that is responsible for the channels.
1205
+
1206
+ The following basic example shows you how to use sharded Pub/Sub:
1207
+
1208
+ ```javascript
1209
+ const cluster: Cluster = new Cluster([{host: host, port: port}], {shardedSubscribers: true});
1210
+
1211
+ //Register the callback
1212
+ cluster.on("smessage", (channel, message) => {
1213
+ console.log(message);
1214
+ });
1215
+
1216
+
1217
+ //Subscribe to the channels on the same slot
1218
+ cluster.ssubscribe("channel{my}:1", "channel{my}:2").then( ( count: number ) => {
1219
+ console.log(count);
1220
+ }).catch( (err) => {
1221
+ console.log(err);
1222
+ });
1223
+
1224
+ //Publish a message
1225
+ cluster.spublish("channel{my}:1", "This is a test message to my first channel.").then((value: number) => {
1226
+ console.log("Published a message to channel{my}:1");
1227
+ });
1228
+ ```
1229
+
1230
+
1199
1231
  ### Events
1200
1232
 
1201
1233
  | Event | Description |
@@ -97,6 +97,15 @@ export interface ClusterOptions extends CommanderOptions {
97
97
  * @default 5000
98
98
  */
99
99
  slotsRefreshInterval?: number;
100
+ /**
101
+ * Use sharded subscribers instead of a single subscriber.
102
+ *
103
+ * If sharded subscribers are used, then one additional subscriber connection per master node
104
+ * is established. If you don't plan to use SPUBLISH/SSUBSCRIBE, then this should be disabled.
105
+ *
106
+ * @default false
107
+ */
108
+ shardedSubscribers?: boolean;
100
109
  /**
101
110
  * Passed to the constructor of `Redis`
102
111
  *
@@ -18,4 +18,5 @@ exports.DEFAULT_CLUSTER_OPTIONS = {
18
18
  dnsLookup: dns_1.lookup,
19
19
  enableAutoPipelining: false,
20
20
  autoPipeliningIgnoredCommands: [],
21
+ shardedSubscribers: false,
21
22
  };
@@ -4,13 +4,26 @@ import ConnectionPool from "./ConnectionPool";
4
4
  export default class ClusterSubscriber {
5
5
  private connectionPool;
6
6
  private emitter;
7
+ private isSharded;
7
8
  private started;
8
9
  private subscriber;
9
10
  private lastActiveSubscriber;
10
- constructor(connectionPool: ConnectionPool, emitter: EventEmitter);
11
+ private slotRange;
12
+ constructor(connectionPool: ConnectionPool, emitter: EventEmitter, isSharded?: boolean);
11
13
  getInstance(): any;
14
+ /**
15
+ * Associate this subscriber to a specific slot range.
16
+ *
17
+ * Returns the range or an empty array if the slot range couldn't be associated.
18
+ *
19
+ * BTW: This is more for debugging and testing purposes.
20
+ *
21
+ * @param range
22
+ */
23
+ associateSlotRange(range: number[]): number[];
12
24
  start(): void;
13
25
  stop(): void;
26
+ isStarted(): boolean;
14
27
  private onSubscriberEnd;
15
28
  private selectSubscriber;
16
29
  }
@@ -5,11 +5,15 @@ const utils_1 = require("../utils");
5
5
  const Redis_1 = require("../Redis");
6
6
  const debug = (0, utils_1.Debug)("cluster:subscriber");
7
7
  class ClusterSubscriber {
8
- constructor(connectionPool, emitter) {
8
+ constructor(connectionPool, emitter, isSharded = false) {
9
9
  this.connectionPool = connectionPool;
10
10
  this.emitter = emitter;
11
+ this.isSharded = isSharded;
11
12
  this.started = false;
13
+ //There is only one connection for the entire pool
12
14
  this.subscriber = null;
15
+ //The slot range for which this subscriber is responsible
16
+ this.slotRange = [];
13
17
  this.onSubscriberEnd = () => {
14
18
  if (!this.started) {
15
19
  debug("subscriber has disconnected, but ClusterSubscriber is not started, so not reconnecting.");
@@ -49,6 +53,21 @@ class ClusterSubscriber {
49
53
  getInstance() {
50
54
  return this.subscriber;
51
55
  }
56
+ /**
57
+ * Associate this subscriber to a specific slot range.
58
+ *
59
+ * Returns the range or an empty array if the slot range couldn't be associated.
60
+ *
61
+ * BTW: This is more for debugging and testing purposes.
62
+ *
63
+ * @param range
64
+ */
65
+ associateSlotRange(range) {
66
+ if (this.isSharded) {
67
+ this.slotRange = range;
68
+ }
69
+ return this.slotRange;
70
+ }
52
71
  start() {
53
72
  this.started = true;
54
73
  this.selectSubscriber();
@@ -60,7 +79,9 @@ class ClusterSubscriber {
60
79
  this.subscriber.disconnect();
61
80
  this.subscriber = null;
62
81
  }
63
- debug("stopped");
82
+ }
83
+ isStarted() {
84
+ return this.started;
64
85
  }
65
86
  selectSubscriber() {
66
87
  const lastActiveSubscriber = this.lastActiveSubscriber;
@@ -91,13 +112,16 @@ class ClusterSubscriber {
91
112
  * provided for the subscriber is correct, and if not, the current subscriber
92
113
  * will be disconnected and a new subscriber will be selected.
93
114
  */
115
+ let connectionPrefix = "subscriber";
116
+ if (this.isSharded)
117
+ connectionPrefix = "ssubscriber";
94
118
  this.subscriber = new Redis_1.default({
95
119
  port: options.port,
96
120
  host: options.host,
97
121
  username: options.username,
98
122
  password: options.password,
99
123
  enableReadyCheck: true,
100
- connectionName: (0, util_1.getConnectionName)("subscriber", options.connectionName),
124
+ connectionName: (0, util_1.getConnectionName)(connectionPrefix, options.connectionName),
101
125
  lazyConnect: true,
102
126
  tls: options.tls,
103
127
  // Don't try to reconnect the subscriber connection. If the connection fails
@@ -153,8 +177,6 @@ class ClusterSubscriber {
153
177
  for (const event of [
154
178
  "message",
155
179
  "messageBuffer",
156
- "smessage",
157
- "smessageBuffer",
158
180
  ]) {
159
181
  this.subscriber.on(event, (arg1, arg2) => {
160
182
  this.emitter.emit(event, arg1, arg2);
@@ -165,6 +187,16 @@ class ClusterSubscriber {
165
187
  this.emitter.emit(event, arg1, arg2, arg3);
166
188
  });
167
189
  }
190
+ if (this.isSharded == true) {
191
+ for (const event of [
192
+ "smessage",
193
+ "smessageBuffer",
194
+ ]) {
195
+ this.subscriber.on(event, (arg1, arg2) => {
196
+ this.emitter.emit(event, arg1, arg2);
197
+ });
198
+ }
199
+ }
168
200
  }
169
201
  }
170
202
  exports.default = ClusterSubscriber;
@@ -0,0 +1,86 @@
1
+ /// <reference types="node" />
2
+ import ClusterSubscriber from "./ClusterSubscriber";
3
+ import Cluster from "./index";
4
+ /**
5
+ * Redis differs between "normal" and sharded PubSub. If using the "normal" PubSub feature, exactly one
6
+ * ClusterSubscriber exists per cluster instance. This works because the Redis cluster bus forwards m
7
+ * messages between shards. However, this has scalability limitations, which is the reason why the sharded
8
+ * PubSub feature was added to Redis. With sharded PubSub, each shard is responsible for its own messages.
9
+ * Given that, we need at least one ClusterSubscriber per master endpoint/node.
10
+ *
11
+ * This class leverages the previously exising ClusterSubscriber by adding support for multiple such subscribers
12
+ * in alignment to the master nodes of the cluster. The ClusterSubscriber class was extended in a non-breaking way
13
+ * to support this feature.
14
+ */
15
+ export default class ClusterSubscriberGroup {
16
+ private cluster;
17
+ private shardedSubscribers;
18
+ private clusterSlots;
19
+ private subscriberToSlotsIndex;
20
+ private channels;
21
+ /**
22
+ * Register callbacks
23
+ *
24
+ * @param cluster
25
+ */
26
+ constructor(cluster: Cluster);
27
+ /**
28
+ * Get the responsible subscriber.
29
+ *
30
+ * Returns null if no subscriber was found
31
+ *
32
+ * @param slot
33
+ */
34
+ getResponsibleSubscriber(slot: number): ClusterSubscriber;
35
+ /**
36
+ * Adds a channel for which this subscriber group is responsible
37
+ *
38
+ * @param channels
39
+ */
40
+ addChannels(channels: (string | Buffer)[]): number;
41
+ /**
42
+ * Removes channels for which the subscriber group is responsible by optionally unsubscribing
43
+ * @param channels
44
+ */
45
+ removeChannels(channels: (string | Buffer)[]): number;
46
+ /**
47
+ * Disconnect all subscribers
48
+ */
49
+ stop(): void;
50
+ /**
51
+ * Start all not yet started subscribers
52
+ */
53
+ start(): void;
54
+ /**
55
+ * Add a subscriber to the group of subscribers
56
+ *
57
+ * @param redis
58
+ */
59
+ private _addSubscriber;
60
+ /**
61
+ * Removes a subscriber from the group
62
+ * @param redis
63
+ */
64
+ private _removeSubscriber;
65
+ /**
66
+ * Refreshes the subscriber-related slot ranges
67
+ *
68
+ * Returns false if no refresh was needed
69
+ *
70
+ * @param cluster
71
+ */
72
+ private _refreshSlots;
73
+ /**
74
+ * Resubscribes to the previous channels
75
+ *
76
+ * @private
77
+ */
78
+ private _resubscribe;
79
+ /**
80
+ * Deep equality of the cluster slots objects
81
+ *
82
+ * @param other
83
+ * @private
84
+ */
85
+ private _slotsAreEqual;
86
+ }
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const utils_1 = require("../utils");
4
+ const ClusterSubscriber_1 = require("./ClusterSubscriber");
5
+ const ConnectionPool_1 = require("./ConnectionPool");
6
+ const util_1 = require("./util");
7
+ const calculateSlot = require("cluster-key-slot");
8
+ const debug = (0, utils_1.Debug)("cluster:subscriberGroup");
9
+ /**
10
+ * Redis differs between "normal" and sharded PubSub. If using the "normal" PubSub feature, exactly one
11
+ * ClusterSubscriber exists per cluster instance. This works because the Redis cluster bus forwards m
12
+ * messages between shards. However, this has scalability limitations, which is the reason why the sharded
13
+ * PubSub feature was added to Redis. With sharded PubSub, each shard is responsible for its own messages.
14
+ * Given that, we need at least one ClusterSubscriber per master endpoint/node.
15
+ *
16
+ * This class leverages the previously exising ClusterSubscriber by adding support for multiple such subscribers
17
+ * in alignment to the master nodes of the cluster. The ClusterSubscriber class was extended in a non-breaking way
18
+ * to support this feature.
19
+ */
20
+ class ClusterSubscriberGroup {
21
+ /**
22
+ * Register callbacks
23
+ *
24
+ * @param cluster
25
+ */
26
+ constructor(cluster) {
27
+ this.cluster = cluster;
28
+ this.shardedSubscribers = new Map();
29
+ this.clusterSlots = [];
30
+ //Simple [min, max] slot ranges aren't enough because you can migrate single slots
31
+ this.subscriberToSlotsIndex = new Map();
32
+ this.channels = new Map();
33
+ cluster.on("+node", (redis) => {
34
+ this._addSubscriber(redis);
35
+ });
36
+ cluster.on("-node", (redis) => {
37
+ this._removeSubscriber(redis);
38
+ });
39
+ cluster.on("refresh", () => {
40
+ this._refreshSlots(cluster);
41
+ });
42
+ }
43
+ /**
44
+ * Get the responsible subscriber.
45
+ *
46
+ * Returns null if no subscriber was found
47
+ *
48
+ * @param slot
49
+ */
50
+ getResponsibleSubscriber(slot) {
51
+ const nodeKey = this.clusterSlots[slot][0];
52
+ return this.shardedSubscribers.get(nodeKey);
53
+ }
54
+ /**
55
+ * Adds a channel for which this subscriber group is responsible
56
+ *
57
+ * @param channels
58
+ */
59
+ addChannels(channels) {
60
+ const slot = calculateSlot(channels[0]);
61
+ //Check if the all channels belong to the same slot and otherwise reject the operation
62
+ channels.forEach((c) => {
63
+ if (calculateSlot(c) != slot)
64
+ return -1;
65
+ });
66
+ const currChannels = this.channels.get(slot);
67
+ if (!currChannels) {
68
+ this.channels.set(slot, channels);
69
+ }
70
+ else {
71
+ this.channels.set(slot, currChannels.concat(channels));
72
+ }
73
+ return [...this.channels.values()].flatMap(v => v).length;
74
+ }
75
+ /**
76
+ * Removes channels for which the subscriber group is responsible by optionally unsubscribing
77
+ * @param channels
78
+ */
79
+ removeChannels(channels) {
80
+ const slot = calculateSlot(channels[0]);
81
+ //Check if the all channels belong to the same slot and otherwise reject the operation
82
+ channels.forEach((c) => {
83
+ if (calculateSlot(c) != slot)
84
+ return -1;
85
+ });
86
+ const slotChannels = this.channels.get(slot);
87
+ if (slotChannels) {
88
+ const updatedChannels = slotChannels.filter(c => !channels.includes(c));
89
+ this.channels.set(slot, updatedChannels);
90
+ }
91
+ return [...this.channels.values()].flatMap(v => v).length;
92
+ }
93
+ /**
94
+ * Disconnect all subscribers
95
+ */
96
+ stop() {
97
+ for (const s of this.shardedSubscribers.values()) {
98
+ s.stop();
99
+ }
100
+ }
101
+ /**
102
+ * Start all not yet started subscribers
103
+ */
104
+ start() {
105
+ for (const s of this.shardedSubscribers.values()) {
106
+ if (!s.isStarted()) {
107
+ s.start();
108
+ }
109
+ }
110
+ }
111
+ /**
112
+ * Add a subscriber to the group of subscribers
113
+ *
114
+ * @param redis
115
+ */
116
+ _addSubscriber(redis) {
117
+ const pool = new ConnectionPool_1.default(redis.options);
118
+ if (pool.addMasterNode(redis)) {
119
+ const sub = new ClusterSubscriber_1.default(pool, this.cluster, true);
120
+ const nodeKey = (0, util_1.getNodeKey)(redis.options);
121
+ this.shardedSubscribers.set(nodeKey, sub);
122
+ sub.start();
123
+ // We need to attempt to resubscribe them in case the new node serves their slot
124
+ this._resubscribe();
125
+ this.cluster.emit("+subscriber");
126
+ return sub;
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * Removes a subscriber from the group
132
+ * @param redis
133
+ */
134
+ _removeSubscriber(redis) {
135
+ const nodeKey = (0, util_1.getNodeKey)(redis.options);
136
+ const sub = this.shardedSubscribers.get(nodeKey);
137
+ if (sub) {
138
+ sub.stop();
139
+ this.shardedSubscribers.delete(nodeKey);
140
+ // Even though the subscriber to this node is going down, we might have another subscriber
141
+ // handling the same slots, so we need to attempt to subscribe the orphaned channels
142
+ this._resubscribe();
143
+ this.cluster.emit("-subscriber");
144
+ }
145
+ return this.shardedSubscribers;
146
+ }
147
+ /**
148
+ * Refreshes the subscriber-related slot ranges
149
+ *
150
+ * Returns false if no refresh was needed
151
+ *
152
+ * @param cluster
153
+ */
154
+ _refreshSlots(cluster) {
155
+ //If there was an actual change, then reassign the slot ranges
156
+ if (this._slotsAreEqual(cluster.slots)) {
157
+ debug("Nothing to refresh because the new cluster map is equal to the previous one.");
158
+ }
159
+ else {
160
+ debug("Refreshing the slots of the subscriber group.");
161
+ //Rebuild the slots index
162
+ this.subscriberToSlotsIndex = new Map();
163
+ for (let slot = 0; slot < cluster.slots.length; slot++) {
164
+ const node = cluster.slots[slot][0];
165
+ if (!this.subscriberToSlotsIndex.has(node)) {
166
+ this.subscriberToSlotsIndex.set(node, []);
167
+ }
168
+ this.subscriberToSlotsIndex.get(node).push(Number(slot));
169
+ }
170
+ //Update the subscribers from the index
171
+ this._resubscribe();
172
+ //Update the cached slots map
173
+ this.clusterSlots = JSON.parse(JSON.stringify(cluster.slots));
174
+ this.cluster.emit("subscribersReady");
175
+ return true;
176
+ }
177
+ return false;
178
+ }
179
+ /**
180
+ * Resubscribes to the previous channels
181
+ *
182
+ * @private
183
+ */
184
+ _resubscribe() {
185
+ if (this.shardedSubscribers) {
186
+ this.shardedSubscribers.forEach((s, nodeKey) => {
187
+ const subscriberSlots = this.subscriberToSlotsIndex.get(nodeKey);
188
+ if (subscriberSlots) {
189
+ //More for debugging purposes
190
+ s.associateSlotRange(subscriberSlots);
191
+ //Resubscribe on the underlying connection
192
+ subscriberSlots.forEach((ss) => {
193
+ //Might return null if being disconnected
194
+ const redis = s.getInstance();
195
+ const channels = this.channels.get(ss);
196
+ if (channels && channels.length > 0) {
197
+ //Try to subscribe now
198
+ if (redis) {
199
+ redis.ssubscribe(channels);
200
+ //If the instance isn't ready yet, then register the re-subscription for later
201
+ redis.on("ready", () => {
202
+ redis.ssubscribe(channels);
203
+ });
204
+ }
205
+ }
206
+ });
207
+ }
208
+ });
209
+ }
210
+ }
211
+ /**
212
+ * Deep equality of the cluster slots objects
213
+ *
214
+ * @param other
215
+ * @private
216
+ */
217
+ _slotsAreEqual(other) {
218
+ if (this.clusterSlots === undefined)
219
+ return false;
220
+ else
221
+ return JSON.stringify(this.clusterSlots) === JSON.stringify(other);
222
+ }
223
+ }
224
+ exports.default = ClusterSubscriberGroup;
@@ -10,6 +10,17 @@ export default class ConnectionPool extends EventEmitter {
10
10
  getNodes(role?: NodeRole): Redis[];
11
11
  getInstanceByKey(key: NodeKey): Redis;
12
12
  getSampleInstance(role: NodeRole): Redis;
13
+ /**
14
+ * Add a master node to the pool
15
+ * @param node
16
+ */
17
+ addMasterNode(node: RedisOptions): boolean;
18
+ /**
19
+ * Creates a Redis connection instance from the node options
20
+ * @param node
21
+ * @param readOnly
22
+ */
23
+ createRedisFromOptions(node: RedisOptions, readOnly: boolean): Redis;
13
24
  /**
14
25
  * Find or create a connection to the node
15
26
  */
@@ -29,6 +29,40 @@ class ConnectionPool extends events_1.EventEmitter {
29
29
  const sampleKey = (0, utils_1.sample)(keys);
30
30
  return this.nodes[role][sampleKey];
31
31
  }
32
+ /**
33
+ * Add a master node to the pool
34
+ * @param node
35
+ */
36
+ addMasterNode(node) {
37
+ const key = (0, util_1.getNodeKey)(node.options);
38
+ const redis = this.createRedisFromOptions(node, node.options.readOnly);
39
+ //Master nodes aren't read-only
40
+ if (!node.options.readOnly) {
41
+ this.nodes.all[key] = redis;
42
+ this.nodes.master[key] = redis;
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Creates a Redis connection instance from the node options
49
+ * @param node
50
+ * @param readOnly
51
+ */
52
+ createRedisFromOptions(node, readOnly) {
53
+ const redis = new Redis_1.default((0, utils_1.defaults)({
54
+ // Never try to reconnect when a node is lose,
55
+ // instead, waiting for a `MOVED` error and
56
+ // fetch the slots again.
57
+ retryStrategy: null,
58
+ // Offline queue should be enabled so that
59
+ // we don't need to wait for the `ready` event
60
+ // before sending commands to the node.
61
+ enableOfflineQueue: true,
62
+ readOnly: readOnly,
63
+ }, node, this.redisOptions, { lazyConnect: true }));
64
+ return redis;
65
+ }
32
66
  /**
33
67
  * Find or create a connection to the node
34
68
  */
@@ -60,17 +94,7 @@ class ConnectionPool extends events_1.EventEmitter {
60
94
  }
61
95
  else {
62
96
  debug("Connecting to %s as %s", key, readOnly ? "slave" : "master");
63
- redis = new Redis_1.default((0, utils_1.defaults)({
64
- // Never try to reconnect when a node is lose,
65
- // instead, waiting for a `MOVED` error and
66
- // fetch the slots again.
67
- retryStrategy: null,
68
- // Offline queue should be enabled so that
69
- // we don't need to wait for the `ready` event
70
- // before sending commands to the node.
71
- enableOfflineQueue: true,
72
- readOnly: readOnly,
73
- }, node, this.redisOptions, { lazyConnect: true }));
97
+ redis = this.createRedisFromOptions(node, readOnly);
74
98
  this.nodes.all[key] = redis;
75
99
  this.nodes[readOnly ? "slave" : "master"][key] = redis;
76
100
  redis.once("end", () => {
@@ -41,6 +41,7 @@ declare class Cluster extends Commander {
41
41
  private delayQueue;
42
42
  private offlineQueue;
43
43
  private subscriber;
44
+ private shardedSubscribers;
44
45
  private slotsTimer;
45
46
  private reconnectTimeout;
46
47
  private isRefreshing;
@@ -18,6 +18,7 @@ const ConnectionPool_1 = require("./ConnectionPool");
18
18
  const DelayQueue_1 = require("./DelayQueue");
19
19
  const util_1 = require("./util");
20
20
  const Deque = require("denque");
21
+ const ClusterSubscriberGroup_1 = require("./ClusterSubscriberGroup");
21
22
  const debug = (0, utils_1.Debug)("cluster");
22
23
  const REJECT_OVERWRITTEN_COMMANDS = new WeakSet();
23
24
  /**
@@ -27,6 +28,7 @@ class Cluster extends Commander_1.default {
27
28
  /**
28
29
  * Creates an instance of Cluster.
29
30
  */
31
+ //TODO: Add an option that enables or disables sharded PubSub
30
32
  constructor(startupNodes, options = {}) {
31
33
  super();
32
34
  this.slots = [];
@@ -60,6 +62,8 @@ class Cluster extends Commander_1.default {
60
62
  events_1.EventEmitter.call(this);
61
63
  this.startupNodes = startupNodes;
62
64
  this.options = (0, utils_1.defaults)({}, options, ClusterOptions_1.DEFAULT_CLUSTER_OPTIONS, this.options);
65
+ if (this.options.shardedSubscribers == true)
66
+ this.shardedSubscribers = new ClusterSubscriberGroup_1.default(this);
63
67
  if (this.options.redisOptions &&
64
68
  this.options.redisOptions.keyPrefix &&
65
69
  !this.options.keyPrefix) {
@@ -172,6 +176,9 @@ class Cluster extends Commander_1.default {
172
176
  }
173
177
  });
174
178
  this.subscriber.start();
179
+ if (this.options.shardedSubscribers) {
180
+ this.shardedSubscribers.start();
181
+ }
175
182
  })
176
183
  .catch((err) => {
177
184
  this.setStatus("close");
@@ -197,6 +204,9 @@ class Cluster extends Commander_1.default {
197
204
  }
198
205
  this.clearNodesRefreshInterval();
199
206
  this.subscriber.stop();
207
+ if (this.options.shardedSubscribers) {
208
+ this.shardedSubscribers.stop();
209
+ }
200
210
  if (status === "wait") {
201
211
  this.setStatus("close");
202
212
  this.handleCloseEvent();
@@ -218,6 +228,9 @@ class Cluster extends Commander_1.default {
218
228
  }
219
229
  this.clearNodesRefreshInterval();
220
230
  this.subscriber.stop();
231
+ if (this.options.shardedSubscribers) {
232
+ this.shardedSubscribers.stop();
233
+ }
221
234
  if (status === "wait") {
222
235
  const ret = (0, standard_as_callback_1.default)(Promise.resolve("OK"), callback);
223
236
  // use setImmediate to make sure "close" event
@@ -409,7 +422,24 @@ class Cluster extends Commander_1.default {
409
422
  }
410
423
  else if (Command_1.default.checkFlag("ENTER_SUBSCRIBER_MODE", command.name) ||
411
424
  Command_1.default.checkFlag("EXIT_SUBSCRIBER_MODE", command.name)) {
412
- redis = _this.subscriber.getInstance();
425
+ if (_this.options.shardedSubscribers == true &&
426
+ (command.name == "ssubscribe" || command.name == "sunsubscribe")) {
427
+ const sub = _this.shardedSubscribers.getResponsibleSubscriber(targetSlot);
428
+ let status = -1;
429
+ if (command.name == "ssubscribe")
430
+ status = _this.shardedSubscribers.addChannels(command.getKeys());
431
+ if (command.name == "sunsubscribe")
432
+ status = _this.shardedSubscribers.removeChannels(command.getKeys());
433
+ if (status !== -1) {
434
+ redis = sub.getInstance();
435
+ }
436
+ else {
437
+ command.reject(new redis_errors_1.AbortError("Can't add or remove the given channels. Are they in the same slot?"));
438
+ }
439
+ }
440
+ else {
441
+ redis = _this.subscriber.getInstance();
442
+ }
413
443
  if (!redis) {
414
444
  command.reject(new redis_errors_1.AbortError("No subscriber for the cluster"));
415
445
  return;
@@ -665,6 +695,7 @@ class Cluster extends Commander_1.default {
665
695
  duplicatedConnection.cluster("SLOTS", (0, utils_1.timeout)((err, result) => {
666
696
  duplicatedConnection.disconnect();
667
697
  if (err) {
698
+ debug("error encountered running CLUSTER.SLOTS: %s", err);
668
699
  return callback(err);
669
700
  }
670
701
  if (this.status === "disconnecting" ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ioredis",
3
- "version": "5.5.0",
3
+ "version": "5.6.1",
4
4
  "description": "A robust, performance-focused and full-featured Redis client for Node.js.",
5
5
  "main": "./built/index.js",
6
6
  "types": "./built/index.d.ts",