@jetit/publisher 3.3.3 → 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/README.md CHANGED
@@ -198,4 +198,6 @@ streams.listen('my-event').subscribe(event => {
198
198
 
199
199
  7. Event-driven workflows: You can use the publisher to create event-driven workflows, where each step in the workflow is triggered by the completion of a previous step. This can be useful for orchestrating complex, multi-step processes.
200
200
 
201
- 8. Message broadcasting: The publisher can be used to broadcast messages to multiple consumers or subscribers, allowing for efficient and scalable communication in applications with many components or services.
201
+ 8. Message broadcasting: The publisher can be used to broadcast messages to multiple consumers or subscribers, allowing for efficient and scalable communication in applications with many components or services.
202
+
203
+ 9. Multicast Publishing: This is the existing PUB/SUB implementation but with the event data being stored into streams for additional processing
package/package.json CHANGED
@@ -1,13 +1,11 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "3.3.3",
3
+ "version": "4.0.0",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
- "@jetit/id": "0.0.11",
6
+ "@jetit/id": "^0.0.12",
7
7
  "ioredis": "^5.3.0",
8
- "rxjs": "^7.8.0"
9
- },
10
- "peerDependencies": {
8
+ "rxjs": "^7.8.0",
11
9
  "tslib": "1.14.1"
12
10
  },
13
11
  "main": "./src/index.js"
@@ -0,0 +1,8 @@
1
+ export declare const PUBLISHER_LOGGER: {
2
+ log: (...args: unknown[]) => void;
3
+ warn: (...args: unknown[]) => void;
4
+ error: (...args: unknown[]) => void;
5
+ };
6
+ export declare const PERFORMANCE_LOGGER: {
7
+ log: (...args: unknown[]) => void;
8
+ };
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PERFORMANCE_LOGGER = exports.PUBLISHER_LOGGER = void 0;
4
+ exports.PUBLISHER_LOGGER = {
5
+ log: (...args) => {
6
+ if (process.env['DEBUG_LOGGING_ENABLED'] === 'TRUE') {
7
+ console.log(...args);
8
+ }
9
+ },
10
+ warn: (...args) => {
11
+ console.warn(...args);
12
+ },
13
+ error: (...args) => {
14
+ console.warn(...args);
15
+ },
16
+ };
17
+ exports.PERFORMANCE_LOGGER = {
18
+ log: (...args) => {
19
+ if (process.env['PERFORMANCE_LOGGING_ENABLED'] !== 'FALSE') {
20
+ console.log(...args);
21
+ }
22
+ },
23
+ };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setRedisConnectionSettings = exports.RedisRegistry = void 0;
4
4
  const ioredis_1 = require("ioredis");
5
+ const logger_1 = require("./logger");
5
6
  class RedisRegistry {
6
7
  static attemptConnection(connectionKey, storeRef = 0) {
7
8
  let ref;
@@ -25,8 +26,9 @@ class RedisRegistry {
25
26
  }
26
27
  static handleDisconnects(connection, connectionKey, storeRef) {
27
28
  connection.on('error', (error) => {
28
- console.error(`PUBLISHER: Redis connection error : ${error.message}`);
29
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Redis connection error : ${error.message}`);
29
30
  connection.removeAllListeners();
31
+ connection.disconnect();
30
32
  RedisRegistry.attemptConnection(connectionKey, storeRef);
31
33
  });
32
34
  }
@@ -42,7 +44,7 @@ class RedisRegistry {
42
44
  ]);
43
45
  if (res === 'NO_PING') {
44
46
  connection.disconnect(true);
45
- console.error('PUBLISHER: failed to ping redis, disconnecting and restarting service.');
47
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: failed to ping redis, disconnecting and restarting service.');
46
48
  process.exit(0);
47
49
  }
48
50
  }, 2000);
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ScheduledProcessor = void 0;
4
4
  const id_1 = require("@jetit/id");
5
5
  const rxjs_1 = require("rxjs");
6
+ const logger_1 = require("./logger");
6
7
  const registry_1 = require("./registry");
7
8
  const utils_1 = require("./utils");
8
9
  /**
@@ -18,27 +19,27 @@ class ScheduledProcessor {
18
19
  constructor(duration = 1000) {
19
20
  this.previousTaskCompleted = true;
20
21
  this.scheduledMessagesTimer = (0, rxjs_1.interval)(duration).subscribe(() => {
21
- console.log('Checking Streams messages at ', new Date().toISOString(), '...');
22
+ logger_1.PUBLISHER_LOGGER.log('Checking Streams messages at ', new Date().toISOString(), '...');
22
23
  /** Do not run scheduler if the previous run is not completed */
23
24
  if (this.previousTaskCompleted) {
24
25
  this.previousTaskCompleted = false;
25
26
  this.processScheduledEvents()
26
27
  .catch((error) => {
27
- console.error('Error while processing scheduled events:', error);
28
+ logger_1.PUBLISHER_LOGGER.error('Error while processing scheduled events:', error);
28
29
  })
29
30
  .then(() => {
30
31
  this.previousTaskCompleted = true;
31
32
  });
32
33
  }
33
34
  else {
34
- console.log('Skipping current scheduler run because previous run is in progress');
35
+ logger_1.PUBLISHER_LOGGER.log('Skipping current scheduler run because previous run is in progress');
35
36
  }
36
37
  });
37
38
  }
38
39
  async processScheduledEvents() {
39
40
  const currentTime = new Date().getTime();
40
41
  const events = await this.redisPublisher.zrangebyscore('se', 0, currentTime);
41
- console.log('Events to process:', events.length);
42
+ logger_1.PUBLISHER_LOGGER.log('Events to process:', events.length);
42
43
  for (const eventString of events) {
43
44
  const eventData = (0, utils_1.decodeScheduledMessage)(eventString);
44
45
  /**
@@ -52,14 +53,14 @@ class ScheduledProcessor {
52
53
  eventData.eventId = (0, id_1.generateID)('HEX', 'FF');
53
54
  await this.redisPublisher.zrem('se', eventString);
54
55
  const consumerGroups = await (0, utils_1.getAllConsumerGroups)(eventData.eventName, this.redisPublisher);
55
- console.log('Scheduled Publishing to consumer groups: ', consumerGroups, 'with id ', eventData.eventId, '...');
56
+ logger_1.PUBLISHER_LOGGER.log('Scheduled Publishing to consumer groups: ', consumerGroups, 'with id ', eventData.eventId, '...');
56
57
  let key = '*';
57
58
  for (const consumerGroup of consumerGroups) {
58
59
  // Publish the event to each consumer group's stream
59
60
  const streamName = `${eventData.eventName}:${consumerGroup}`;
60
61
  const generatedKey = await this.redisPublisher
61
62
  .xadd(streamName, '*', 'data', JSON.stringify(eventData))
62
- .catch((e) => console.error(`PUBLISHER: Publishing event ${eventData.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(eventData)}, ${e} `));
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} `));
63
64
  if (key === '*')
64
65
  key = generatedKey ?? key;
65
66
  }
@@ -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>): 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.
@@ -67,7 +68,7 @@ export declare class Streams {
67
68
  *
68
69
  * await streams.scheduledPublish(futureTime, eventData);
69
70
  */
70
- scheduledPublish<TData = unknown, TName extends string = string>(scheduledTime: Date, eventData: PublishData<TData, TName>, uniquePerInstance?: boolean, repeatInterval?: number): Promise<void>;
71
+ scheduledPublish<TData = unknown, TName extends string = string>(scheduledTime: Date, eventData: PublishData<TData, TName>, uniquePerInstance?: boolean, repeatInterval?: number, multicast?: boolean): Promise<void>;
71
72
  /**
72
73
  * Listens for events with the given name and returns an Observable that emits an EventData<T> object
73
74
  * each time a new event is received.
@@ -75,7 +76,7 @@ export declare class Streams {
75
76
  * The method uses a BehaviorSubject to emit the events as Observables. The BehaviorSubject ensures
76
77
  * that new subscribers receive the last emitted event, even if they subscribe after the event has been emitted.
77
78
  *
78
- * If an error occurs while subscribing, the method logs the error to the console and throws
79
+ * If an error occurs while subscribing, the method logs the error to the PUBLISHER_LOGGER and throws
79
80
  * an error. This is done to prevent the service from continuing without a proper event subscription.
80
81
  *
81
82
  * There is retry logic with exponential backoff to handle error cases. These are also controllable by the
@@ -92,7 +93,7 @@ export declare class Streams {
92
93
  *
93
94
  * // Subscribe to the Observable and log each new event
94
95
  * orderCreated.subscribe((event) => {
95
- * console.log('New order created:', event.data);
96
+ * PUBLISHER_LOGGER.log('New order created:', event.data);
96
97
  * });
97
98
  */
98
99
  listen<T = unknown, const TName extends string = string>(eventName: TName, maxRetries?: number, initialDelay?: number): Observable<EventData<T, TName>>;
@@ -108,16 +109,26 @@ export declare class Streams {
108
109
  * process.on('SIGINT', shutdown);
109
110
  *
110
111
  * async function shutdown(): Promise<void> {
111
- * console.log('Graceful shutdown initiated.');
112
+ * PUBLISHER_LOGGER.log('Graceful shutdown initiated.');
112
113
  * try {
113
114
  * await streams.close();
114
- * console.log('Resources and connections successfully closed.');
115
+ * PUBLISHER_LOGGER.log('Resources and connections successfully closed.');
115
116
  * } catch (error) {
116
- * console.error('Error during graceful shutdown:', error);
117
+ * PUBLISHER_LOGGER.error('Error during graceful shutdown:', error);
117
118
  * }
118
119
  * process.exit(0);
119
120
  * }
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
  }
@@ -3,10 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Streams = void 0;
4
4
  const id_1 = require("@jetit/id");
5
5
  const rxjs_1 = require("rxjs");
6
+ const logger_1 = require("./logger");
6
7
  const registry_1 = require("./registry");
7
8
  const utils_1 = require("./utils");
8
9
  function publisherErrorHandler(error) {
9
- console.error('PUBLISHER UNHANDLED ERROR: ', JSON.stringify(error));
10
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER UNHANDLED ERROR: ', JSON.stringify(error));
10
11
  }
11
12
  class Streams {
12
13
  get redisPublisher() {
@@ -38,23 +39,24 @@ class Streams {
38
39
  this.instanceUniqueId = process.env['INSTANCE_ID'] ?? (0, id_1.generateID)('HEX', 'FE');
39
40
  this.instanceId = `${serviceName}:${this.instanceUniqueId}`;
40
41
  this.consumerGroupName = `cg-${serviceName}`;
41
- console.log(`PUBLISHER: Instance ID: ${this.instanceId}`);
42
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Instance ID: ${this.instanceId}`);
42
43
  const cleanUpInterval = parseInt(process.env['CLEANUP_INTERVAL'] || `${1000 * 60 * 60}`, 10) ?? 1000 * 60 * 60;
43
44
  this.cleanUpTimer = setInterval(() => {
44
45
  this.runClear(cleanUpInterval);
45
46
  }, cleanUpInterval);
46
47
  }
47
48
  async runClear(cleanUpInterval) {
48
- console.log('PUBLISHER: Running Clearance', this.eventsListened);
49
+ logger_1.PUBLISHER_LOGGER.log('PUBLISHER: Running Clearance', this.eventsListened);
49
50
  for (const eventName of this.eventsListened) {
50
51
  process.nextTick(async () => {
51
52
  /** This removes the messages from the stream after they have been processed according to cleanup interval */
52
53
  await this.cleanupAcknowledgedMessages(eventName, cleanUpInterval).catch(publisherErrorHandler);
53
- console.log(`Cleanup process for Acknowledged messages completed for ${eventName}`);
54
+ logger_1.PUBLISHER_LOGGER.log(`Cleanup process for Acknowledged messages completed for ${eventName}`);
54
55
  });
55
56
  }
56
57
  }
57
- async publish(data) {
58
+ async publish(data, multicast = false) {
59
+ const publishStartTime = process.hrtime();
58
60
  if (data.eventId)
59
61
  data.republishEvent = data.eventId;
60
62
  data.eventId = (0, id_1.generateID)('HEX', 'FF');
@@ -63,28 +65,32 @@ class Streams {
63
65
  const consumerGroups = await (0, utils_1.getAllConsumerGroups)(data.eventName, this.redisPublisher);
64
66
  let key = '*';
65
67
  if (consumerGroups.length > 0) {
66
- console.log(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')}`);
68
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')}`);
67
69
  try {
68
70
  for (const consumerGroup of consumerGroups) {
69
71
  // Publish the event to each consumer group's stream
70
72
  const streamName = `${data.eventName}:${consumerGroup}`;
71
73
  const generatedKey = await this.redisPublisher
72
74
  .xadd(streamName, key, 'data', JSON.stringify(data))
73
- .catch((e) => console.error(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(data)}, ${e} `));
75
+ .catch((e) => logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(data)}, ${e} `));
74
76
  if (key === '*')
75
77
  key = generatedKey ?? key;
76
78
  }
77
- await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key);
79
+ await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key, multicast);
78
80
  }
79
81
  catch (error) {
80
- console.error(`PUBLISHER: Error while publishing event for service ${this.consumerGroupName} with instance ${this.instanceId}: `, error);
82
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error while publishing event for service ${this.consumerGroupName} with instance ${this.instanceId}: `, error);
81
83
  throw new Error('Publisher Error');
82
84
  }
83
85
  }
84
86
  else
85
- console.log(`PUBLISHER: Event publish failed for event ${data.eventName}, reason: no consumers ${consumerGroups}`);
87
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Event publish failed for event ${data.eventName}, reason: no consumers ${consumerGroups}`);
88
+ const publishEndTime = process.hrtime(publishStartTime);
89
+ const elapsedTime = publishEndTime[0] * 1000 + publishEndTime[1] / 1000000;
90
+ logger_1.PERFORMANCE_LOGGER.log(`PTIME;${key};${data.eventName};${Date.now()};${elapsedTime}`);
91
+ return key;
86
92
  }
87
- async scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0) {
93
+ async scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0, multicast = false) {
88
94
  const currentTime = new Date();
89
95
  delete eventData.repeatInterval;
90
96
  if (repeatInterval > 0) {
@@ -94,14 +100,14 @@ class Streams {
94
100
  throw new Error('PUBLISHER: Cannot schedule an event in the past');
95
101
  }
96
102
  else if (Math.abs(scheduledTime.getTime() - currentTime.getTime()) <= 500) {
97
- await this.publish(eventData);
103
+ await this.publish(eventData, multicast);
98
104
  }
99
105
  else {
100
106
  const key = (0, utils_1.encodeScheduledMessage)(eventData);
101
107
  if (uniquePerInstance === true) {
102
108
  const existingJob = await this.redisPublisher.zscore('se', key);
103
109
  if (existingJob) {
104
- console.log(`PUBLISHER: Job with data '${eventData}' already exists. Skipping.`);
110
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Job with data '${eventData}' already exists. Skipping.`);
105
111
  return;
106
112
  }
107
113
  }
@@ -115,7 +121,7 @@ class Streams {
115
121
  * The method uses a BehaviorSubject to emit the events as Observables. The BehaviorSubject ensures
116
122
  * that new subscribers receive the last emitted event, even if they subscribe after the event has been emitted.
117
123
  *
118
- * If an error occurs while subscribing, the method logs the error to the console and throws
124
+ * If an error occurs while subscribing, the method logs the error to the PUBLISHER_LOGGER and throws
119
125
  * an error. This is done to prevent the service from continuing without a proper event subscription.
120
126
  *
121
127
  * There is retry logic with exponential backoff to handle error cases. These are also controllable by the
@@ -132,7 +138,7 @@ class Streams {
132
138
  *
133
139
  * // Subscribe to the Observable and log each new event
134
140
  * orderCreated.subscribe((event) => {
135
- * console.log('New order created:', event.data);
141
+ * PUBLISHER_LOGGER.log('New order created:', event.data);
136
142
  * });
137
143
  */
138
144
  listen(eventName, maxRetries = 5, initialDelay = 1000) {
@@ -140,11 +146,11 @@ class Streams {
140
146
  count: maxRetries,
141
147
  delay: (error, retryAttempt) => {
142
148
  const delay = initialDelay * Math.pow(2, retryAttempt);
143
- console.error(`PUBLISHER: Error in listen: ${error.message}. Retrying in ${delay}ms (attempt ${retryAttempt + 1})`);
149
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error in listen: ${error.message}. Retrying in ${delay}ms (attempt ${retryAttempt + 1})`);
144
150
  return (0, rxjs_1.timer)(delay);
145
151
  },
146
152
  }), (0, rxjs_1.catchError)((error) => {
147
- console.error(`PUBLISHER: Error in listen after ${maxRetries} retries: ${error.message}`);
153
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error in listen after ${maxRetries} retries: ${error.message}`);
148
154
  return (0, rxjs_1.throwError)(() => new Error(error.message));
149
155
  }));
150
156
  }
@@ -156,61 +162,88 @@ class Streams {
156
162
  await this.redisGroups
157
163
  .xgroup('CREATE', streamName, this.consumerGroupName, '0', 'MKSTREAM')
158
164
  .then(() => {
159
- console.log(`Group created created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
165
+ logger_1.PUBLISHER_LOGGER.log(`Group created created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
160
166
  })
161
167
  .catch((e) => {
162
- console.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
168
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
163
169
  });
164
170
  const createConsumerStatus = (await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId));
165
171
  await this.redisGroups.sadd(key, eventName);
166
172
  const addToCGSet = await this.redisGroups.sadd(`${eventName}`, this.consumerGroupName);
167
173
  const addToFlushSet = await this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName);
168
- console.log(`PUBLISHER: Consumer Registered and created with ${this.instanceId} under ${this.consumerGroupName} with ${createConsumerStatus} consumers and with the following status ${JSON.stringify({ addToCGSet, addToFlushSet })}`);
174
+ 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 })}`);
169
175
  return createConsumerStatus === 0 || createConsumerStatus === 1;
170
176
  }
171
177
  listenInternals(eventName) {
172
178
  const bs = new rxjs_1.BehaviorSubject(null);
173
- 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
+ }));
174
187
  const streamName = `${eventName}:${this.consumerGroupName}`;
