@jetit/publisher 5.0.0 → 5.2.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
@@ -116,11 +116,45 @@ await publisher.publish(eventData);
116
116
  ### Subscribing to Events
117
117
 
118
118
  ```typescript
119
+ // Basic subscription with automatic acknowledgment
119
120
  publisher.listen('user-registered').subscribe(event => {
120
121
  console.log('New user registered:', event.data);
121
122
  });
123
+
124
+ // Subscription with external acknowledgment
125
+ const options = {
126
+ externalAcknowledgement: true
127
+ };
128
+
129
+ publisher.listen('user-registered', options).subscribe(async event => {
130
+ try {
131
+ console.log('New user registered:', event.data);
132
+ // Process the event
133
+ await processUserRegistration(event.data);
134
+
135
+ // Manually acknowledge the message after successful processing
136
+ await publisher.acknowledgeMessage(event.ackKey);
137
+ } catch (error) {
138
+ // Handle error - message will not be acknowledged and will be reprocessed
139
+ console.error('Failed to process user registration:', error);
140
+ }
141
+ });
122
142
  ```
123
143
 
144
+ The `externalAcknowledgement` option allows you to manually control when messages are acknowledged. This is useful when:
145
+ - You need to ensure message processing is complete before acknowledgment
146
+ - You want to implement custom retry logic
147
+ - You need to coordinate acknowledgment with other operations
148
+ - You want to implement transaction-like behavior
149
+
150
+ When `externalAcknowledgement` is set to `true`:
151
+ 1. Messages won't be automatically acknowledged after delivery
152
+ 2. Each message contains an `ackKey` that must be used to acknowledge it
153
+ 3. Unacknowledged messages will be redelivered to other consumers
154
+ 4. You must explicitly call `acknowledgeMessage(event.ackKey)` after successful processing
155
+
156
+ **Note:** Be careful with external acknowledgment as failing to acknowledge messages can lead to message redelivery and potential duplicate processing.
157
+
124
158
  ### Scheduled Publishing
125
159
 
