@jetit/publisher 3.3.5 → 4.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "3.3.5",
3
+ "version": "4.0.0",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "^0.0.12",
@@ -43,10 +43,11 @@ export declare class Streams {
43
43
  * const eventData = { eventName: 'order.created', data: orderData };
44
44
  * await streams.publish(eventData);
45
45
  */
46
- publish<TData = unknown, TName extends string = string>(data: PublishData<TData, TName>, multicast?: boolean): Promise<void>;
46
+ publish<TData = unknown, TName extends string = string>(data: PublishData<TData, TName>, multicast?: boolean): Promise<string>;
47
47
  /**
48
48
  * Schedules an event to be published at a specified future time. Thee event gets published if the
49
- * differnece between the current time and the scheduled time is less than 500ms.
49
+ * differnece between the current time and the scheduled time is less than 500ms. The granularity
50
+ * of scheduled publish is 5 seconds. So it doesnt make sense to run anything less than the 5 secs time
50
51
  *
51
52
  * @param scheduledTime - The Date object representing the future time when the event should be published.
52
53
  * @param eventData - The event data object, containing the event name and its associated data.
@@ -120,4 +121,14 @@ export declare class Streams {
120
121
  */
121
122
  close(): Promise<void>;
122
123
  private cleanupAcknowledgedMessages;
124
+ /**
125
+ * This function should be added to Surf Signal to publish periodic diagnostic information
126
+ * on the health of the stream
127
+ */
128
+ getUnacknowledgedMessagesForStream(eventName: string): Promise<{
129
+ count: number;
130
+ countOnThisConsumer?: number | undefined;
131
+ messageIds: string[];
132
+ messages?: unknown[] | undefined;
133
+ }>;
123
134
  }
@@ -88,6 +88,7 @@ class Streams {
88
88
  const publishEndTime = process.hrtime(publishStartTime);
89
89
  const elapsedTime = publishEndTime[0] * 1000 + publishEndTime[1] / 1000000;
90
90
  logger_1.PERFORMANCE_LOGGER.log(`PTIME;${key};${data.eventName};${Date.now()};${elapsedTime}`);
91
+ return key;
91
92
  }
92
93
  async scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0, multicast = false) {
93
94
  const currentTime = new Date();
@@ -175,13 +176,23 @@ class Streams {
175
176
  }
176
177
  listenInternals(eventName) {
177
178
  const bs = new rxjs_1.BehaviorSubject(null);
178
- const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1));
179
+ const timer = (0, rxjs_1.interval)(10000).subscribe(async () => {
180
+ /** Clear earlier unprocessed messages. Runs every 10 seconds */
181
+ await processMessage(this.redisGroups, '0', false);
182
+ });
183
+ const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1), (0, rxjs_1.finalize)(() => {
184
+ /** Cleanup timer */
185
+ timer.unsubscribe();
186
+ }));
179
187
  const streamName = `${eventName}:${this.consumerGroupName}`;
