@jetit/publisher 5.6.3 → 6.0.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.
@@ -17,6 +17,9 @@ export declare class Streams {
17
17
  private subscriptions;
18
18
  private duplicateChecker;
19
19
  private circuitBreaker;
20
+ private readonly findHighestStreamIdScript;
21
+ private findHighestStreamIdScriptSHA;
22
+ private optimizationActiveUntil;
20
23
  private get redisPublisher();
21
24
  private get redisGroups();
22
25
  private DEFAULT_STREAMS_CONFIG;
@@ -35,6 +38,27 @@ export declare class Streams {
35
38
  * const streams = new Streams('POS');
36
39
  */
37
40
  constructor(serviceName: string, config?: Partial<IStreamsConfig>, redisConnectionId?: string);
41
+ /**
42
+ * Loads Lua scripts into Redis server cache
43
+ */
44
+ private loadScripts;
45
+ /**
46
+ * Executes a cached script, falling back to EVAL if needed
47
+ */
48
+ private executeScript;
49
+ /**
50
+ * Checks if optimization mode is active for a given event type.
51
+ * Cleans up expired entries during the check.
52
+ */
53
+ private isOptimizationActive;
54
+ /**
55
+ * Activates optimization mode for a given event type for a configured duration.
56
+ */
57
+ private activateOptimizationFor;
58
+ /**
59
+ * Deletes a specific message ID from multiple consumer group streams.
60
+ */
61
+ private deleteMessageFromStreams;
38
62
  private setupCircuitBreakerListeners;
39
63
  private runClear;
40
64
  /**
@@ -40,6 +40,75 @@ class Streams {
40
40
  this.redisConnectionId = redisConnectionId;
41
41
  this.eventsListened = [];
42
42
  this.subscriptions = new Map();
43
+ // Add these properties to store the script SHA and optimization state
44
+ this.findHighestStreamIdScript = `
45
+ -- KEYS[1]: event name
46
+ -- KEYS[2]: maximum streams to check
47
+ -- ARGV[1..n]: consumer group names
48
+
49
+ local eventName = KEYS[1]
50
+ local maxStreamsToCheck = tonumber(KEYS[2])
51
+ local highestTimestamp = 0
52
+ local highestSeq = 0
53
+
54
+ -- Calculate how many streams to check (all or max)
55
+ local streamsToCheck = math.min(#ARGV, maxStreamsToCheck)
56
+
57
+ -- If we have more streams than we want to check, use sampling
58
+ local streamIndices = {}
59
+ if streamsToCheck < #ARGV then
60
+ -- Deterministic sampling (every Nth stream)
61
+ local step = math.floor(#ARGV / streamsToCheck)
62
+ local offset = math.floor(step / 2) -- Start in the middle of each segment
63
+
64
+ for i=1, streamsToCheck do
65
+ local idx = offset + (i-1) * step
66
+ if idx <= #ARGV then
67
+ table.insert(streamIndices, idx)
68
+ end
69
+ end
70
+
71
+ -- Ensure we also check the last stream in case it's ahead
72
+ if #streamIndices == 0 or streamIndices[#streamIndices] ~= #ARGV then
73
+ if #ARGV > 0 then table.insert(streamIndices, #ARGV) end
74
+ end
75
+ else
76
+ -- Check all streams
77
+ for i=1, #ARGV do
78
+ streamIndices[i] = i
79
+ end
80
+ end
81
+
82
+ -- Check selected streams
83
+ for _, i in ipairs(streamIndices) do
84
+ local streamName = eventName .. ":" .. ARGV[i]
85
+ local latestEntries = redis.call('XREVRANGE', streamName, '+', '-', 'COUNT', 1)
86
+
87
+ if #latestEntries > 0 then
88
+ local id = latestEntries[1][1]
89
+ local dashPos = string.find(id, '-')
90
+
91
+ if dashPos then
92
+ local timestamp = tonumber(string.sub(id, 1, dashPos - 1))
93
+ local seq = tonumber(string.sub(id, dashPos + 1))
94
+
95
+ if timestamp > highestTimestamp then
96
+ highestTimestamp = timestamp
97
+ highestSeq = seq
98
+ elseif timestamp == highestTimestamp and seq > highestSeq then
99
+ highestSeq = seq
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ -- Add a safety buffer to the sequence number
106
+ highestSeq = highestSeq + 10
107
+
108
+ return { highestTimestamp, highestSeq }
109
+ `;
110
+ this.findHighestStreamIdScriptSHA = null;
111
+ this.optimizationActiveUntil = new Map(); // Tracks optimization state per event
43
112
  this.DEFAULT_STREAMS_CONFIG = {
44
113
  immediatePublishThreshold: 500,
45
114
  unprocessedMessageThreshold: 25,
@@ -56,6 +125,10 @@ class Streams {
56
125
  halfOpenStateMaxAttempts: 10,
57
126
  maxStoredEvents: 10000,
58
127
  },
128
+ // New defaults
129
+ optimizationDurationMs: 2 * 60 * 1000, // 2 minutes
130
+ optimizationThreshold: 20, // Enable optimization for >20 consumer groups
131
+ maxPendingTasks: 1000,
59
132
  };
60
133
  /** Initialise Config properties */
61
134
  this.config = { ...this.config, ...this.DEFAULT_STREAMS_CONFIG, ...config };
@@ -80,6 +153,120 @@ class Streams {
80
153
  this.circuitBreaker = new circuit_breaker_1.CircuitBreaker(this.config.circuitBreaker, this.redisPublisher);
81
154
  if (this.config.circuitBreaker.enabled)
82
155
  this.setupCircuitBreakerListeners();
156
+ // Load scripts when Redis is available
157
+ this.loadScripts().catch((error) => {
158
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Failed to load Redis scripts:', error);
159
+ });
160
+ }
161
+ /**
162
+ * Loads Lua scripts into Redis server cache
163
+ */
164
+ async loadScripts() {
165
+ try {
166
+ // Wait until Redis connection is established (simple check)
167
+ if (!this._redisPublisher) {
168
+ await new Promise((resolve) => setTimeout(resolve, 100)); // Wait briefly
169
+ if (!this._redisPublisher) {
170
+ logger_1.PUBLISHER_LOGGER.warn('PUBLISHER: Redis connection not ready for script loading.');
171
+ return;
172
+ }
173
+ }
174
+ const redis = this.redisPublisher;
175
+ // Load the script and store its SHA
176
+ this.findHighestStreamIdScriptSHA = (await redis.script('LOAD', this.findHighestStreamIdScript));
177
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Successfully loaded stream ID script with SHA: ${this.findHighestStreamIdScriptSHA}`);
178
+ }
179
+ catch (error) {
180
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error loading Redis scripts:', error);
181
+ // We'll try again later if needed via executeScript fallback
182
+ }
183
+ }
184
+ /**
185
+ * Executes a cached script, falling back to EVAL if needed
186
+ */
187
+ async executeScript(script, scriptSHA, numKeys, ...args) {
188
+ try {
189
+ // If we have the SHA, try EVALSHA first
190
+ if (scriptSHA) {
191
+ try {
192
+ // Ensure redisPublisher is available
193
+ if (!this._redisPublisher)
194
+ throw new Error('Redis publisher connection not available');
195
+ return await this.redisPublisher.evalsha(scriptSHA, numKeys, ...args);
196
+ }
197
+ catch (error) {
198
+ // Add type 'any' to error
199
+ // If the script isn't found on the server, fall back to EVAL
200
+ if (error?.message && typeof error.message === 'string' && error.message.includes('NOSCRIPT')) {
201
+ logger_1.PUBLISHER_LOGGER.warn('PUBLISHER: Script not found on Redis server, reloading...');
202
+ if (!this._redisPublisher)
203
+ throw new Error('Redis publisher connection not available for script reload');
204
+ const newSHA = (await this.redisPublisher.script('LOAD', script)); // Cast result to string
205
+ // Update the stored SHA
206
+ if (script === this.findHighestStreamIdScript) {
207
+ this.findHighestStreamIdScriptSHA = newSHA;
208
+ }
209
+ // Retry with EVALSHA
210
+ return await this.redisPublisher.evalsha(newSHA, numKeys, ...args);
211
+ }
212
+ throw error;
213
+ }
214
+ }
215
+ // If we don't have the SHA yet, use EVAL directly
216
+ if (!this._redisPublisher)
217
+ throw new Error('Redis publisher connection not available for EVAL');
218
+ return await this.redisPublisher.eval(script, numKeys, ...args);
219
+ }
220
+ catch (error) {
221
+ // Add type 'any' to error
222
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error executing Redis script:', error);
223
+ throw error; // Re-throw the original error
224
+ }
225
+ }
226
+ /**
227
+ * Checks if optimization mode is active for a given event type.
228
+ * Cleans up expired entries during the check.
229
+ */
230
+ isOptimizationActive(eventName) {
231
+ const expirationTime = this.optimizationActiveUntil.get(eventName);
232
+ if (!expirationTime)
233
+ return false;
234
+ if (Date.now() > expirationTime) {
235
+ // Clean up expired entry during publish operation
236
+ this.optimizationActiveUntil.delete(eventName);
237
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Deactivated optimization for ${eventName}`);
238
+ return false;
239
+ }
240
+ return true;
241
+ }
242
+ /**
243
+ * Activates optimization mode for a given event type for a configured duration.
244
+ */
245
+ activateOptimizationFor(eventName) {
246
+ const durationMs = this.config.optimizationDurationMs ?? this.DEFAULT_STREAMS_CONFIG.optimizationDurationMs;
247
+ const expirationTime = Date.now() + durationMs;
248
+ this.optimizationActiveUntil.set(eventName, expirationTime);
249
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Activated optimization for ${eventName} until ${new Date(expirationTime).toISOString()}`);
250
+ }
251
+ /**
252
+ * Deletes a specific message ID from multiple consumer group streams.
253
+ */
254
+ async deleteMessageFromStreams(eventName, consumerGroups, messageId) {
255
+ const deletionPromises = consumerGroups.map(async (consumerGroup) => {
256
+ const streamName = `${eventName}:${consumerGroup}`;
257
+ try {
258
+ if (!this._redisPublisher)
259
+ throw new Error('Redis publisher connection not available for XDEL');
260
+ await this.redisPublisher.xdel(streamName, messageId);
261
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Deleted message ${messageId} from ${streamName} for rollback`);
262
+ }
263
+ catch (error) {
264
+ // Log error but continue trying to delete from other streams
265
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error deleting message ${messageId} from ${streamName}:`, error);
266
+ }
267
+ });
268
+ // Wait for all deletions to attempt completion
269
+ await Promise.allSettled(deletionPromises);
83
270
  }
84
271
  setupCircuitBreakerListeners() {
85
272
  this.circuitBreaker.on('stateChange', async (newState) => {
@@ -141,25 +328,98 @@ class Streams {
141
328
  tracker.startRedisOperation();
142
329
  const consumerGroups = await (0, utils_1.getAllConsumerGroups)(data.eventName, this.redisPublisher);
143
330
  tracker.endRedisOperation();
144
- let key = '*';
331
+ let key = '*'; // Default to auto-generated ID
332
+ const publishedGroups = []; // Track successfully published groups for rollback
145
333
  if (consumerGroups.length > 0) {
146
334
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')}`);
147
- for (const consumerGroup of consumerGroups) {
148
- // Publish the event to each consumer group's stream
149
- const streamName = `${data.eventName}:${consumerGroup}`;
335
+ // Check if optimization is already active or if threshold is met
336
+ const optimizationThreshold = this.config.optimizationThreshold ?? this.DEFAULT_STREAMS_CONFIG.optimizationThreshold;
337
+ const useOptimization = this.isOptimizationActive(data.eventName) || consumerGroups.length > optimizationThreshold;
338
+ // If optimization is needed, generate the optimized ID upfront
339
+ if (useOptimization) {
340
+ const maxStreamsToCheck = Math.max(5, Math.ceil(consumerGroups.length * 0.25));
150
341
  tracker.startRedisOperation();
151
- const generatedKey = await this.redisPublisher.xadd(streamName, key, 'data', JSON.stringify(data));
152
- tracker.endRedisOperation();
153
- tracker.incrementEventCount();
154
- tracker.incrementMessageRate('publish', data.eventName);
155
- if (this.metricsCollector) {
156
- this.metricsCollector.addMetrics(tracker.getMetrics());
342
+ try {
343
+ const result = (await this.executeScript(this.findHighestStreamIdScript, this.findHighestStreamIdScriptSHA, 2, // Two keys
344
+ data.eventName, maxStreamsToCheck.toString(), ...consumerGroups));
345
+ const [highestTimestamp, highestSeq] = result;
346
+ const currentTime = Date.now();
347
+ if (currentTime > highestTimestamp) {
348
+ key = `${currentTime}-0`;
349
+ }
350
+ else {
351
+ key = `${highestTimestamp}-${highestSeq}`;
352
+ }
353
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Using optimized ID generation: ${key}`);
354
+ }
355
+ catch (scriptError) {
356
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error in optimized ID generation script, falling back to auto-generated ID:', scriptError);
357
+ key = '*';
358
+ }
359
+ finally {
360
+ tracker.endRedisOperation();
157
361
  }
158
- if (key === '*')
159
- key = generatedKey ?? key;
160
362
  }
161
- await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key, multicast);
162
- await this.circuitBreaker.recordSuccess();
363
+ // Publish to each consumer group
364
+ try {
365
+ for (const consumerGroup of consumerGroups) {
366
+ const streamName = `${data.eventName}:${consumerGroup}`;
367
+ tracker.startRedisOperation();
368
+ try {
369
+ const generatedKey = await this.redisPublisher.xadd(streamName, key, 'data', JSON.stringify(data));
370
+ tracker.endRedisOperation();
371
+ // If using auto-generated ID, capture it for the first stream
372
+ if (key === '*')
373
+ key = generatedKey ?? key;
374
+ // Track successful publish
375
+ publishedGroups.push(consumerGroup);
376
+ }
377
+ catch (error) {
378
+ tracker.endRedisOperation(); // Ensure tracker ends even on error
379
+ // Check if the error is the ID conflict error
380
+ if ((typeof error === 'string' && error.includes('equal or smaller than the target stream top item')) ||
381
+ (error?.message &&
382
+ typeof error.message === 'string' &&
383
+ error.message.includes('equal or smaller than the target stream top item'))) {
384
+ logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: ID conflict detected for ${streamName} (key: ${key}), switching to optimized mode for ${data.eventName} for duration: ${this.config.optimizationDurationMs ?? this.DEFAULT_STREAMS_CONFIG.optimizationDurationMs / 1000}. Consumers: ${consumerGroups.length}`);
385
+ // Delete already published messages if any (using the captured key)
386
+ if (publishedGroups.length > 0) {
387
+ await this.deleteMessageFromStreams(data.eventName, publishedGroups, key);
388
+ publishedGroups.length = 0;
389
+ }
390
+ // Activate optimization for this event type
391
+ this.activateOptimizationFor(data.eventName);
392
+ // Recursively call publish with same data (optimization will be active now)
393
+ // Important: Return the result of the recursive call
394
+ return this.publish(data, multicast);
395
+ }
396
+ else {
397
+ // For other XADD errors, rethrow to trigger the outer catch block
398
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Unhandled XADD error for ${streamName} (key: ${key}): ${error?.message}`);
399
+ throw error;
400
+ }
401
+ }
402
+ // Increment metrics only after successful publish
403
+ tracker.incrementEventCount();
404
+ tracker.incrementMessageRate('publish', data.eventName);
405
+ if (this.metricsCollector) {
406
+ this.metricsCollector.addMetrics(tracker.getMetrics());
407
+ }
408
+ } // End of for loop
409
+ // Notify subscribers only after all groups are successfully published (or retried)
410
+ await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key, multicast);
411
+ await this.circuitBreaker.recordSuccess();
412
+ }
413
+ catch (publishError) {
414
+ // If we failed during the loop (and it wasn't handled by the adaptive logic),
415
+ // try to clean up any messages already sent in this attempt.
416
+ if (publishedGroups.length > 0) {
417
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error during multi-group publish for ${data.eventName}, rolling back published messages for key ${key}, publishedMessage: ${JSON.stringify(data)}`);
418
+ await this.deleteMessageFromStreams(data.eventName, publishedGroups, key);
419
+ }
420
+ // Rethrow the error that caused the failure
421
+ throw publishError;
422
+ }
163
423
  }