175
- const processMessage = async (redisClient, messageId, processPending = false) => {
176
- console.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
188
+ const processMessage = async (redisClient, messageId, multicast = false, processPending = false) => {
189
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
177
190
  try {
178
191
  try {
179
- const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1, this.instanceId);
180
- if (pendingDetails[2] === 0) {
181
- console.warn(`PUBLISHER: Message ${messageId} for ${streamName} already acknowledged.`);
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);
196
+ if (pendingDetails[2] === 0 && multicast === false) {
197
+ logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: MACK ${messageId} for ${streamName}`);
182
198
  return;
183
199
  }
184
200
  }
185
201
  catch (e) {
186
202
  // Ignore the xpending error and continue
187
- console.error('XPENDING ERROR: To be handled');
188
- console.warn(JSON.stringify(e));
203
+ logger_1.PUBLISHER_LOGGER.error('XPENDING ERROR: To be handled');
204
+ logger_1.PUBLISHER_LOGGER.warn(JSON.stringify(e));
205
+ }
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
+ }
189
218
  }
190
- const messages = await redisClient.xrange(streamName, messageId, messageId);
191
- if (messages && messages.length) {
192
- const eventData = JSON.parse(messages[0][1][1]);
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) {
193
226
  bs.next(eventData);
194
227
  await redisClient.xack(streamName, this.consumerGroupName, messageId);
195
228
  await redisClient.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
196
229
  }
197
230
  else {
198
- console.warn(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
231
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
199
232
  }
200
233
  /** Process Unprocessed Message if this is a main tree, otherwise limit to processing 100 messages that are unacknowledged */
201
234
  if (!processPending) {
202
- const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, 25);
203
- if (unprocessedMessageIds.count > 25) {
204
- console.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
235
+ const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, this.instanceId);
236
+ if (unprocessedMessageIds.countOnThisConsumer && unprocessedMessageIds.countOnThisConsumer > 25) {
237
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
205
238
  }
206
239
  for (const id of unprocessedMessageIds.messageIds) {
207
- console.log(`PUBLISHER: Reporcessing unprocessed message with id: ${id}`);
208
- await processMessage(redisClient, id, true);
240
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Reporcessing unprocessed message with id: ${id}`);
241
+ await processMessage(redisClient, id, multicast, true);
209
242
  }
210
243
  }
211
244
  }
