ioredis 5.8.1 β†’ 5.9.0

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
@@ -21,7 +21,7 @@ ioredis is a stable project and maintenance is done on a best-effort basis for r
21
21
  ioredis is a robust, full-featured Redis client that is
22
22
  used in the world's biggest online commerce company [Alibaba](http://www.alibaba.com/) and many other awesome companies.
23
23
 
24
- 0. Full-featured. It supports [Cluster](http://redis.io/topics/cluster-tutorial), [Sentinel](https://redis.io/docs/reference/sentinel-clients), [Streams](https://redis.io/topics/streams-intro), [Pipelining](http://redis.io/topics/pipelining), and of course [Lua scripting](http://redis.io/commands/eval), [Redis Functions](https://redis.io/topics/functions-intro), [Pub/Sub](http://redis.io/topics/pubsub) (with the support of binary messages).
24
+ 0. Full-featured. It supports [Cluster](http://redis.io/topics/cluster-tutorial), [Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/), [Streams](https://redis.io/topics/streams-intro), [Pipelining](http://redis.io/topics/pipelining), and of course [Lua scripting](http://redis.io/commands/eval), [Redis Functions](https://redis.io/topics/functions-intro), [Pub/Sub](http://redis.io/topics/pubsub) (with the support of binary messages).
25
25
  1. High performance πŸš€.
26
26
  2. Delightful API πŸ˜„. It works with Node callbacks and Native promises.
27
27
  3. Transformation of command arguments and replies.
@@ -798,6 +798,20 @@ const redis = new Redis({
798
798
 
799
799
  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).
800
800
 
801
+ ### Blocking Command Timeout
802
+
803
+ ioredis can apply a client-side timeout to blocking commands (such as `blpop`, `brpop`, `bzpopmin`, `bzmpop`, `blmpop`, `xread`, `xreadgroup`, etc.). This protects against scenarios where the TCP connection becomes a zombie (e.g., due to a silent network failure like a Docker network disconnect) and Redis never replies.
804
+
805
+ For commands with a finite timeout (e.g., `blpop("key", 5)`), ioredis automatically sets a client-side deadline based on the command's timeout plus a small grace period. If no reply arrives before the deadline, the command resolves with `null`β€”the same value Redis returns when a blocking command times out normally.
806
+
807
+ For commands that intentionally block forever (e.g., `timeout = 0` or `BLOCK 0`), you can provide a safety net via the optional `blockingTimeout` option (milliseconds):
808
+
809
+ ```javascript
810
+ const redis = new Redis({
811
+ blockingTimeout: 30000, // Resolve with null after 30 seconds when timeout=0/BLOCK 0
812
+ });
813
+ ```
814
+
801
815
  ### Reconnect on Error
802
816
 
803
817
  Besides auto-reconnect when the connection is closed, ioredis supports reconnecting on certain Redis errors using the `reconnectOnError` option. Here's an example that will reconnect when receiving `READONLY` error:
@@ -1213,7 +1227,7 @@ cluster.on("smessage", (channel, message) => {
1213
1227
  console.log(message);
1214
1228
  });
1215
1229
 
1216
-
1230
+
1217
1231
  //Subscribe to the channels on the same slot
1218
1232
  cluster.ssubscribe("channel{my}:1", "channel{my}:2").then( ( count: number ) => {
1219
1233
  console.log(count);
@@ -30,6 +30,30 @@ export interface CommandNameFlags {
30
30
  ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe", "ssubscribe"];
31
31
  EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe", "sunsubscribe"];
32
32
  WILL_DISCONNECT: ["quit"];
33
+ HANDSHAKE_COMMANDS: ["auth", "select", "client", "readonly", "info"];
34
+ IGNORE_RECONNECT_ON_ERROR: ["client"];
35
+ BLOCKING_COMMANDS: [
36
+ "blpop",
37
+ "brpop",
38
+ "brpoplpush",
39
+ "blmove",
40
+ "bzpopmin",
41
+ "bzpopmax",
42
+ "bzmpop",
43
+ "blmpop",
44
+ "xread",
45
+ "xreadgroup"
46
+ ];
47
+ LAST_ARG_TIMEOUT_COMMANDS: [
48
+ "blpop",
49
+ "brpop",
50
+ "brpoplpush",
51
+ "blmove",
52
+ "bzpopmin",
53
+ "bzpopmax"
54
+ ];
55
+ FIRST_ARG_TIMEOUT_COMMANDS: ["bzmpop", "blmpop"];
56
+ BLOCK_OPTION_COMMANDS: ["xread", "xreadgroup"];
33
57
  }
34
58
  /**
35
59
  * Command instance
@@ -80,6 +104,8 @@ export default class Command implements Respondable {
80
104
  private callback;
81
105
  private transformed;
82
106
  private _commandTimeoutTimer?;
107
+ private _blockingTimeoutTimer?;
108
+ private _blockingDeadline?;
83
109
  private slot?;
84
110
  private keys?;
85
111
  /**
@@ -108,6 +134,24 @@ export default class Command implements Respondable {
108
134
  * and generating an error.
109
135
  */
110
136
  setTimeout(ms: number): void;
137
+ /**
138
+ * Set a timeout for blocking commands.
139
+ * When the timeout expires, the command resolves with null (matching Redis behavior).
140
+ * This handles the case of undetectable network failures (e.g., docker network disconnect)
141
+ * where the TCP connection becomes a zombie and no close event fires.
142
+ */
143
+ setBlockingTimeout(ms: number): void;
144
+ /**
145
+ * Extract the blocking timeout from the command arguments.
146
+ *
147
+ * @returns The timeout in seconds, null for indefinite blocking (timeout of 0),
148
+ * or undefined if this is not a blocking command
149
+ */
150
+ extractBlockingTimeout(): number | null | undefined;
151
+ /**
152
+ * Clear the command and blocking timers
153
+ */
154
+ private _clearTimers;
111
155
  private initPromise;
112
156
  /**
113
157
  * Iterate through the command arguments that are considered keys.
package/built/Command.js CHANGED
@@ -4,6 +4,7 @@ const commands_1 = require("@ioredis/commands");
4
4
  const calculateSlot = require("cluster-key-slot");
5
5
  const standard_as_callback_1 = require("standard-as-callback");
6
6
  const utils_1 = require("./utils");
7
+ const argumentParsers_1 = require("./utils/argumentParsers");
7
8
  /**
8
9
  * Command instance
9
10
  *
@@ -72,6 +73,7 @@ class Command {
72
73
  * Check whether the command has the flag
73
74
  */
74
75
  static checkFlag(flagName, commandName) {
76
+ commandName = commandName.toLowerCase();
75
77
  return !!this.getFlagMap()[flagName][commandName];
76
78
  }
77
79
  static setArgumentTransformer(name, func) {
@@ -194,6 +196,81 @@ class Command {
194
196
  }, ms);
195
197
  }
196
198
  }
199
+ /**
200
+ * Set a timeout for blocking commands.
201
+ * When the timeout expires, the command resolves with null (matching Redis behavior).
202
+ * This handles the case of undetectable network failures (e.g., docker network disconnect)
203
+ * where the TCP connection becomes a zombie and no close event fires.
204
+ */
205
+ setBlockingTimeout(ms) {
206
+ if (ms <= 0) {
207
+ return;
208
+ }
209
+ // Clear existing timer if any (can happen when command moves from offline to command queue)
210
+ if (this._blockingTimeoutTimer) {
211
+ clearTimeout(this._blockingTimeoutTimer);
212
+ this._blockingTimeoutTimer = undefined;
213
+ }
214
+ const now = Date.now();
215
+ // First call: establish absolute deadline
216
+ if (this._blockingDeadline === undefined) {
217
+ this._blockingDeadline = now + ms;
218
+ }
219
+ // Check if we've already exceeded the deadline
220
+ const remaining = this._blockingDeadline - now;
221
+ if (remaining <= 0) {
222
+ // Resolve with null to indicate timeout (same as Redis behavior)
223
+ this.resolve(null);
224
+ return;
225
+ }
226
+ this._blockingTimeoutTimer = setTimeout(() => {
227
+ if (this.isResolved) {
228
+ this._blockingTimeoutTimer = undefined;
229
+ return;
230
+ }
231
+ this._blockingTimeoutTimer = undefined;
232
+ // Timeout expired - resolve with null (same as Redis behavior when blocking command times out)
233
+ this.resolve(null);
234
+ }, remaining);
235
+ }
236
+ /**
237
+ * Extract the blocking timeout from the command arguments.
238
+ *
239
+ * @returns The timeout in seconds, null for indefinite blocking (timeout of 0),
240
+ * or undefined if this is not a blocking command
241
+ */
242
+ extractBlockingTimeout() {
243
+ const args = this.args;
244
+ if (!args || args.length === 0) {
245
+ return undefined;
246
+ }
247
+ const name = this.name.toLowerCase();
248
+ if (Command.checkFlag("LAST_ARG_TIMEOUT_COMMANDS", name)) {
249
+ return (0, argumentParsers_1.parseSecondsArgument)(args[args.length - 1]);
250
+ }
251
+ if (Command.checkFlag("FIRST_ARG_TIMEOUT_COMMANDS", name)) {
252
+ return (0, argumentParsers_1.parseSecondsArgument)(args[0]);
253
+ }
254
+ if (Command.checkFlag("BLOCK_OPTION_COMMANDS", name)) {
255
+ return (0, argumentParsers_1.parseBlockOption)(args);
256
+ }
257
+ return undefined;
258
+ }
259
+ /**
260
+ * Clear the command and blocking timers
261
+ */
262
+ _clearTimers() {
263
+ const existingTimer = this._commandTimeoutTimer;
264
+ if (existingTimer) {
265
+ clearTimeout(existingTimer);
266
+ delete this._commandTimeoutTimer;
267
+ }
268
+ const blockingTimer = this._blockingTimeoutTimer;
269
+ if (blockingTimer) {
270
+ clearTimeout(blockingTimer);
271
+ delete this._blockingTimeoutTimer;
272
+ }
273
+ }
197
274
  initPromise() {
198
275
  const promise = new Promise((resolve, reject) => {
199
276
  if (!this.transformed) {
@@ -205,14 +282,15 @@ class Command {
205
282
  this.stringifyArguments();
206
283
  }
207
284
  this.resolve = this._convertValue(resolve);
208
- if (this.errorStack) {
209
- this.reject = (err) => {
285
+ this.reject = (err) => {
286
+ this._clearTimers();
287
+ if (this.errorStack) {
210
288
  reject((0, utils_1.optimizeErrorStack)(err, this.errorStack.stack, __dirname));
211
- };
212
- }
213
- else {
214
- this.reject = reject;
215
- }
289
+ }
290
+ else {
291
+ reject(err);
292
+ }
293
+ };
216
294
  });
217
295
  this.promise = (0, standard_as_callback_1.default)(promise, this.callback);
218
296
  }
@@ -222,9 +300,11 @@ class Command {
222
300
  _iterateKeys(transform = (key) => key) {
223
301
  if (typeof this.keys === "undefined") {
224
302
  this.keys = [];
225
- if ((0, commands_1.exists)(this.name)) {
303
+ if ((0, commands_1.exists)(this.name, { caseInsensitive: true })) {
226
304
  // @ts-expect-error
227
- const keyIndexes = (0, commands_1.getKeyIndexes)(this.name, this.args);
305
+ const keyIndexes = (0, commands_1.getKeyIndexes)(this.name, this.args, {
306
+ nameCaseInsensitive: true,
307
+ });
228
308
  for (const index of keyIndexes) {
229
309
  this.args[index] = transform(this.args[index]);
230
310
  this.keys.push(this.args[index]);
@@ -239,11 +319,7 @@ class Command {
239
319
  _convertValue(resolve) {
240
320
  return (value) => {
241
321
  try {
242
- const existingTimer = this._commandTimeoutTimer;
243
- if (existingTimer) {
244
- clearTimeout(existingTimer);
245
- delete this._commandTimeoutTimer;
246
- }
322
+ this._clearTimers();
247
323
  resolve(this.transformReply(value));
248
324
  this.isResolved = true;
249
325
  }
@@ -270,6 +346,30 @@ Command.FLAGS = {
270
346
  ENTER_SUBSCRIBER_MODE: ["subscribe", "psubscribe", "ssubscribe"],
271
347
  EXIT_SUBSCRIBER_MODE: ["unsubscribe", "punsubscribe", "sunsubscribe"],
272
348
  WILL_DISCONNECT: ["quit"],
349
+ HANDSHAKE_COMMANDS: ["auth", "select", "client", "readonly", "info"],
350
+ IGNORE_RECONNECT_ON_ERROR: ["client"],
351
+ BLOCKING_COMMANDS: [
352
+ "blpop",
353
+ "brpop",
354
+ "brpoplpush",
355
+ "blmove",
356
+ "bzpopmin",
357
+ "bzpopmax",
358
+ "bzmpop",
359
+ "blmpop",
360
+ "xread",
361
+ "xreadgroup",
362
+ ],
363
+ LAST_ARG_TIMEOUT_COMMANDS: [
364
+ "blpop",
365
+ "brpop",
366
+ "brpoplpush",
367
+ "blmove",
368
+ "bzpopmin",
369
+ "bzpopmax",
370
+ ],
371
+ FIRST_ARG_TIMEOUT_COMMANDS: ["bzmpop", "blmpop"],
372
+ BLOCK_OPTION_COMMANDS: ["xread", "xreadgroup"],
273
373
  };
274
374
  Command._transformer = {
275
375
  argument: {},
package/built/Pipeline.js CHANGED
@@ -101,7 +101,8 @@ class Pipeline extends Commander_1.default {
101
101
  }
102
102
  }
103
103
  else if (!command.inTransaction) {
104
- const isReadOnly = (0, commands_1.exists)(command.name) && (0, commands_1.hasFlag)(command.name, "readonly");
104
+ const isReadOnly = (0, commands_1.exists)(command.name, { caseInsensitive: true }) &&
105
+ (0, commands_1.hasFlag)(command.name, "readonly", { nameCaseInsensitive: true });
105
106
  if (!isReadOnly) {
106
107
  retriable = false;
107
108
  break;
package/built/Redis.d.ts CHANGED
@@ -161,6 +161,8 @@ declare class Redis extends Commander implements DataHandledable {
161
161
  * @ignore
162
162
  */
163
163
  sendCommand(command: Command, stream?: WriteableStream): unknown;
164
+ private getBlockingTimeoutInMs;
165
+ private getConfiguredBlockingTimeout;
164
166
  private setSocketTimeout;
165
167
  scanStream(options?: ScanStreamOptions): ScanStream;
166
168
  scanBufferStream(options?: ScanStreamOptions): ScanStream;
package/built/Redis.js CHANGED
@@ -325,7 +325,7 @@ class Redis extends Commander_1.default {
325
325
  * @ignore
326
326
  */
327
327
  sendCommand(command, stream) {
328
- var _a, _b;
328
+ var _a, _b, _c;
329
329
  if (this.status === "wait") {
330
330
  this.connect().catch(lodash_1.noop);
331
331
  }
@@ -341,11 +341,13 @@ class Redis extends Commander_1.default {
341
341
  if (typeof this.options.commandTimeout === "number") {
342
342
  command.setTimeout(this.options.commandTimeout);
343
343
  }
344
+ const blockingTimeout = this.getBlockingTimeoutInMs(command);
344
345
  let writable = this.status === "ready" ||
345
346
  (!stream &&
346
347
  this.status === "connect" &&
347
- (0, commands_1.exists)(command.name) &&
348
- (0, commands_1.hasFlag)(command.name, "loading"));
348
+ (0, commands_1.exists)(command.name, { caseInsensitive: true }) &&
349
+ ((0, commands_1.hasFlag)(command.name, "loading", { nameCaseInsensitive: true }) ||
350
+ Command_1.default.checkFlag("HANDSHAKE_COMMANDS", command.name)));
349
351
  if (!this.stream) {
350
352
  writable = false;
351
353
  }
@@ -377,11 +379,20 @@ class Redis extends Commander_1.default {
377
379
  stream: stream,
378
380
  select: this.condition.select,
379
381
  });
382
+ // For blocking commands, set a timeout while queued to ensure they don't wait forever
383
+ // if connection never becomes ready (e.g., docker network disconnect scenario)
384
+ // Use blockingTimeout if configured, otherwise fall back to the command's own timeout
385
+ if (Command_1.default.checkFlag("BLOCKING_COMMANDS", command.name)) {
386
+ const offlineTimeout = (_b = this.getConfiguredBlockingTimeout()) !== null && _b !== void 0 ? _b : blockingTimeout;
387
+ if (offlineTimeout !== undefined) {
388
+ command.setBlockingTimeout(offlineTimeout);
389
+ }
390
+ }
380
391
  }
381
392
  else {
382
393
  // @ts-expect-error
383
394
  if (debug.enabled) {
384
- debug("write command[%s]: %d -> %s(%o)", this._getDescription(), (_b = this.condition) === null || _b === void 0 ? void 0 : _b.select, command.name, command.args);
395
+ debug("write command[%s]: %d -> %s(%o)", this._getDescription(), (_c = this.condition) === null || _c === void 0 ? void 0 : _c.select, command.name, command.args);
385
396
  }
386
397
  if (stream) {
387
398
  if ("isPipeline" in stream && stream.isPipeline) {
@@ -399,6 +410,9 @@ class Redis extends Commander_1.default {
399
410
  stream: stream,
400
411
  select: this.condition.select,
401
412
  });
413
+ if (blockingTimeout !== undefined) {
414
+ command.setBlockingTimeout(blockingTimeout);
415
+ }
402
416
  if (Command_1.default.checkFlag("WILL_DISCONNECT", command.name)) {
403
417
  this.manuallyClosing = true;
404
418
  }
@@ -416,6 +430,33 @@ class Redis extends Commander_1.default {
416
430
  }
417
431
  return command.promise;
418
432
  }
433
+ getBlockingTimeoutInMs(command) {
434
+ var _a;
435
+ if (!Command_1.default.checkFlag("BLOCKING_COMMANDS", command.name)) {
436
+ return undefined;
437
+ }
438
+ const timeout = command.extractBlockingTimeout();
439
+ if (typeof timeout === "number") {
440
+ if (timeout > 0) {
441
+ // Finite timeout from command args - add grace period
442
+ return timeout + ((_a = this.options.blockingTimeoutGrace) !== null && _a !== void 0 ? _a : RedisOptions_1.DEFAULT_REDIS_OPTIONS.blockingTimeoutGrace);
443
+ }
444
+ // Command has timeout=0 (block forever), use blockingTimeout option as safety net
445
+ return this.getConfiguredBlockingTimeout();
446
+ }
447
+ if (timeout === null) {
448
+ // No BLOCK option found (e.g., XREAD without BLOCK), use blockingTimeout as safety net
449
+ return this.getConfiguredBlockingTimeout();
450
+ }
451
+ return undefined;
452
+ }
453
+ getConfiguredBlockingTimeout() {
454
+ if (typeof this.options.blockingTimeout === "number" &&
455
+ this.options.blockingTimeout > 0) {
456
+ return this.options.blockingTimeout;
457
+ }
458
+ return undefined;
459
+ }
419
460
  setSocketTimeout() {
420
461
  this.socketTimeoutTimer = setTimeout(() => {
421
462
  this.stream.destroy(new Error(`Socket timeout. Expecting data, but didn't receive any in ${this.options.socketTimeout}ms.`));
@@ -501,7 +542,8 @@ class Redis extends Commander_1.default {
501
542
  handleReconnection(err, item) {
502
543
  var _a;
503
544
  let needReconnect = false;
504
- if (this.options.reconnectOnError) {
545
+ if (this.options.reconnectOnError &&
546
+ !Command_1.default.checkFlag("IGNORE_RECONNECT_ON_ERROR", item.command.name)) {
505
547
  needReconnect = this.options.reconnectOnError(err);
506
548
  }
507
549
  switch (needReconnect) {
@@ -19,6 +19,7 @@ exports.notAllowedAutoPipelineCommands = [
19
19
  "unsubscribe",
20
20
  "unpsubscribe",
21
21
  "select",
22
+ "client",
22
23
  ];
23
24
  function executeAutoPipeline(client, slotKey) {
24
25
  /*
@@ -1,37 +1,39 @@
1
1
  /// <reference types="node" />
2
- import ClusterSubscriber from "./ClusterSubscriber";
3
- import Cluster from "./index";
2
+ import * as EventEmitter from "events";
3
+ import ShardedSubscriber from "./ShardedSubscriber";
4
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.
5
+ * Redis distinguishes between "normal" and sharded PubSub. When using the normal PubSub feature,
6
+ * exactly one subscriber exists per cluster instance because the Redis cluster bus forwards
7
+ * messages between shards. Sharded PubSub removes this limitation by making each shard
8
+ * responsible for its own messages.
10
9
  *
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.
10
+ * This class coordinates one ShardedSubscriber per master node in the cluster, providing
11
+ * sharded PubSub support while keeping the public API backward compatible.
14
12
  */
15
13
  export default class ClusterSubscriberGroup {
16
- private cluster;
14
+ private readonly subscriberGroupEmitter;
17
15
  private shardedSubscribers;
18
16
  private clusterSlots;
19
17
  private subscriberToSlotsIndex;
20
18
  private channels;
19
+ private failedAttemptsByNode;
20
+ private isResetting;
21
+ private pendingReset;
22
+ private static readonly MAX_RETRY_ATTEMPTS;
23
+ private static readonly MAX_BACKOFF_MS;
24
+ private static readonly BASE_BACKOFF_MS;
21
25
  /**
22
26
  * Register callbacks
23
27
  *
24
28
  * @param cluster
25
29
  */
26
- constructor(cluster: Cluster, refreshSlotsCacheCallback: () => void);
30
+ constructor(subscriberGroupEmitter: EventEmitter);
27
31
  /**
28
32
  * Get the responsible subscriber.
29
33
  *
30
- * Returns null if no subscriber was found
31
- *
32
34
  * @param slot
33
35
  */
34
- getResponsibleSubscriber(slot: number): ClusterSubscriber;
36
+ getResponsibleSubscriber(slot: number): ShardedSubscriber | undefined;
35
37
  /**
36
38
  * Adds a channel for which this subscriber group is responsible
37
39
  *
@@ -50,24 +52,17 @@ export default class ClusterSubscriberGroup {
50
52
  /**
51
53
  * Start all not yet started subscribers
52
54
  */
53
- start(): void;
54
- /**
55
- * Add a subscriber to the group of subscribers
56
- *
57
- * @param redis
58
- */
59
- private _addSubscriber;
55
+ start(): Promise<any[]>;
60
56
  /**
61
- * Removes a subscriber from the group
62
- * @param redis
57
+ * Resets the subscriber group by disconnecting all subscribers that are no longer needed and connecting new ones.
63
58
  */
64
- private _removeSubscriber;
59
+ reset(clusterSlots: string[][], clusterNodes: any[]): Promise<void>;
65
60
  /**
66
61
  * Refreshes the subscriber-related slot ranges
67
62
  *
68
63
  * Returns false if no refresh was needed
69
64
  *
70
- * @param cluster
65
+ * @param targetSlots
71
66
  */
72
67
  private _refreshSlots;
73
68
  /**
@@ -83,4 +78,28 @@ export default class ClusterSubscriberGroup {
83
78
  * @private
84
79
  */
85
80
  private _slotsAreEqual;
81
+ /**
82
+ * Checks if any subscribers are in an unhealthy state.
83
+ *
84
+ * A subscriber is considered unhealthy if:
85
+ * - It exists but is not started (failed/disconnected)
86
+ * - It's missing entirely for a node that should have one
87
+ *
88
+ * @returns true if any subscribers need to be recreated
89
+ */
90
+ private hasUnhealthySubscribers;
91
+ /**
92
+ * Handles failed subscriber connections by emitting an event to refresh the slots cache
93
+ * after a backoff period.
94
+ *
95
+ * @param error
96
+ * @param nodeKey
97
+ */
98
+ private handleSubscriberConnectFailed;
99
+ /**
100
+ * Handles successful subscriber connections by resetting the failed attempts counter.
101
+ *
102
+ * @param nodeKey
103
+ */
104
+ private handleSubscriberConnectSucceeded;
86
105
  }