164
424
  else {
165
425
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Event publish failed for event ${data.eventName}, reason: no consumers ${consumerGroups}`);
@@ -314,26 +574,19 @@ class Streams {
314
574
  if (!isNewSubscription) {
315
575
  return bs.asObservable().pipe((0, rxjs_1.skip)(1));
316
576
  }
317
- const cleanupInterval = 10000; // 10 seconds
318
- const timer = (0, rxjs_1.interval)(cleanupInterval).subscribe({
319
- next: async () => {
320
- try {
321
- await processMessage(this.redisGroups, '0', new tracker_1.MetricsTracker(), false);
322
- }
323
- catch (error) {
324
- logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Error in running recurring cleanup task:', error);
325
- }
326
- },
327
- error: (error) => {
328
- logger_1.PUBLISHER_LOGGER.error('PUBLISHER: Fatal error in cleanup timer:', error);
329
- },
330
- });
577
+ // 10-second cleanup timer disabled was re-enabled in v6.0.0 but causes
578
+ // double publishes and triggers unbounded pending retry spawning.
579
+ // Pending retries are driven by Pub/Sub notifications only.
580
+ const timer = { unsubscribe: () => { } };
331
581
  // Create observable with proper cleanup
332
582
  const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1), (0, rxjs_1.finalize)(() => {
333
583
  timer.unsubscribe();
334
584
  // Clean up subscription on completion
335
585
  this.removeSubscription(eventName, subscriptionId);
336
586
  }));
587
+ let activePendingTasks = 0;
588
+ let lastCapLogTime = 0;
589
+ const maxPendingTasks = this.config.maxPendingTasks ?? 1000;
337
590
  const processMessage = async (redisClient, messageId, tracker, multicast = false, processPending = false) => {
338
591
  // Skip processing if subscription was removed. This is needed because the processing is independent of the subscription
339
592
  if (!this.subscriptions.has(eventName)) {
@@ -348,8 +601,8 @@ class Streams {
348
601
  // 3. Pending message reprocessing
349
602
  if (!multicast && messageId !== '0' && !processPending) {
350
603
  try {
351
- const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1);
352
- if (pendingDetails[2] === 0) {
604
+ const pendingDetails = (await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1));
605
+ if (pendingDetails && pendingDetails.length > 0 && pendingDetails[0] && pendingDetails[0][3] === 0) {
353
606
  logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: Message ${messageId} already processed for ${streamName}`);