212
245
  catch (e) {
213
- console.error(`PUBLISHER: Error processing message ${messageId} for ${streamName}`, e);
246
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error processing message ${messageId} for ${streamName}`, e);
214
247
  }
215
248
  };
216
249
  /** Register the consumer and setup the Observable */
@@ -220,15 +253,20 @@ class Streams {
220
253
  throw new Error('PUBLISHER: Cannot setup consumer');
221
254
  const eventStreamClient = registry_1.RedisRegistry.getConnection(`sub-${eventName}`);
222
255
  eventStreamClient.subscribe(eventName).then(() => {
223
- console.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName}`);
256
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName}`);
224
257
  });
225
- eventStreamClient.on('message', async (channel, messageId) => {
226
- console.log(`PUBLISHER: Stream Notification Received for event ${eventName} with message ID ${messageId}`);
227
- await processMessage(this.redisGroups, messageId);
258
+ eventStreamClient.on('message', async (channel, data) => {
259
+ const subscribeStartTime = process.hrtime();
260
+ const { messageId, multicast } = JSON.parse(data);
261
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Stream Notification Received for event ${eventName} with message ID ${messageId}`);
262
+ await processMessage(this.redisGroups, messageId, multicast);
263
+ const subscribendTime = process.hrtime(subscribeStartTime);
264
+ const elapsedTime = subscribendTime[0] * 1000 + subscribendTime[1] / 1000000;
265
+ logger_1.PERFORMANCE_LOGGER.log(`STIME;${messageId};${data.eventName};${Date.now()};${elapsedTime}`);
228
266
  });
