@jetit/publisher 5.2.1 → 5.3.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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "5.2.1",
3
+ "version": "5.3.2",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "^0.0.12",
7
- "events": "3.3.0",
8
7
  "ioredis": "^5.3.0",
9
8
  "rxjs": "^7.8.0",
10
- "tslib": "2.5.0"
9
+ "tslib": "2.7.0"
11
10
  },
11
+ "types": "./src/index.d.ts",
12
12
  "main": "./src/index.js"
13
- }
13
+ }
@@ -111,33 +111,26 @@ class MetricsCollector {
111
111
  const individualDepths = {};
112
112
  let cursor = '0';
113
113
  do {
114
- const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', `ack:*:cg-*`, 'COUNT', 100);
114
+ const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', '*:cg-*', 'COUNT', 100);
115
115
  cursor = nextCursor;
116
116
  if (keys.length > 0) {
117
- const pipeline = this.redisClient.pipeline();
118
- keys.forEach((key) => {
119
- pipeline.xlen(key.slice(4));
120
- pipeline.zcard(key);
121
- });
122
- const results = await pipeline.exec();
123
- if (!results) {
124
- continue;
125
- }
126
- for (let i = 0; i < results.length; i += 2) {
127
- const key = keys[i / 2];
128
- const [streamLengthErr, streamLength] = results[i];
129
- const [ackCountErr, ackCount] = results[i + 1];
130
- if (streamLengthErr) {
131
- console.error(`Error getting length for key: ${streamLengthErr}`);
132
- continue;
117
+ for (const streamKey of keys) {
118
+ try {
119
+ // Get stream length and pending info
120
+ const streamLength = await this.redisClient.xlen(streamKey);
121
+ // Extract consumer group name from stream key (format: eventName:cg-serviceName)
122
+ const consumerGroup = streamKey.split(':')[1];
123
+ // XPENDING returns [count, min-id, max-id, consumer-list]
124
+ const pendingInfo = await this.redisClient.xpending(streamKey, consumerGroup);
125
+ const totalPending = pendingInfo ? Number(pendingInfo[0]) : 0;
126
+ const queueDepth = Math.max(0, streamLength - totalPending);
127
+ totalDepth += queueDepth;
128
+ individualDepths[streamKey] = queueDepth;
133
129
  }
134
- if (ackCountErr) {
135
- console.error(`Error getting ack count for key: ${ackCountErr}`);
130
+ catch (error) {
131
+ console.error(`Error processing key ${streamKey}:`, error);
136
132
  continue;
137
133
  }
138
- const queueDepth = Math.max(0, streamLength - ackCount);
139
- totalDepth += queueDepth;
140
- individualDepths[key] = queueDepth;
141
134
  }
142
135
  }
143
136
  } while (cursor !== '0');
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { EventEmitter } from 'events';
3
2
  import { RedisType } from '../redis/registry';
4
3
  import { EventData, IStreamsConfig } from '../redis/types';
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.publishScheduledBatch = exports.publishBatch = void 0;
3
+ exports.publishBatch = publishBatch;
4
+ exports.publishScheduledBatch = publishScheduledBatch;
4
5
  const id_1 = require("@jetit/id");
5
6
  const tracker_1 = require("../monitoring/tracker");
6
7
  const logger_1 = require("./logger");
@@ -72,8 +73,6 @@ async function publishBatchWithRetry(streams, events, options) {
72
73
  function publishBatch(streams, events, options = {}) {
73
74
  return publishBatchWithRetry(streams, events, options);
74
75
  }
75
- exports.publishBatch = publishBatch;
76
76
  function publishScheduledBatch(streams, events, options) {
77
77
  return publishBatchWithRetry(streams, events, options);
78
78
  }
79
- exports.publishScheduledBatch = publishScheduledBatch;
@@ -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
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setRedisConnectionSettings = exports.RedisRegistry = void 0;
3
+ exports.RedisRegistry = void 0;
4
+ exports.setRedisConnectionSettings = setRedisConnectionSettings;
4
5
  const ioredis_1 = require("ioredis");
5
6
  const logger_1 = require("./logger");
6
7
  class RedisRegistry {
@@ -103,4 +104,3 @@ RedisRegistry.options = {
103
104
  function setRedisConnectionSettings(options) {
104
105
  RedisRegistry.setOptions(options);
105
106
  }
106
- exports.setRedisConnectionSettings = setRedisConnectionSettings;
@@ -257,60 +257,40 @@ class Streams {
257
257
  const setKeyForK8sHandling = `instance:${this.instanceUniqueId}:consumerGroupName`;
258
258
  this.eventsListened.push(eventName);
259
259
  try {
260
- // Check if the consumer group already exists
261
- let groupInfo = [];
260
+ // Try to create the consumer group and consumer in one go
261
+ // If group doesn't exist, this will create it with MKSTREAM
262
+ // If group exists but consumer doesn't, this will create just the consumer
263
+ // If both exist, this will be a no-op
262
264
  try {
263
- groupInfo = (await this.redisGroups.xinfo('GROUPS', streamName));
264
- }
265
- catch (e) {
266
- // Do nothing
267
- }
268
- let groupExists = false;
269
- for (const group of groupInfo) {
270
- if (group[1] === this.consumerGroupName) {
271
- groupExists = true;
272
- break;
273
- }
274
- }
275
- if (!groupExists) {
276
265
  await this.redisGroups.xgroup('CREATE', streamName, this.consumerGroupName, '0', 'MKSTREAM');
277
266
  logger_1.PUBLISHER_LOGGER.log(`Group created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
278
267
  }
279
- else {
268
+ catch (e) {
269
+ // BUSYGROUP error means group already exists, which is fine
270
+ if (!e.message.includes('BUSYGROUP')) {
271
+ throw e;
272
+ }
280
273
  logger_1.PUBLISHER_LOGGER.log(`Group already exists for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
281
274
  }
275
+ // Create consumer (idempotent operation)
276
+ const createConsumerStatus = await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId);
277
+ // Register event and consumer group in parallel
278
+ const [addToCGSet, addToFlushSet] = await Promise.all([
279
+ this.redisGroups.sadd(`${eventName}`, this.consumerGroupName),
280
+ this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName),
281
+ this.redisGroups.sadd(key, eventName),
282
+ ]);
283
+ logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Consumer Registered with ${this.instanceId} under ${this.consumerGroupName} with status ${JSON.stringify({
284
+ createConsumerStatus,
285
+ addToCGSet,
286
+ addToFlushSet,
287
+ })}`);
288
+ return true;
282
289
  }
283
- catch (e) {
284
- logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
285
- }
286
- // Check if the consumer already exists in the group
287
- let consumers = [];
288
- try {
289
- consumers = (await this.redisGroups.xinfo('CONSUMERS', streamName, this.consumerGroupName));
290
- }
291
- catch (e) {
292
- // Do nothing
293
- }
294
- let consumerExists = false;
295
- for (const consumer of consumers) {
296
- if (consumer[1] === this.instanceId) {
297
- consumerExists = true;
298
- break;
299
- }
300
- }
301
- let createConsumerStatus;
302
- if (!consumerExists) {
303
- createConsumerStatus = await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId);
304
- }
305
- else {
306
- createConsumerStatus = 0; // Consumer already exists
307
- logger_1.PUBLISHER_LOGGER.log(`Consumer already exists for ${JSON.stringify({ streamName, cgn: this.consumerGroupName, instanceId: this.instanceId })}`);
290
+ catch (error) {
291
+ logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Consumer registration failed for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}:`, error);
292
+ return false;
308
293
  }
309
- await this.redisGroups.sadd(key, eventName);
310
- const addToCGSet = await this.redisGroups.sadd(`${eventName}`, this.consumerGroupName);
311
- const addToFlushSet = await this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName);
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 })}`);
313
- return createConsumerStatus === 0 || createConsumerStatus === 1;
314
294
  }
315
295
  listenInternals(eventName, subscriptionId, eventFilter, filterKeepAlive = 24 * 60 * 60 * 1000, publishOnceGuarantee = false, externalAcknowledgement = false) {
316
296
  if (!this.subscriptions.has(eventName)) {
@@ -336,21 +316,22 @@ class Streams {
336
316
  const processMessage = async (redisClient, messageId, tracker, multicast = false, processPending = false) => {
337
317
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
338
318
  try {
339
- try {
340
- /**
341
- * Check if the message is already acquired by another client and is pending.
342
- * This check should only happen if the message is not a multicast message
343
- */
344
- const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1);
345
- if (pendingDetails[2] === 0 && !multicast) {
346
- logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: MACK ${messageId} for ${streamName}`);
347
- return;
319
+ // Skip XPENDING check for:
320
+ // 1. Multicast messages (meant for all consumers)
321
+ // 2. Initial message processing (messageId = '0')
322
+ // 3. Pending message reprocessing
323
+ if (!multicast && messageId !== '0' && !processPending) {
324
+ try {
325
+ const pendingDetails = await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1);
326
+ if (pendingDetails[2] === 0) {
327
+ logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: Message ${messageId} already processed for ${streamName}`);
328
+ return;
329
+ }
330
+ }
331
+ catch (e) {
332
+ // If XPENDING fails, continue with processing
333
+ logger_1.PUBLISHER_LOGGER.warn(`PUBLISHER: XPENDING check failed for ${messageId}, continuing with processing`);
348
334
  }
349
- }
350
- catch (e) {
351
- // Ignore the xpending error and continue
352
- logger_1.PUBLISHER_LOGGER.error('XPENDING ERROR: To be handled');
353
- logger_1.PUBLISHER_LOGGER.warn(JSON.stringify(e));
354
335
  }
355
336
  let eventData;
356
337
  /**
@@ -416,7 +397,7 @@ class Streams {
416
397
  }
417
398
  }
418
399
  if (!externalAcknowledgement)
419
- this.acknowledgeMessage(ackKey);
400
+ await this.acknowledgeMessage(ackKey);
420
401
  }
421
402
  catch (processingError) {
422
403
  logger_1.PUBLISHER_LOGGER.error(`Processing error for message ${messageId}:`, processingError);
@@ -551,16 +532,40 @@ class Streams {
551
532
  async cleanupAcknowledgedMessages(eventName, interval = this.config.acknowledgedMessageCleanupInterval) {
552
533
  const streamName = `${eventName}:${this.consumerGroupName}`;
553
534
  const cleanupThreshold = Date.now() - interval;
554
- const acknowledgedMessages = await this.redisGroups.zrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
555
- if (acknowledgedMessages && acknowledgedMessages.length > 0) {
556
- // Remove acknowledged messages from the stream in batches
557
- const batchSize = 100;
558
- for (let i = 0; i < acknowledgedMessages.length; i += batchSize) {
559
- const batch = acknowledgedMessages.slice(i, i + batchSize);
560
- await this.redisGroups.xdel(streamName, ...batch);
535
+ const CHUNK_SIZE = 10000; // Process messages in chunks to avoid memory issues
536
+ try {
537
+ // Get pending info for this consumer group once
538
+ const pendingInfo = await this.redisGroups.xpending(streamName, this.consumerGroupName);
539
+ const hasPendingMessages = pendingInfo && pendingInfo[0] > 0;
540
+ let lastId = '-'; // Start from the beginning of the stream
541
+ while (true) {
542
+ // Get a chunk of messages
543
+ const messages = await this.redisGroups.xrange(streamName, lastId, cleanupThreshold.toString(), 'COUNT', CHUNK_SIZE);
544
+ if (!messages || messages.length === 0)
545
+ break;
546
+ // Update lastId for next iteration (exclusive)
547
+ lastId = messages[messages.length - 1][0];
548
+ if (hasPendingMessages) {
549
+ // If there are pending messages, we need to check each message
550
+ const pendingDetails = (await this.redisGroups.xpending(streamName, this.consumerGroupName, lastId, '+', CHUNK_SIZE, this.instanceId));
551
+ const pendingIds = new Set(pendingDetails.map((detail) => detail[0]));
552
+ const acknowledgedIds = messages.map((msg) => msg[0]).filter((id) => !pendingIds.has(id));
553
+ if (acknowledgedIds.length > 0) {
554
+ await this.redisGroups.xdel(streamName, ...acknowledgedIds);
555
+ }
556
+ }
557
+ else {
558
+ // If no pending messages, we can safely delete all messages in this chunk
559
+ const messageIds = messages.map((msg) => msg[0]);
560
+ await this.redisGroups.xdel(streamName, ...messageIds);
561
+ }
562
+ // If we got less than CHUNK_SIZE messages, we've reached the end
563
+ if (messages.length < CHUNK_SIZE)
564
+ break;
561
565
  }
562
- // Remove acknowledged messages from the Sorted Set
563
- await this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
566
+ }
567
+ catch (error) {
568
+ logger_1.PUBLISHER_LOGGER.error(`Error during cleanup for ${streamName}:`, error);
564
569
  }
565
570
  }
566
571
  async getDiagnosticData(events) {
@@ -676,7 +681,6 @@ class Streams {
676
681
  async acknowledgeMessage(ackKey) {
677
682
  const { streamName, messageId } = this.demergeMessageKey(ackKey);
678
683
  await this.redisGroups.xack(streamName, this.consumerGroupName, messageId);
679
- await this.redisGroups.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
680
684
  }
681
685
  frameMessageKey(streamName, messageId) {
682
686
  return `${streamName}##${messageId}`;
@@ -1,12 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UTILS = exports.decodeScheduledMessage = exports.encodeScheduledMessage = exports.removedScheduledJob = exports.notifySubscribers = exports.getMessageStatesCount = exports.getUnacknowledgedMessages = exports.getSummaryOnStreamConsumerGroup = exports.getAllConsumerGroups = void 0;
3
+ exports.UTILS = void 0;
4
+ exports.getAllConsumerGroups = getAllConsumerGroups;
5
+ exports.getSummaryOnStreamConsumerGroup = getSummaryOnStreamConsumerGroup;
6
+ exports.getUnacknowledgedMessages = getUnacknowledgedMessages;
7
+ exports.getMessageStatesCount = getMessageStatesCount;
8
+ exports.notifySubscribers = notifySubscribers;
9
+ exports.removedScheduledJob = removedScheduledJob;
10
+ exports.encodeScheduledMessage = encodeScheduledMessage;
11
+ exports.decodeScheduledMessage = decodeScheduledMessage;
4
12
  const logger_1 = require("./logger");
5
13
  async function getAllConsumerGroups(eventName, redisConnection) {
6
14
  const consumerGroups = await redisConnection.smembers(`${eventName}`);
7
15
  return consumerGroups;
8
16
  }
9
- exports.getAllConsumerGroups = getAllConsumerGroups;
10
17
  function* getSummaryOnStreamConsumerGroup(redisClient, consumerGroupName, streamName) {
11
18
  const [count, , , consumers] = (yield redisClient.xpending(streamName, consumerGroupName));
12
19
  yield {
@@ -21,7 +28,6 @@ function* getSummaryOnStreamConsumerGroup(redisClient, consumerGroupName, stream
21
28
  : [],
22
29
  };
23
30
  }
24
- exports.getSummaryOnStreamConsumerGroup = getSummaryOnStreamConsumerGroup;
25
31
  async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, consumerName, count = 500) {
26
32
  try {
27
33
  // Get pending messages summary
@@ -32,12 +38,16 @@ async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamN
32
38
  }
33
39
  // Use the smallest and largest IDs to get a detailed range
34
40
  const pendingMessageCount = summary[0];
35
- // Get detailed information in the range
36
- let pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count, consumerName));
41
+ // Create a message ID for 2 seconds ago to exclude recent messages
42
+ const oneSecondAgo = Date.now() - 2000;
43
+ const minId = '0-0'; // Start from beginning
44
+ const maxId = `${oneSecondAgo}-0`; // Only include messages older than 1 second
45
+ // Get detailed information in the range, excluding recent messages
46
+ let pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, minId, maxId, count, consumerName));
37
47
  /** If no pending messages on consumer, fetch messages from other consumers that haven't been claimed for more than 10s */