354
607
  return;
355
608
  }
@@ -372,12 +625,28 @@ class Streams {
372
625
  /**
373
626
  * Very very rare case of this occurring. Cases where the running service is super overloaded
374
627
  * that causes a messaged to be sent to processing with a delay, but eventually before being
375
- * processed gets picked up by another instance, leading to multiple publications
628
+ * processed gets picked up by another instance, leading to multiple publications. This makes
629
+ * sure that there is a check to ensure that the message belongs to this consumer before being
630
+ * processed
376
631
  */
377
632
  if (processPending) {
378
- const claimed = await redisClient.xclaim(streamName, this.consumerGroupName, this.instanceId, 10000, messageId, 'JUSTID');
379
- if (!claimed || claimed.length === 0) {
380
- return; // Message already claimed or acknowledged by another consumer, so don't repush to the subscriber
633
+ const pendingDetails = (await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1));
634
+ /** Message is not in PEL */
635
+ if (!pendingDetails || pendingDetails.length === 0)
636
+ return;
637
+ const [, instanceId, idleTime, deliveryCount] = pendingDetails[0][1];
638
+ /** Message is claimed by another consumer within the group, so do not process */
639
+ if (instanceId !== this.instanceId) {
640
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Process Pending message is claimed by another consumer : ${JSON.stringify({
641
+ instanceId,
642
+ idleTime,
643
+ deliveryCount,
644
+ messageId,
645
+ streamName,
646
+ originalInstanceId: this.instanceId,
647
+ consumerGroupName: this.consumerGroupName,
648
+ })}`);
649
+ return;
381
650
  }
382
651
  }
383
652
  const messages = await redisClient.xrange(streamName, messageId, messageId);
@@ -511,12 +780,47 @@ class Streams {
511
780
  unprocessedMessageIds.countOnThisConsumer > this.config.unprocessedMessageThreshold) {
512
781
  logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
513
782
  }
514
- // Process messages with rate limiting
515
- const processWithDelay = async (id, index) => {
516
- await new Promise((resolve) => setTimeout(resolve, index * getDelay(unprocessedMessageIds.countOnThisConsumer ?? 1)));
517
- await processMessage(redisClient, id, new tracker_1.MetricsTracker(), multicast, true);
518
- };
519
- unprocessedMessageIds.messageIds.map((id, index) => processWithDelay(id, index));
783
+ if (maxPendingTasks > 0) {
784
+ const available = maxPendingTasks - activePendingTasks;
785
+ if (available <= 0) {
786
+ const now = Date.now();
787
+ if (now - lastCapLogTime > 30000) {
788
+ logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: Pending task cap reached (${activePendingTasks}/${maxPendingTasks}), skipping batch for ${streamName}`);
789
+ lastCapLogTime = now;
790
+ }
791
+ }
792
+ else {
793
+ const batch = unprocessedMessageIds.messageIds.slice(0, available);
794
+ batch.forEach((id, index) => {
795
+ activePendingTasks++;
796
+ void (async () => {
797
+ try {
798
+ await new Promise((resolve) => setTimeout(resolve, index * getDelay(unprocessedMessageIds.countOnThisConsumer ?? 1)));
799
+ await processMessage(redisClient, id, new tracker_1.MetricsTracker(), multicast, true);
800
+ }
801
+ catch (err) {
802
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error in pending retry task for ${id}`, err);
803
+ }
804
+ finally {
805
+ activePendingTasks--;
806
+ }
807
+ })();
808
+ });
809
+ }
810
+ }
811
+ else {
812
+ unprocessedMessageIds.messageIds.forEach((id, index) => {
813
+ void (async () => {
814
+ try {
815
+ await new Promise((resolve) => setTimeout(resolve, index * getDelay(unprocessedMessageIds.countOnThisConsumer ?? 1)));
816
+ await processMessage(redisClient, id, new tracker_1.MetricsTracker(), multicast, true);
817
+ }
818
+ catch (err) {
819
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error in pending retry task for ${id}`, err);
820
+ }
821
+ })();
822
+ });
823
+ }
520
824
  }