229
267
  })
230
268
  .catch((e) => {
231
- console.error(`PUBLISHER: Error during consumer registration for ${eventName}`, e);
269
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error during consumer registration for ${eventName}`, e);
232
270
  });
233
271
  return observable;
234
272
  }
@@ -242,12 +280,12 @@ class Streams {
242
280
  * process.on('SIGINT', shutdown);
243
281
  *
244
282
  * async function shutdown(): Promise<void> {
245
- * console.log('Graceful shutdown initiated.');
283
+ * PUBLISHER_LOGGER.log('Graceful shutdown initiated.');
246
284
  * try {
247
285
  * await streams.close();
248
- * console.log('Resources and connections successfully closed.');
286
+ * PUBLISHER_LOGGER.log('Resources and connections successfully closed.');
249
287
  * } catch (error) {
250
- * console.error('Error during graceful shutdown:', error);
288
+ * PUBLISHER_LOGGER.error('Error during graceful shutdown:', error);
251
289
  * }
252
290
  * process.exit(0);
253
291
  * }
@@ -279,5 +317,14 @@ class Streams {
279
317
  await this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
280
318
  }
281
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
+ }
282
329
  }
283
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
  }>;
@@ -10,7 +11,7 @@ export declare function getMessageStatesCount(redisClient: RedisType, streamName
10
11
  acknowledged: number;
11
12
  unacknowledged: number;
12
13
  }>;
13
- export declare function notifySubscribers(redisClient: RedisType, eventName: string, messageId: string): Promise<void>;
14
+ export declare function notifySubscribers(redisClient: RedisType, eventName: string, messageId: string, multicast?: boolean): Promise<void>;
14
15
  export declare function removedScheduledJob(redisClient: RedisType, eventString: string): Promise<void>;
15
16
  export declare function encodeScheduledMessage<TData, TName extends string>(data: EventData<TData, TName>): string;
16
17
  export declare function decodeScheduledMessage(data: string): EventData<never, never>;
@@ -1,31 +1,38 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UTILS = exports.decodeScheduledMessage = exports.encodeScheduledMessage = exports.removedScheduledJob = exports.notifySubscribers = exports.getMessageStatesCount = exports.getUnacknowledgedMessages = exports.getAllConsumerGroups = void 0;
4
+ const logger_1 = require("./logger");
4
5
  async function getAllConsumerGroups(eventName, redisConnection) {
5
6
  const consumerGroups = await redisConnection.smembers(`${eventName}`);
6
7
  return consumerGroups;
7
8
  }
8
9
  exports.getAllConsumerGroups = getAllConsumerGroups;
9
- async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, count = 500) {
10
+ async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, consumerName, count = 500) {
10
11
  try {
11
12
  // Get pending messages summary
12
13
  const summary = await redisClient.xpending(streamName, consumerGroupName);
13
- if (!summary || summary[1] === 0) {
14
+ if (!summary || summary[0] === 0) {
14
15
  // If count is zero
15
16
  return { count: 0, messageIds: [] };
16
17
  }
17
18
  // Use the smallest and largest IDs to get a detailed range
18
- const pendingMessageCount = summary[1];
19
+ const pendingMessageCount = summary[0];
19
20
  // Get detailed information in the range
20
- 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
+ }
21
27
  return {
22
28
  count: pendingMessageCount,
29
+ countOnThisConsumer: pendingMessages.length,
23
30
  messageIds: pendingMessages.map((message) => message[0]),
24
31
  messages: pendingMessages,
25
32
  };
26
33
  }
27
34
  catch (error) {
28
- console.error(`PUBLISHER: Error fetching unacknowledged messages for ${streamName}`, error);
35
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error fetching unacknowledged messages for ${streamName}`, error);
29
36
  return { count: 0, messageIds: [] };
30
37
  }