180
188
  const processMessage = async (redisClient, messageId, multicast = false, processPending = false) => {
181
189
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
182
190
  try {
183
191
  try {
184
- const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1, this.instanceId);
192
+ /**
193
+ * Check if the message is already acquired by another client and is pending.
194
+ */
195
+ const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1);
185
196
  if (pendingDetails[2] === 0 && multicast === false) {
186
197
  logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: MACK ${messageId} for ${streamName}`);
187
198
  return;
@@ -192,20 +203,37 @@ class Streams {
192
203
  logger_1.PUBLISHER_LOGGER.error('XPENDING ERROR: To be handled');
193
204
  logger_1.PUBLISHER_LOGGER.warn(JSON.stringify(e));
194
205
  }
195
- const messages = await redisClient.xrange(streamName, messageId, messageId);
196
- if (messages && messages.length) {
197
- const eventData = JSON.parse(messages[0][1][1]);
206
+ let eventData;
207
+ /**
208
+ * Both multicast messages and pending messages cannot be read by xreadgroup
209
+ * Multicast messages should not be claimed by a single consumer. And pending messages
210
+ * are usually behind in the stream so XREADGROUP will not read them and hence
211
+ * they need to be read using XRANGE.
212
+ */
213
+ if (multicast === true || processPending) {
214
+ const messages = await redisClient.xrange(streamName, messageId, messageId);
215
+ if (messages && messages.length) {
216
+ eventData = JSON.parse(messages[0][1][1]);
217
+ }
218
+ }
219
+ else {
220
+ const messages = (await redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'STREAMS', streamName, '>'));
221
+ if (messages && messages.length) {
222
+ eventData = JSON.parse(messages[0][1][0][1][1]);
223
+ }
224
+ }
225
+ if (eventData) {
198
226
  bs.next(eventData);
199
227
  await redisClient.xack(streamName, this.consumerGroupName, messageId);
200
228
  await redisClient.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
201
229
  }
202
230
  else {
203
- logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
231
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
204
232
  }
205
233
  /** Process Unprocessed Message if this is a main tree, otherwise limit to processing 100 messages that are unacknowledged */
206
234
  if (!processPending) {
207
- const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, 25);
208
- if (unprocessedMessageIds.count > 25) {
235
+ const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, this.instanceId);
236
+ if (unprocessedMessageIds.countOnThisConsumer && unprocessedMessageIds.countOnThisConsumer > 25) {
209
237
  logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
210
238
  }
211
239
  for (const id of unprocessedMessageIds.messageIds) {
@@ -289,5 +317,14 @@ class Streams {
289
317
  await this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
290
318
  }
291
319
  }
320
+ /**
321
+ * This function should be added to Surf Signal to publish periodic diagnostic information
322
+ * on the health of the stream
323
+ */
324
+ async getUnacknowledgedMessagesForStream(eventName) {
325
+ const streamName = `${eventName}:${this.consumerGroupName}`;
326
+ const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(this.redisGroups, this.consumerGroupName, streamName, this.instanceId);
327
+ return unprocessedMessageIds;
328
+ }
292
329
  }
293
330
  exports.Streams = Streams;
@@ -1,8 +1,9 @@
1
1
  import { RedisType } from './registry';
2
2
  import { EventData } from './types';
3
3
  export declare function getAllConsumerGroups(eventName: string, redisConnection: RedisType): Promise<string[]>;
4
- export declare function getUnacknowledgedMessages(redisClient: RedisType, consumerGroupName: string, streamName: string, count?: number): Promise<{
4
+ export declare function getUnacknowledgedMessages(redisClient: RedisType, consumerGroupName: string, streamName: string, consumerName: string, count?: number): Promise<{
5
5
  count: number;
6
+ countOnThisConsumer?: number;
6
7
  messageIds: string[];
7
8
  messages?: unknown[];
8
9
  }>;
@@ -7,20 +7,26 @@ async function getAllConsumerGroups(eventName, redisConnection) {
7
7
  return consumerGroups;
8
8
  }
9
9
  exports.getAllConsumerGroups = getAllConsumerGroups;
10
- async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, count = 500) {
10
+ async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, consumerName, count = 500) {
11
11
  try {
12
12
  // Get pending messages summary
13
13
  const summary = await redisClient.xpending(streamName, consumerGroupName);
14
- if (!summary || summary[1] === 0) {
14
+ if (!summary || summary[0] === 0) {
15
15
  // If count is zero
16
16
  return { count: 0, messageIds: [] };
17
17
  }
18
18
  // Use the smallest and largest IDs to get a detailed range
19
- const pendingMessageCount = summary[1];
19
+ const pendingMessageCount = summary[0];
20
20
  // Get detailed information in the range
21
- const pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count));
21
+ let pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count, consumerName));
22
+ /** If no pending messages on consumer, fetch messages from other consumers that haven't been claimed for more than 10s */
23
+ if (count > pendingMessages.length && pendingMessages.length === 0) {
24
+ await redisClient.xautoclaim(streamName, consumerGroupName, consumerName, 10000, '0-0', 'COUNT', 100);
25
+ pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count, consumerName));
26
+ }
22
27
  return {
23
28
  count: pendingMessageCount,
29
+ countOnThisConsumer: pendingMessages.length,
24
30
  messageIds: pendingMessages.map((message) => message[0]),
25
31
  messages: pendingMessages,
26
32
  };