521
825
  }
522
826
  catch (e) {
@@ -597,6 +901,9 @@ class Streams {
597
901
  */
598
902
  async close() {
599
903
  try {
904
+ // Reset optimization state and script SHA
905
+ this.optimizationActiveUntil.clear();
906
+ this.findHighestStreamIdScriptSHA = null;
600
907
  if (this.cleanUpTimer) {
601
908
  clearInterval(this.cleanUpTimer);
602
909
  }
@@ -54,6 +54,9 @@ export interface IStreamsConfig {
54
54
  halfOpenStateMaxAttempts: number;
55
55
  maxStoredEvents: number;
56
56
  };
57
+ optimizationDurationMs?: number;
58
+ optimizationThreshold?: number;
59
+ maxPendingTasks?: number;
57
60
  }
58
61
  export type TEventFilter<T> = (event: EventData<T, string>) => boolean;
59
62
  export interface ISubscription<T, TName extends string = string> {
@@ -12,7 +12,7 @@ exports.decodeScheduledMessage = decodeScheduledMessage;
12
12
  const logger_1 = require("./logger");
13
13
  async function getAllConsumerGroups(eventName, redisConnection) {
14
14
  const consumerGroups = await redisConnection.smembers(`${eventName}`);
15
- return consumerGroups;
15
+ return consumerGroups.sort();
16
16
  }
17
17
  function* getSummaryOnStreamConsumerGroup(redisClient, consumerGroupName, streamName) {
18
18
  const [count, , , consumers] = (yield redisClient.xpending(streamName, consumerGroupName));