@jetit/publisher 5.1.1 → 5.2.2
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
|
@@ -10,7 +10,7 @@ class ContentBasedDeduplication {
|
|
|
10
10
|
this.hmacSecret = 'content-hash-calculation';
|
|
11
11
|
}
|
|
12
12
|
calculateEventHash(eventData) {
|
|
13
|
-
const { eventId, timestamp, createdAt, ...hashableData } = eventData;
|
|
13
|
+
const { eventId, timestamp, createdAt, republishEvent, ...hashableData } = eventData;
|
|
14
14
|
const hmac = (0, crypto_1.createHmac)('sha256', this.hmacSecret);
|
|
15
15
|
return hmac.update(JSON.stringify(hashableData)).digest('hex');
|
|
16
16
|
}
|
|
@@ -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
|
}
|
package/src/lib/redis/streams.js
CHANGED
|
@@ -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
|
-
|
|
418
|
-
|
|
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};${
|
|
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;
|
package/src/lib/redis/types.d.ts
CHANGED
|
@@ -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;
|