38
48
  if (count > pendingMessages.length && pendingMessages.length === 0) {
39
49
  await redisClient.xautoclaim(streamName, consumerGroupName, consumerName, 10000, '0-0', 'COUNT', 100);
40
- pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count, consumerName));
50
+ pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, minId, maxId, count, consumerName));
41
51
  }
42
52
  return {
43
53
  count: pendingMessageCount,
@@ -51,7 +61,6 @@ async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamN
51
61
  return { count: 0, messageIds: [] };
52
62
  }
53
63
  }
54
- exports.getUnacknowledgedMessages = getUnacknowledgedMessages;
55
64
  async function getMessageStatesCount(redisClient, streamName, consumerGroup) {
56
65
  try {
57
66
  const pendingInfo = (await redisClient.xpending(streamName, consumerGroup));
@@ -66,11 +75,9 @@ async function getMessageStatesCount(redisClient, streamName, consumerGroup) {
66
75
  return { acknowledged: 0, unacknowledged: 0 };
67
76
  }
68
77
  }
69
- exports.getMessageStatesCount = getMessageStatesCount;
70
78
  async function notifySubscribers(redisClient, eventName, messageId, multicast = false) {
71
79
  await redisClient.publish(eventName, JSON.stringify({ messageId, multicast }));
72
80
  }
73
- exports.notifySubscribers = notifySubscribers;
74
81
  async function removedScheduledJob(redisClient, eventString) {
75
82
  const currentTime = new Date().getTime();
76
83
  const events = await redisClient.zrangebyscore('se', 0, currentTime);
@@ -79,7 +86,6 @@ async function removedScheduledJob(redisClient, eventString) {
79
86
  const eventsLater = await redisClient.zrangebyscore('se', 0, currentTime);
80
87
  logger_1.PUBLISHER_LOGGER.log(`Total Events in scheduled queue: ${eventsLater.length}`);
81
88
  }
82
- exports.removedScheduledJob = removedScheduledJob;
83
89
  function encodeScheduledMessage(data) {
84
90
  const eventName = data.eventName;
85
91
  const eventData = JSON.stringify(data.data);
@@ -87,7 +93,6 @@ function encodeScheduledMessage(data) {
87
93
  const eventDataBuffer = Buffer.from(eventData, 'utf8').toString('base64');
88
94
  return `${eventName}%%${eventDataBuffer}%%${repeatInterval}`;
89
95
  }
90
- exports.encodeScheduledMessage = encodeScheduledMessage;
91
96
  function decodeScheduledMessage(data) {
92
97
  const parts = data.split('%%');
93
98
  const eventName = parts[0];
@@ -97,7 +102,6 @@ function decodeScheduledMessage(data) {
97
102
  repeatInterval: parseInt(parts[2]),
98
103
  };
99
104
  }
100
- exports.decodeScheduledMessage = decodeScheduledMessage;
101
105
  exports.UTILS = {
102
106
  getMessageStatesCount,
103
107
  getUnacknowledgedMessages,