126
160
  ```typescript
@@ -278,4 +312,4 @@ If you encounter issues:
278
312
  2. Verify that consumer groups are correctly created
279
313
  3. Monitor the DLQ for failed events
280
314
  4. Review the performance metrics for any anomalies
281
- 5. Check the logs for detailed error messages
315
+ 5. Check the logs for detailed error messages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "5.0.0",
3
+ "version": "5.2.1",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "^0.0.12",
@@ -21,6 +21,11 @@ export declare class PrometheusAdapter {
21
21
  private subscribeErrorCount;
22
22
  private individualQueueDepth;
23
23
  private duplicateEventCount;
24
+ private messageRatePublish;
25
+ private messageRateSubscribe;
26
+ private processingTimeHistogram;
27
+ private redisCommandLatency;
28
+ private consumerLag;
24
29
  /**
25
30
  *
26
31
  * @param streams [Publisher]
@@ -75,6 +75,36 @@ class PrometheusAdapter {
75
75
  labelNames: ['queue_name'],
76
76
  registers: [this.registry],
77
77
  });
78
+ this.messageRatePublish = new this.promClient.Gauge({
79
+ name: 'message_rate_publish',
80
+ help: 'Number of messages published per event type',
81
+ labelNames: ['event_type'],
82
+ registers: [this.registry],
83
+ });
84
+ this.messageRateSubscribe = new this.promClient.Gauge({
85
+ name: 'message_rate_subscribe',
86
+ help: 'Number of messages subscribed per event type',
87
+ labelNames: ['event_type'],
88
+ registers: [this.registry],
89
+ });
90
+ this.processingTimeHistogram = new this.promClient.Histogram({
91
+ name: 'processing_time_histogram',
92
+ help: 'Distribution of event processing times',
93
+ buckets: [0, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000],
94
+ registers: [this.registry],
95
+ });
96
+ this.redisCommandLatency = new this.promClient.Gauge({
97
+ name: 'redis_command_latency',
98
+ help: 'Latency of Redis commands',
99
+ labelNames: ['command'],
100
+ registers: [this.registry],
101
+ });
102
+ this.consumerLag = new this.promClient.Gauge({
103
+ name: 'consumer_lag',
104
+ help: 'Lag of consumers in milliseconds',
105
+ labelNames: ['consumer_group'],
106
+ registers: [this.registry],
107
+ });
78
108
  this.registry.registerMetric(this.queueDepth);
79
109
  this.registry.registerMetric(this.dlqSize);
80
110
  this.registry.registerMetric(this.dlqRate);
@@ -87,6 +117,11 @@ class PrometheusAdapter {
87
117
  this.registry.registerMetric(this.subscribeErrorCount);
88
118
  this.registry.registerMetric(this.individualQueueDepth);
89
119
  this.registry.registerMetric(this.duplicateEventCount);
120
+ this.registry.registerMetric(this.messageRatePublish);
121
+ this.registry.registerMetric(this.messageRateSubscribe);
122
+ this.registry.registerMetric(this.processingTimeHistogram);
123
+ this.registry.registerMetric(this.redisCommandLatency);
124
+ this.registry.registerMetric(this.consumerLag);
90
125
  }
91
126
  async updateMetrics() {
92
127
  const metrics = await this.streams.getLatestMetrics();
@@ -111,6 +146,24 @@ class PrometheusAdapter {
111
146
  this.individualQueueDepth.set({ queue_name: queueName }, depth);
112
147
  });
113
148
  }
149
+ Object.entries(metrics.messageRatePublish).forEach(([eventType, rate]) => {
150
+ this.messageRatePublish.set({ event_type: eventType }, rate);
151
+ });
152
+ Object.entries(metrics.messageRateSubscribe).forEach(([eventType, rate]) => {
153
+ this.messageRateSubscribe.set({ event_type: eventType }, rate);
154
+ });
155
+ Object.entries(metrics.processingTimeDistribution).forEach(([bucket, count]) => {
156
+ const [lower, upper] = bucket.split('-').map(Number);
157
+ for (let i = 0; i < count; i++) {
158
+ this.processingTimeHistogram.observe(upper || lower);
159
+ }
160
+ });
161
+ Object.entries(metrics.redisCommandLatencies).forEach(([command, latency]) => {
162
+ this.redisCommandLatency.set({ command }, latency);
163
+ });
164
+ Object.entries(metrics.consumerLag).forEach(([consumerGroup, lag]) => {
165
+ this.consumerLag.set({ consumer_group: consumerGroup }, lag);
166
+ });
114
167
  }
115
168
  /**
116
169
  * @param app This needs to be an instance of express or fastify something that supports the express api
@@ -10,6 +10,11 @@ export declare class MetricsCollector {
10
10
  constructor(config: IMetricsCollectorConfig, dlq: DeadLetterQueue);
11
11
  addMetrics(metrics: IPerformanceMetrics): void;
12
12
  private aggregateAndStoreMetrics;
13
+ private aggregateMessageRates;
14
+ private aggregateHistogram;
15
+ private aggregateRedisCommandLatencies;
16
+ private aggregateConsumerLag;
17
+ private calculateAverages;
13
18
  private getQueueDepth;
14
19
  private storeMetrics;
15
20
  getMetrics(startTime: number, endTime: number): Promise<IAggregatedMetrics[]>;
@@ -32,6 +32,11 @@ class MetricsCollector {
32
32
  dlqRate: 0,
33
33
  operationCount: this.metrics.length,
34
34
  duplicateEventsCount: 0,
35
+ messageRatePublish: {},
36
+ messageRateSubscribe: {},
37
+ processingTimeDistribution: {},
38
+ redisCommandLatencies: {},
39
+ consumerLag: {},
35
40
  };
36
41
  for (const metric of this.metrics) {
37
42
  aggregated.totalTime += metric.totalTime;
@@ -41,6 +46,11 @@ class MetricsCollector {
41
46
  aggregated.publishErrorCount += metric.publishErrorCount;
42
47
  aggregated.subscribeErrorCount += metric.subscribeErrorCount;
43
48
  aggregated.duplicateEventsCount += metric.duplicateEventsCount;
49
+ this.aggregateMessageRates(aggregated.messageRatePublish, metric.messageRatePublish);
50
+ this.aggregateMessageRates(aggregated.messageRateSubscribe, metric.messageRateSubscribe);
51
+ this.aggregateHistogram(aggregated.processingTimeDistribution, metric.processingTimeDistribution);
52
+ this.aggregateRedisCommandLatencies(aggregated.redisCommandLatencies, metric.redisCommandLatencies);
53
+ this.aggregateConsumerLag(aggregated.consumerLag, metric.consumerLag);
44
54
  }
45
55
  // Calculate averages for time-based metrics
46
56
  aggregated.totalTime /= aggregated.operationCount;
@@ -49,21 +59,65 @@ class MetricsCollector {
49
59
  const dlqStats = await this.dlq.getDLQStats();
50
60
  aggregated.dlqSize = dlqStats.size;
51
61
  aggregated.dlqRate = dlqStats.additionRate;
62
+ this.calculateAverages(aggregated.redisCommandLatencies);
63
+ this.calculateAverages(aggregated.consumerLag);
52
64
  await this.storeMetrics(aggregated);
53
65
  this.metrics = [];
54
66
  }
67
+ aggregateMessageRates(target, source) {
68
+ for (const [eventType, count] of Object.entries(source)) {
69
+ if (!target[eventType])
70
+ target[eventType] = 0;
71
+ target[eventType] += count;
72
+ }
73
+ }
74
+ aggregateHistogram(target, source) {
75
+ for (const [bucket, count] of Object.entries(source)) {
76
+ if (!target[bucket])
77
+ target[bucket] = 0;
78
+ target[bucket] += count;
79
+ }
80
+ }
81
+ aggregateRedisCommandLatencies(target, source) {
82
+ for (const [command, { total, count }] of Object.entries(source)) {
83
+ if (!target[command]) {
84
+ target[command] = { total: 0, count: 0 };
85
+ }
86
+ target[command].total += total;
87
+ target[command].count += count;
88
+ }
89
+ }
90
+ aggregateConsumerLag(target, source) {
91
+ for (const [consumerGroup, { total, count }] of Object.entries(source)) {
92
+ if (!target[consumerGroup]) {
93
+ target[consumerGroup] = { total: 0, count: 0 };
94
+ }
95
+ target[consumerGroup].total += total;
96
+ target[consumerGroup].count += count;
97
+ }
98
+ }
99
+ calculateAverages(data) {
100
+ for (const key in data) {
101
+ if (data[key].count > 0) {
102
+ data[key].total = data[key].total / data[key].count;
103
+ }
104
+ else {
105
+ data[key].total = 0;
106
+ }
107
+ }
108
+ }
55
109
  async getQueueDepth() {
56
110
  let totalDepth = 0;
57
111
  const individualDepths = {};
58
112
  let cursor = '0';
59
113
  do {
60
- const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', '*:cg-*', 'COUNT', 100);
114
+ const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', `ack:*:cg-*`, 'COUNT', 100);
61
115
  cursor = nextCursor;
62
116
  if (keys.length > 0) {
63
117
  const pipeline = this.redisClient.pipeline();
64
118
  keys.forEach((key) => {
65
- pipeline.xlen(key);
66
- pipeline.zcard(`ack:${key}`);
119
+ pipeline.xlen(key.slice(4));
120
+ pipeline.zcard(key);
67
121
  });
68
122
  const results = await pipeline.exec();
69
123
  if (!results) {
@@ -11,5 +11,10 @@ export declare class MetricsTracker {
11
11
  incrementDuplicateEvent(): void;
12
12
  incrementErrorCount(type: 'publish' | 'subscribe'): void;
13
13
  getMetrics(): IPerformanceMetrics;
14
+ incrementMessageRate(type: 'publish' | 'subscribe', eventType: string): void;
15
+ addProcessingTime(time: number): void;
16
+ private getBucket;
17
+ addRedisCommandLatency(command: string, time: number): void;
18
+ setConsumerLag(consumerGroup: string, lag: number): void;
14
19
  reset(): void;
15
20
  }
@@ -12,6 +12,11 @@ class MetricsTracker {
12
12
  publishErrorCount: 0,
13
13
  subscribeErrorCount: 0,
14
14
  duplicateEventsCount: 0,
15
+ messageRatePublish: {},
16
+ messageRateSubscribe: {},
17
+ processingTimeDistribution: {},
18
+ redisCommandLatencies: {},
19
+ consumerLag: {},
15
20
  };
16
21
  this.startTime = (0, node_process_1.hrtime)();
17
22
  }
@@ -43,6 +48,43 @@ class MetricsTracker {
43
48
  this.metrics.totalTime = totalSeconds * 1000 + totalNanoseconds / 1e6;
44
49
  return { ...this.metrics };
45
50
  }
51
+ incrementMessageRate(type, eventType) {
52
+ const key = `messageRate${type.charAt(0).toUpperCase() + type.slice(1)}`;
53
+ if (!this.metrics[key][eventType]) {
54
+ this.metrics[key][eventType] = 0;
55
+ }
56
+ this.metrics[key][eventType]++;
57
+ }
58
+ addProcessingTime(time) {
59
+ const bucket = this.getBucket(time);
60
+ if (!this.metrics.processingTimeDistribution[bucket]) {
61
+ this.metrics.processingTimeDistribution[bucket] = 0;
62
+ }
63
+ this.metrics.processingTimeDistribution[bucket]++;
64
+ }
65
+ getBucket(time) {
66
+ const buckets = [0, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
67
+ for (let i = 0; i < buckets.length; i++) {
68
+ if (time <= buckets[i]) {
69
+ return i === 0 ? `0-${buckets[i]}` : `${buckets[i - 1] + 1}-${buckets[i]}`;
70
+ }
71
+ }
72
+ return `>${buckets[buckets.length - 1]}`;
73
+ }
74
+ addRedisCommandLatency(command, time) {
75
+ if (!this.metrics.redisCommandLatencies[command]) {
76
+ this.metrics.redisCommandLatencies[command] = { total: 0, count: 0 };
77
+ }
78
+ this.metrics.redisCommandLatencies[command].total += time;
79
+ this.metrics.redisCommandLatencies[command].count++;
80
+ }
81
+ setConsumerLag(consumerGroup, lag) {
82
+ if (!this.metrics.consumerLag[consumerGroup]) {
83
+ this.metrics.consumerLag[consumerGroup] = { total: 0, count: 0 };
84
+ }
85
+ this.metrics.consumerLag[consumerGroup].total += lag;
86
+ this.metrics.consumerLag[consumerGroup].count++;
87
+ }
46
88
  reset() {
47
89
  this.startTime = (0, node_process_1.hrtime)();
48
90
  this.metrics = {
@@ -53,6 +95,11 @@ class MetricsTracker {
53
95
  publishErrorCount: 0,
54
96
  subscribeErrorCount: 0,
55
97
  duplicateEventsCount: 0,
98
+ messageRatePublish: {},
99
+ messageRateSubscribe: {},
100
+ processingTimeDistribution: {},
101
+ redisCommandLatencies: {},
102
+ consumerLag: {},
56
103
  };
57
104
  }
58
105
  }
@@ -19,7 +19,7 @@ export interface IMetricsCollectorConfig {
19
19
  collectionInterval: number;
20
20
  retentionPeriod: number;
21
21
  }
22
- export type TQueryableMetrics = Omit<IAggregatedMetrics, 'timestamp' | 'individualQueueDepts'>;
22
+ export type TQueryableMetrics = Omit<IAggregatedMetrics, 'timestamp' | 'individualQueueDepts' | 'messageRatePublish' | 'messageRateSubscribe' | 'processingTimeDistribution' | 'redisCommandLatencies' | 'consumerLag'>;
23
23
  export interface IQueueDepths {
24
24
  total: number;
25
25
  individual: {
@@ -34,4 +34,25 @@ export interface IPerformanceMetrics {
34
34
  publishErrorCount: number;
35
35
  subscribeErrorCount: number;
36
36
  duplicateEventsCount: number;
37
+ messageRatePublish: {
38
+ [eventType: string]: number;
39
+ };
40
+ messageRateSubscribe: {
41
+ [eventType: string]: number;
42
+ };
43
+ processingTimeDistribution: {
44
+ [bucket: string]: number;
45
+ };
46
+ redisCommandLatencies: {
47
+ [command: string]: {
48
+ total: number;
49
+ count: number;
50
+ };
51
+ };
52
+ consumerLag: {
53
+ [consumerGroup: string]: {
54
+ total: number;
55
+ count: number;
56
+ };
57
+ };
37
58
  }
@@ -15,7 +15,9 @@ class ContentBasedDeduplication {
15
15
  return hmac.update(JSON.stringify(hashableData)).digest('hex');
16
16
  }
17
17
  async isDuplicate(event, consumerGroupName) {
18
- const eventHash = this.calculateEventHash(event.data);
18
+ if (event.data === null)
19
+ return false;
20
+ const eventHash = this.calculateEventHash(event);
19
21
  const key = `processed:${consumerGroupName}:${eventHash}`;
20
22
  const result = await this.redisClient.set(key, '1', 'EX', this.ttl, 'NX');
21
23
  const isDuplicate = result === null;
@@ -62,11 +62,18 @@ class RedisRegistry {
62
62
  },
63
63
  });
64
64
  }
65
- else
65
+ else if (RedisRegistry.options.sentinels) {
66
+ ref = new ioredis_1.default({
67
+ ...RedisRegistry.options.sentinels.options,
68
+ sentinels: RedisRegistry.options.sentinels.nodes,
69
+ });
70
+ }
71
+ else {
66
72
  ref = new ioredis_1.default({
67
73
  ...RedisRegistry.options.redis,
68
74
  db: storeRef,
69
75
  });
76
+ }
70
77
  }
71
78
  return ref;
72
79
  }
@@ -59,7 +59,7 @@ class ScheduledProcessor {
59
59
  // Publish the event to each consumer group's stream
60
60
  const streamName = `${eventData.eventName}:${consumerGroup}`;
61
61
  const generatedKey = await this.redisPublisher
62
- .xadd(streamName, '*', 'data', JSON.stringify(eventData))
62
+ .xadd(streamName, key, 'data', JSON.stringify(eventData))
63
63
  .catch((e) => logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Publishing event ${eventData.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(eventData)}, ${e} `));
