@jetit/publisher 5.1.1 → 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.1.1",
3
+ "version": "5.2.1",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "^0.0.12",
@@ -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
  }
@@ -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
  }
@@ -233,10 +233,11 @@ class Streams {
233
233
  initialDelay: this.config.initialRetryDelay,
234
234
  filterKeepAlive: this.config.filterKeepAlive,
235
235
  publishOnceGuarantee: false,
236
+ externalAcknowledgement: false,
236
237
  ...listenerOptions,
237
238
  };
238
239
  const subscriptionId = (0, id_1.generateID)('HEX');
239
- 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)({
240
241
  count: options.maxRetries,
241
242
  delay: (error, retryAttempt) => {
242
243
  const delay = options.initialDelay * Math.pow(2, retryAttempt);
@@ -311,7 +312,7 @@ class Streams {
311
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 })}`);
312
313
  return createConsumerStatus === 0 || createConsumerStatus === 1;
313
314
  }
314
- 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) {
315
316
  if (!this.subscriptions.has(eventName)) {
316
317
  this.subscriptions.set(eventName, new Map());
317
318
  }
@@ -327,7 +328,6 @@ class Streams {
327
328
  /** Clear earlier unprocessed messages. Runs every 10 seconds */
328
329
  await processMessage(this.redisGroups, '0', new tracker_1.MetricsTracker(), false);
329
330
  });
330
- let lastMatchTime = Date.now();
331
331
  const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1), (0, rxjs_1.finalize)(() => {
332
332
  /** Cleanup timer */
333
333
  timer.unsubscribe();
@@ -393,13 +393,14 @@ class Streams {
393
393
  }
394
394
  }
395
395
  try {
396
+ const ackKey = this.frameMessageKey(streamName, messageId);
396
397
  const subscriptions = this.subscriptions.get(eventName);
397
398
  if (subscriptions) {
398
399
  const subscriptionEntries = Array.from(subscriptions.entries());
399
400
  for (let i = 0; i < subscriptionEntries.length; i++) {
400
401
  const [subId, sub] = subscriptionEntries[i];
401
402
  if (!sub.filter || sub.filter(eventData)) {
402
- sub.subject.next(eventData);
403
+ sub.subject.next({ ...eventData, ackKey });
403
404
  sub.lastMatchTime = Date.now();
404
405
  }
405
406
  else if (Date.now() - sub.lastMatchTime > sub.keepAlive) {
@@ -414,8 +415,8 @@ class Streams {
414
415
  }
415
416
  }
416
417
  }
417
- await redisClient.xack(streamName, this.consumerGroupName, messageId);
418
- await redisClient.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
418
+ if (!externalAcknowledgement)
419
+ this.acknowledgeMessage(ackKey);
419
420
  }
420
421
  catch (processingError) {
421
422
  logger_1.PUBLISHER_LOGGER.error(`Processing error for message ${messageId}:`, processingError);
@@ -497,7 +498,7 @@ class Streams {
497
498
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Stream Notification Received for event ${eventName} with message ID ${messageIdRead}`);
498
499
  await processMessage(this.redisGroups, messageIdRead, tracker, multicastRead);
499
500
  const metrics = tracker.getMetrics();
500
- 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}`);
501
502
  if (this.metricsCollector) {
502
503
  this.metricsCollector.addMetrics(tracker.getMetrics());
503
504
  }
@@ -672,5 +673,17 @@ class Streams {
672
673
  await this.circuitBreaker.clearStoredEvents();
673
674
  }
674
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
+ }
675
688
  }
676
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;