31
38
  }
@@ -40,22 +47,22 @@ async function getMessageStatesCount(redisClient, streamName, consumerGroup) {
40
47
  };
41
48
  }
42
49
  catch (error) {
43
- console.error(`PUBLISHER: Error fetching message states count for ${streamName}`, error);
50
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Error fetching message states count for ${streamName}`, error);
44
51
  return { acknowledged: 0, unacknowledged: 0 };
45
52
  }
46
53
  }
47
54
  exports.getMessageStatesCount = getMessageStatesCount;
48
- async function notifySubscribers(redisClient, eventName, messageId) {
49
- await redisClient.publish(eventName, messageId);
55
+ async function notifySubscribers(redisClient, eventName, messageId, multicast = false) {
56
+ await redisClient.publish(eventName, JSON.stringify({ messageId, multicast }));
50
57
  }
51
58
  exports.notifySubscribers = notifySubscribers;
52
59
  async function removedScheduledJob(redisClient, eventString) {
53
60
  const currentTime = new Date().getTime();
54
61
  const events = await redisClient.zrangebyscore('se', 0, currentTime);
55
- console.log(`Total Events in scheduled queue: ${events.length}`);
62
+ logger_1.PUBLISHER_LOGGER.log(`Total Events in scheduled queue: ${events.length}`);
56
63
  await redisClient.zrem('se', eventString);
57
64
  const eventsLater = await redisClient.zrangebyscore('se', 0, currentTime);
58
- console.log(`Total Events in scheduled queue: ${eventsLater.length}`);
65
+ logger_1.PUBLISHER_LOGGER.log(`Total Events in scheduled queue: ${eventsLater.length}`);
59
66
  }
60
67
  exports.removedScheduledJob = removedScheduledJob;
61
68
  function encodeScheduledMessage(data) {