64
64
  if (key === '*')
65
65
  key = generatedKey ?? key;
@@ -200,4 +200,7 @@ export declare class Streams {
200
200
  * circuit is OPEN
201
201
  */
202
202
  processStoredEvents(): Promise<void>;
203
+ acknowledgeMessage(ackKey: string): Promise<void>;
204
+ private frameMessageKey;
205
+ private demergeMessageKey;
203
206
  }
@@ -53,7 +53,7 @@ class Streams {
53
53
  errorThresholdPercentage: 50,
54
54
  openStateDuration: 30000,
55
55
  halfOpenStateMaxAttempts: 10,
56
- maxStoredEvents: 5000,
56
+ maxStoredEvents: 10000,
57
57
  },
58
58
  };
59
59
  /** Initialise Config properties */
@@ -149,6 +149,7 @@ class Streams {
149
149
  const generatedKey = await this.redisPublisher.xadd(streamName, key, 'data', JSON.stringify(data));
150
150
  tracker.endRedisOperation();
151
151
  tracker.incrementEventCount();
152
+ tracker.incrementMessageRate('publish', data.eventName);
152
153
  if (this.metricsCollector) {
153
154
  this.metricsCollector.addMetrics(tracker.getMetrics());
154
155
  }
@@ -232,10 +233,11 @@ class Streams {
232
233
  initialDelay: this.config.initialRetryDelay,
233
234
  filterKeepAlive: this.config.filterKeepAlive,
234
235
  publishOnceGuarantee: false,
236
+ externalAcknowledgement: false,
235
237
  ...listenerOptions,
236
238
  };
237
239
  const subscriptionId = (0, id_1.generateID)('HEX');
238
- return this.listenInternals(eventName, subscriptionId, options.eventFilter, options.filterKeepAlive, options.publishOnceGuarantee).pipe((0, rxjs_1.retry)({
240
+ return this.listenInternals(eventName, subscriptionId, options.eventFilter, options.filterKeepAlive, options.publishOnceGuarantee, options.externalAcknowledgement).pipe((0, rxjs_1.retry)({
239
241
  count: options.maxRetries,
240
242
  delay: (error, retryAttempt) => {
241
243
  const delay = options.initialDelay * Math.pow(2, retryAttempt);
@@ -256,7 +258,13 @@ class Streams {
256
258
  this.eventsListened.push(eventName);
257
259
  try {
258
260
  // Check if the consumer group already exists
259
- const groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
261
+ let groupInfo = [];
262
+ try {
263
+ groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
264
+ }
265
+ catch (e) {
266
+ // Do nothing
267
+ }
260
268
  let groupExists = false;
261
269
  for (const group of groupInfo) {
262
270
  if (group[1] === this.consumerGroupName) {
@@ -276,7 +284,13 @@ class Streams {
276
284
  logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
277
285
  }
278
286
  // Check if the consumer already exists in the group
279
- const consumers = (await this.redisGroups.xinfo('CONSUMERS', streamName, this.consumerGroupName));
287
+ let consumers = [];
288
+ try {
289
+ consumers = (await this.redisGroups.xinfo('CONSUMERS', streamName, this.consumerGroupName));
290
+ }
291
+ catch (e) {
292
+ // Do nothing
293
+ }
280
294
  let consumerExists = false;
281
295
  for (const consumer of consumers) {
282
296
  if (consumer[1] === this.instanceId) {
@@ -298,7 +312,7 @@ class Streams {
298
312
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Consumer Registered and created with ${this.instanceId} under ${this.consumerGroupName} with ${createConsumerStatus} consumers and with the following status ${JSON.stringify({ addToCGSet, addToFlushSet })}`);
299
313
  return createConsumerStatus === 0 || createConsumerStatus === 1;
300
314
  }
301
- listenInternals(eventName, subscriptionId, eventFilter, filterKeepAlive = 24 * 60 * 60 * 1000, publishOnceGuarantee = false) {
315
+ listenInternals(eventName, subscriptionId, eventFilter, filterKeepAlive = 24 * 60 * 60 * 1000, publishOnceGuarantee = false, externalAcknowledgement = false) {
302
316
  if (!this.subscriptions.has(eventName)) {
303
317
  this.subscriptions.set(eventName, new Map());
304
318
  }
@@ -314,7 +328,6 @@ class Streams {
314
328
  /** Clear earlier unprocessed messages. Runs every 10 seconds */
315
329
  await processMessage(this.redisGroups, '0', new tracker_1.MetricsTracker(), false);
316
330
  });
317
- let lastMatchTime = Date.now();
318
331
  const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1), (0, rxjs_1.finalize)(() => {
319
332
  /** Cleanup timer */
320
333
  timer.unsubscribe();
@@ -361,6 +374,10 @@ class Streams {
361
374
  else {
362
375
  const messages = (await redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'STREAMS', streamName, '>'));
363
376
  if (messages && messages.length) {
377
+ if (messageId === '0') {
378
+ messageId = messages[0][1][0][0];
379
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Reprocessing unprocessed message with id: ${messageId}`);
380
+ }
364
381
  eventData = JSON.parse(messages[0][1][0][1][1]);
365
382
  }
366
383
  }
@@ -376,13 +393,14 @@ class Streams {
376
393
  }
377
394
  }
378
395
  try {
396
+ const ackKey = this.frameMessageKey(streamName, messageId);
379
397
  const subscriptions = this.subscriptions.get(eventName);
380
398
  if (subscriptions) {
381
399
  const subscriptionEntries = Array.from(subscriptions.entries());
382
400
  for (let i = 0; i < subscriptionEntries.length; i++) {
383
401
  const [subId, sub] = subscriptionEntries[i];
384
402
  if (!sub.filter || sub.filter(eventData)) {
385
- sub.subject.next(eventData);
403
+ sub.subject.next({ ...eventData, ackKey });
386
404
  sub.lastMatchTime = Date.now();
387
405
  }
388
406
  else if (Date.now() - sub.lastMatchTime > sub.keepAlive) {
@@ -397,8 +415,8 @@ class Streams {
397
415
  }
398
416
  }
399
417
  }
400
- await redisClient.xack(streamName, this.consumerGroupName, messageId);
401
- await redisClient.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
418
+ if (!externalAcknowledgement)
419
+ this.acknowledgeMessage(ackKey);
402
420
  }
403
421
  catch (processingError) {
404
422
  logger_1.PUBLISHER_LOGGER.error(`Processing error for message ${messageId}:`, processingError);
@@ -412,6 +430,11 @@ class Streams {
412
430
  };
413
431
  await this.dlq.addToDLQ(dlqEvent);
414
432
  }
433
+ tracker.incrementMessageRate('subscribe', eventData.eventName);
434
+ const processingTime = Date.now() - eventData.createdAt;
435
+ tracker.addProcessingTime(processingTime);
436
+ const lag = Date.now() - eventData.createdAt;
437
+ tracker.setConsumerLag(this.consumerGroupName, lag);
415
438
  }
416
439
  else {
417
440
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
@@ -475,7 +498,7 @@ class Streams {
475
498
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Stream Notification Received for event ${eventName} with message ID ${messageIdRead}`);
476
499
  await processMessage(this.redisGroups, messageIdRead, tracker, multicastRead);
477
500
  const metrics = tracker.getMetrics();
478
- logger_1.PERFORMANCE_LOGGER.log(`STIME;${messageIdRead};${data.eventName};${Date.now()};${metrics.totalTime};${metrics.redisOperationTime};${metrics.processingTime}`);
501
+ logger_1.PERFORMANCE_LOGGER.log(`STIME;${messageIdRead};${eventName};${Date.now()};${metrics.totalTime};${metrics.redisOperationTime};${metrics.processingTime}`);
479
502
  if (this.metricsCollector) {
480
503
  this.metricsCollector.addMetrics(tracker.getMetrics());
481
504
  }
@@ -650,5 +673,17 @@ class Streams {
650
673
  await this.circuitBreaker.clearStoredEvents();
651
674
  }
652
675
  }
676
+ async acknowledgeMessage(ackKey) {
677
+ const { streamName, messageId } = this.demergeMessageKey(ackKey);
678
+ await this.redisGroups.xack(streamName, this.consumerGroupName, messageId);
679
+ await this.redisGroups.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
680
+ }
681
+ frameMessageKey(streamName, messageId) {
682
+ return `${streamName}##${messageId}`;
683
+ }
684
+ demergeMessageKey(messageKey) {
685
+ const [streamName, messageId] = messageKey.split('##');
686
+ return { streamName, messageId };
687
+ }
653
688
  }
654
689
  exports.Streams = Streams;
@@ -5,6 +5,7 @@ export type EventData<TData, TName extends string> = {
5
5
  createdAt?: number;
6
6
  republishEvent?: string;
7
7
  repeatInterval?: number;
8
+ ackKey?: string;
8
9
  };
9
10
  export type PublishData<TData, TName extends string> = {
10
11
  data: TData;
@@ -13,7 +14,7 @@ export type PublishData<TData, TName extends string> = {
13
14
  };
14
15
  export type PendingMessages = [never, never, string, string, Array<[string, number]>];
15
16
  export type ClaimedMessages = Array<[never, string]>;
16
- import { ClusterNode, ClusterOptions, RedisOptions } from 'ioredis';
17
+ import { ClusterNode, ClusterOptions, RedisOptions, SentinelAddress, SentinelConnectionOptions } from 'ioredis';
17
18
  import { BehaviorSubject } from 'rxjs';
18
19
  export interface IOptions {
19
20
  redis?: RedisOptions;
@@ -21,6 +22,10 @@ export interface IOptions {
21
22
  options: ClusterOptions;
22
23
  nodes: Array<ClusterNode>;
23
24
  };
25
+ sentinels?: {
26
+ options: SentinelConnectionOptions;
27
+ nodes: Array<SentinelAddress>;
28
+ };
24
29
  }
25
30
  export interface IDLQEvent extends EventData<unknown, string> {
26
31
  eventId: string;
@@ -63,6 +68,7 @@ export interface IListenOptions<T> {
63
68
  eventFilter?: TEventFilter<T>;
64
69
  filterKeepAlive: number;
65
70
  publishOnceGuarantee: boolean;
71
+ externalAcknowledgement: boolean;
66
72
  }
67
73
  export interface BatchPublishOptions {
68
74
  batchSize?: number;