@jetit/publisher 5.4.1 → 5.6.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
@@ -102,6 +102,16 @@ const config: Partial<IStreamsConfig> = {
102
102
  const publisher = new Publisher('MyService', config);
103
103
  ```
104
104
 
105
+ Additionally, the `Publisher` constructor accepts a redisConnectionId parameter, which is used to identify the connection used by the publisher. This is useful when using multiple connections in a environment.
106
+
107
+ ```typescript
108
+ setRedisConfig(options1, 'redis-connection-id');
109
+ const publisher1 = new Publisher('MyService', config, 'redis-connection-id'); // <-- use this connection (options1) for publishing
110
+
111
+ setRedisConfig(options2, 'another-redis-connection-id');
112
+ const publisher2 = new Publisher('MyService', config, 'another-redis-connection-id'); // <-- use this connection (options2) for publishing
113
+ ```
114
+
105
115
  ### Publishing Events
106
116
 
107
117
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "5.4.1",
3
+ "version": "5.6.0",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "^0.0.13",
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MetricsCollector = void 0;
4
+ const logger_1 = require("../redis/logger");
4
5
  class MetricsCollector {
5
6
  constructor(config, dlq) {
6
7
  this.metrics = [];
@@ -116,19 +117,34 @@ class MetricsCollector {
116
117
  if (keys.length > 0) {
117
118
  for (const streamKey of keys) {
118
119
  try {
120
+ // Verify if the key is a stream before proceeding
121
+ const keyType = await this.redisClient.type(streamKey);
122
+ if (keyType !== 'stream') {
123
+ continue; // Skip non-stream keys
124
+ }
119
125
  // Get stream length and pending info
120
126
  const streamLength = await this.redisClient.xlen(streamKey);
121
127
  // Extract consumer group name from stream key (format: eventName:cg-serviceName)
122
- const consumerGroup = streamKey.split(':')[1];
128
+ const [eventName, consumerGroup] = streamKey.split(':');
129
+ if (!consumerGroup?.startsWith('cg-')) {
130
+ continue; // Skip if key format doesn't match expected pattern
131
+ }
123
132
  // 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;
133
+ let totalPending = 0;
134
+ try {
135
+ const pendingInfo = await this.redisClient.xpending(streamKey, consumerGroup);
136
+ totalPending = pendingInfo ? Number(pendingInfo[0]) : 0;
137
+ }
138
+ catch (error) {
139
+ logger_1.PUBLISHER_LOGGER.error(`Error getting pending info for stream ${streamKey}:`, error);
140
+ // Continue with totalPending as 0 if XPENDING fails
141
+ }
126
142
  const queueDepth = Math.max(0, streamLength - totalPending);
127
143
  totalDepth += queueDepth;
128
144
  individualDepths[streamKey] = queueDepth;
129
145
  }
130
146
  catch (error) {
131
- console.error(`Error processing key ${streamKey}:`, error);
147
+ logger_1.PUBLISHER_LOGGER.error(`Error processing key ${streamKey}:`, error);
132
148
  continue;
133
149
  }
134
150
  }
@@ -4,4 +4,4 @@ export { ScheduledProcessor as __SCHEDULER_INTERNALS__ } from './redis/scheduler
4
4
  export { UTILS as StreamUtilityFunctions } from './redis/utils';
5
5
  export { PrometheusAdapter } from './monitoring/adapters/prom';
6
6
  export { publishBatch, publishScheduledBatch } from './redis/batch';
7
- export { IListenOptions, IStreamsConfig, TEventFilter } from './redis/types';
7
+ export type { EventData, IListenOptions, IOptions, IStreamsConfig, PublishData, TEventFilter } from './redis/types';
@@ -18,7 +18,11 @@ class DeadLetterQueue {
18
18
  return;
19
19
  }
20
20
  const eventJson = JSON.stringify(event);
21
- await this.redisClient.multi().hset(DLQ_HASH_KEY, event.eventId, eventJson).zadd(DLQ_ZSET_KEY, event.timestamp, event.eventId).exec();
21
+ // Execute commands separately to avoid CROSSSLOT error in cluster mode
22
+ await Promise.all([
23
+ this.redisClient.hset(DLQ_HASH_KEY, event.eventId, eventJson),
24
+ this.redisClient.zadd(DLQ_ZSET_KEY, event.timestamp, event.eventId),
25
+ ]);
22
26
  await this.incrementRateLimit();
23
27
  logger_1.PUBLISHER_LOGGER.log(`DLQ: Added event ${event.eventId} to Dead Letter Queue`);
24
28
  }
@@ -69,8 +73,12 @@ class DeadLetterQueue {
69
73
  }
70
74
  async removeFromDLQ(eventId) {
71
75
  try {
72
- const removed = await this.redisClient.multi().hdel(DLQ_HASH_KEY, eventId).zrem(DLQ_ZSET_KEY, eventId).exec();
73
- if (removed && removed[0][1] === 1 && removed[1][1] === 1) {
76
+ // Execute commands separately to avoid CROSSSLOT error in cluster mode
77
+ const [hdelResult, zremResult] = await Promise.all([
78
+ this.redisClient.hdel(DLQ_HASH_KEY, eventId),
79
+ this.redisClient.zrem(DLQ_ZSET_KEY, eventId),
80
+ ]);
81
+ if (hdelResult === 1 && zremResult === 1) {
74
82
  logger_1.PUBLISHER_LOGGER.log(`DLQ: Successfully removed event ${eventId} from Dead Letter Queue`);
75
83
  return true;
76
84
  }
@@ -90,11 +98,11 @@ class DeadLetterQueue {
90
98
  try {
91
99
  const expiredEventIds = await this.redisClient.zrangebyscore(DLQ_ZSET_KEY, 0, cutoffTime);
92
100
  if (expiredEventIds.length > 0) {
93
- await this.redisClient
94
- .multi()
95
- .hdel(DLQ_HASH_KEY, ...expiredEventIds)
96
- .zremrangebyscore(DLQ_ZSET_KEY, 0, cutoffTime)
97
- .exec();
101
+ // Execute commands separately to avoid CROSSSLOT error in cluster mode
102
+ await Promise.all([
103
+ this.redisClient.hdel(DLQ_HASH_KEY, ...expiredEventIds),
104
+ this.redisClient.zremrangebyscore(DLQ_ZSET_KEY, 0, cutoffTime),
105
+ ]);
98
106
  }
99
107
  logger_1.PUBLISHER_LOGGER.log(`DLQ: Cleaned up ${expiredEventIds.length} expired events from Dead Letter Queue`);
100
108
  }
@@ -104,16 +112,9 @@ class DeadLetterQueue {
104
112
  }
105
113
  async getDLQStats() {
106
114
  try {
107
- const results = await this.redisClient.multi().zcard(DLQ_ZSET_KEY).get(DLQ_RATE_LIMIT_KEY).exec();
108
- if (!results) {
109
- throw new Error('Failed to execute Redis commands');
110
- }
111
- const [sizeResult, additionRateResult] = results;
112
- if (sizeResult[0] || additionRateResult[0]) {
113
- throw new Error('Error executing Redis commands');
114
- }
115
- const size = sizeResult[1];
116
- const additionRate = parseInt(additionRateResult[1] || '0', 10);
115
+ // Execute commands separately to avoid CROSSSLOT error in cluster mode
116
+ const [size, additionRateStr] = await Promise.all([this.redisClient.zcard(DLQ_ZSET_KEY), this.redisClient.get(DLQ_RATE_LIMIT_KEY)]);
117
+ const additionRate = parseInt(additionRateStr || '0', 10);
117
118
  return { size, additionRate };
118
119
  }
119
120
  catch (error) {
@@ -126,8 +127,12 @@ class DeadLetterQueue {
126
127
  return parseInt(currentRate, 10) >= this.maxEventsThreshold;
127
128
  }
128
129
  async incrementRateLimit() {
129
- await this.redisClient.incr(DLQ_RATE_LIMIT_KEY);
130
- await this.redisClient.expire(DLQ_RATE_LIMIT_KEY, 60); // Reset rate limit after 1 minute
130
+ // These commands operate on the same key so they can be combined in cluster mode
131
+ await this.redisClient
132
+ .multi()
133
+ .incr(DLQ_RATE_LIMIT_KEY)
134
+ .expire(DLQ_RATE_LIMIT_KEY, 60) // Reset rate limit after 1 minute
135
+ .exec();
131
136
  }
132
137
  }
133
138
  exports.DeadLetterQueue = DeadLetterQueue;
@@ -4,12 +4,12 @@ export type RedisType = Redis | Cluster;
4
4
  export declare class RedisRegistry {
5
5
  private static registry;
6
6
  private static options;
7
- static attemptConnection(connectionKey: string, storeRef?: number): RedisType;
8
- static handleDisconnects(connection: RedisType, connectionKey: string, storeRef: number): void;
9
- static handlePing(connection: RedisType): void;
10
- static getConnection(connectionType?: string, storeRef?: number): RedisType;
11
- static setOptions(options: IOptions): void;
12
- static _getOptions(): IOptions;
7
+ static attemptConnection(redisConnectionId: string, connectionKey: string, storeRef?: number): RedisType;
8
+ static handleDisconnects(redisConnectionId: string, connection: RedisType, connectionKey: string, storeRef: number): void;
9
+ static handlePing(redisConnectionId: string, connection: RedisType): void;
10
+ static getConnection(redisConnectionId: string, connectionType?: string, storeRef?: number): RedisType;
11
+ static setOptions(options: IOptions, redisConnectionId: string): void;
12
+ static _getOptions(redisConnectionId: string): IOptions;
13
13
  }
14
14
  /**
15
15
  * This function is used to set Redis Connection options per instance. If no
@@ -19,4 +19,4 @@ export declare class RedisRegistry {
19
19
  *
20
20
  * @param options
21
21
  */
22
- export declare function setRedisConnectionSettings(options: IOptions): void;
22
+ export declare function setRedisConnectionSettings(options: IOptions, redisConnectionId?: string): void;
@@ -5,35 +5,40 @@ exports.setRedisConnectionSettings = setRedisConnectionSettings;
5
5
  const ioredis_1 = require("ioredis");
6
6
  const logger_1 = require("./logger");
7
7
  class RedisRegistry {
8
- static attemptConnection(connectionKey, storeRef = 0) {
8
+ static attemptConnection(redisConnectionId, connectionKey, storeRef = 0) {
9
9
  let ref;
10
- if (RedisRegistry.options.cluster) {
11
- ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes, {
12
- ...RedisRegistry.options.cluster.options,
10
+ const options = RedisRegistry.options[redisConnectionId];
11
+ if (!options)
12
+ throw new Error(`PUBLISHER: Redis config key ${redisConnectionId} not found`);
13
+ if (options.cluster) {
14
+ ref = new ioredis_1.Cluster(options.cluster.nodes, {
15
+ ...options.cluster.options,
13
16
  redisOptions: {
14
- ...RedisRegistry.options.cluster.options.redisOptions,
17
+ ...options.cluster.options.redisOptions,
15
18
  db: storeRef,
16
19
  },
17
20
  });
18
21
  }
19
22
  else
20
23
  ref = new ioredis_1.default({
21
- ...RedisRegistry.options.redis,
24
+ ...options.redis,
22
25
  db: storeRef,
23
26
  });
24
- RedisRegistry.registry.set(connectionKey, ref);
25
- RedisRegistry.handleDisconnects(ref, connectionKey, storeRef);
27
+ if (!RedisRegistry.registry[connectionKey])
28
+ RedisRegistry.registry[connectionKey] = new Map();
29
+ RedisRegistry.registry[connectionKey].set(redisConnectionId, ref);
30
+ RedisRegistry.handleDisconnects(redisConnectionId, ref, connectionKey, storeRef);
26
31
  return ref;
27
32
  }
28
- static handleDisconnects(connection, connectionKey, storeRef) {
33
+ static handleDisconnects(redisConnectionId, connection, connectionKey, storeRef) {
29
34
  connection.on('error', (error) => {
30
35
  logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Redis connection error : ${error.message}`);
31
36
  connection.removeAllListeners();
32
37
  connection.disconnect();
33
- RedisRegistry.attemptConnection(connectionKey, storeRef);
38
+ RedisRegistry.attemptConnection(redisConnectionId, connectionKey, storeRef);
34
39
  });
35
40
  }
36
- static handlePing(connection) {
41
+ static handlePing(redisConnectionId, connection) {
37
42
  setInterval(async () => {
38
43
  const res = await Promise.race([
39
44
  connection.ping(),
@@ -45,52 +50,61 @@ class RedisRegistry {
45
50
  ]);
46
51
  if (res === 'NO_PING') {
47
52
  connection.disconnect(true);
48
- logger_1.PUBLISHER_LOGGER.error('PUBLISHER: failed to ping redis, disconnecting and restarting service.');
53
+ logger_1.PUBLISHER_LOGGER.error('PUBLISHER: failed to ping redis, disconnecting and restarting service for redisConnectionId : ' + redisConnectionId);
49
54
  process.exit(0);
50
55
  }
51
56
  }, 2000);
52
57
  }
53
- static getConnection(connectionType = 'primary', storeRef = 0) {
58
+ static getConnection(redisConnectionId, connectionType = 'primary', storeRef = 0) {
54
59
  const connectionKey = `${connectionType}${storeRef}`;
55
- let ref = this.registry.get(connectionKey);
60
+ const options = RedisRegistry.options[redisConnectionId];
61
+ if (!options)
62
+ throw new Error(`PUBLISHER: Redis config key ${redisConnectionId} not found`);
63
+ if (!this.registry[redisConnectionId])
64
+ this.registry[redisConnectionId] = new Map();
65
+ let ref = this.registry[redisConnectionId].get(connectionKey);
56
66
  if (!ref) {
57
- if (RedisRegistry.options.cluster) {
58
- ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes, {
59
- ...RedisRegistry.options.cluster.options,
67
+ if (options.cluster) {
68
+ ref = new ioredis_1.Cluster(options.cluster.nodes, {
69
+ ...options.cluster.options,
60
70
  redisOptions: {
61
- ...RedisRegistry.options.cluster.options.redisOptions,
71
+ ...options.cluster.options.redisOptions,
62
72
  db: storeRef,
63
73
  },
64
74
  });
65
75
  }
66
- else if (RedisRegistry.options.sentinels) {
76
+ else if (options.sentinels) {
67
77
  ref = new ioredis_1.default({
68
- ...RedisRegistry.options.sentinels.options,
69
- sentinels: RedisRegistry.options.sentinels.nodes,
78
+ ...options.sentinels.options,
79
+ sentinels: options.sentinels.nodes,
70
80
  });
71
81
  }
72
82
  else {
73
83
  ref = new ioredis_1.default({
74
- ...RedisRegistry.options.redis,
84
+ ...options.redis,
75
85
  db: storeRef,
76
86
  });
77
87
  }
78
88
  }
79
89
  return ref;
80
90
  }
81
- static setOptions(options) {
82
- RedisRegistry.options = options;
91
+ static setOptions(options, redisConnectionId) {
92
+ RedisRegistry.options[redisConnectionId] = options;
83
93
  }
84
- static _getOptions() {
85
- return RedisRegistry.options;
94
+ static _getOptions(redisConnectionId) {
95
+ return RedisRegistry.options[redisConnectionId];
86
96
  }
87
97
  }
88
98
  exports.RedisRegistry = RedisRegistry;
89
- RedisRegistry.registry = new Map();
99
+ RedisRegistry.registry = {
100
+ default: new Map(),
101
+ };
90
102
  RedisRegistry.options = {
91
- redis: {
92
- port: parseInt(process.env['REDIS_PORT'] ?? '6379'),
93
- host: process.env['REDIS_HOST'] ?? 'localhost',
103
+ default: {
104
+ redis: {
105
+ port: parseInt(process.env['REDIS_PORT'] ?? '6379'),
106
+ host: process.env['REDIS_HOST'] ?? 'localhost',
107
+ },
94
108
  },
95
109
  };
96
110
  /**
@@ -101,6 +115,6 @@ RedisRegistry.options = {
101
115
  *
102
116
  * @param options
103
117
  */
104
- function setRedisConnectionSettings(options) {
105
- RedisRegistry.setOptions(options);
118
+ function setRedisConnectionSettings(options, redisConnectionId = 'default') {
119
+ RedisRegistry.setOptions(options, redisConnectionId);
106
120
  }
@@ -4,11 +4,12 @@ import { RedisType } from './registry';
4
4
  * meant to be used internally by the scheduler application
5
5
  */
6
6
  export declare class ScheduledProcessor {
7
+ private redisConnectionId;
7
8
  private scheduledMessagesTimer;
8
9
  private _redisPublisher?;
9
10
  private previousTaskCompleted;
10
11
  get redisPublisher(): RedisType;
11
- constructor(duration?: number);
12
+ constructor(duration?: number, redisConnectionId?: string);
12
13
  private processScheduledEvents;
13
14
  getAllScheduledEvents(): Promise<Array<string>>;
14
15
  close(): Promise<void>;
@@ -13,10 +13,11 @@ const utils_1 = require("./utils");
13
13
  class ScheduledProcessor {
14
14
  get redisPublisher() {
15
15
  if (!this._redisPublisher)
16
- this._redisPublisher = registry_1.RedisRegistry.getConnection('publish');
16
+ this._redisPublisher = registry_1.RedisRegistry.getConnection(this.redisConnectionId, 'publish');
17
17
  return this._redisPublisher;
18
18
  }
19
- constructor(duration = 1000) {
19
+ constructor(duration = 1000, redisConnectionId = 'default') {
20
+ this.redisConnectionId = redisConnectionId;
20
21
  this.previousTaskCompleted = true;
21
22
  this.scheduledMessagesTimer = (0, rxjs_1.interval)(duration).subscribe(() => {
22
23
  logger_1.PUBLISHER_LOGGER.log('Checking Streams messages at ', new Date().toISOString(), '...');
@@ -3,6 +3,7 @@ import { IAggregatedMetrics, TQueryableMetrics } from '../monitoring/types';
3
3
  import { CircuitState } from '../performance/circuit_breaker';
4
4
  import { EventData, IListenOptions, IStreamsConfig, PublishData } from './types';
5
5
  export declare class Streams {
6
+ private redisConnectionId;
6
7
  private _redisPublisher?;
7
8
  private _redisGroups?;
8
9
  private config;
@@ -33,7 +34,7 @@ export declare class Streams {
33
34
  * // Create a new Streams instance for the "POS" service
34
35
  * const streams = new Streams('POS');
35
36
  */
36
- constructor(serviceName: string, config?: Partial<IStreamsConfig>);
37
+ constructor(serviceName: string, config?: Partial<IStreamsConfig>, redisConnectionId?: string);
37
38
  private setupCircuitBreakerListeners;
38
39
  private runClear;
39
40
  /**
@@ -14,12 +14,12 @@ const utils_1 = require("./utils");
14
14
  class Streams {
15
15
  get redisPublisher() {
16
16
  if (!this._redisPublisher)
17
- this._redisPublisher = registry_1.RedisRegistry.getConnection('publish');
17
+ this._redisPublisher = registry_1.RedisRegistry.getConnection(this.redisConnectionId, 'publish');
18
18
  return this._redisPublisher;
19
19
  }
20
20
  get redisGroups() {
21
21
  if (!this._redisGroups)
22
- this._redisGroups = registry_1.RedisRegistry.getConnection('groups');
22
+ this._redisGroups = registry_1.RedisRegistry.getConnection(this.redisConnectionId, 'groups');
23
23
  return this._redisGroups;
24
24
  }
25
25
  /**
@@ -36,7 +36,8 @@ class Streams {
36
36
  * // Create a new Streams instance for the "POS" service
37
37
  * const streams = new Streams('POS');
38
38
  */
39
- constructor(serviceName, config = {}) {
39
+ constructor(serviceName, config = {}, redisConnectionId = 'default') {
40
+ this.redisConnectionId = redisConnectionId;
40
41
  this.eventsListened = [];
41
42
  this.subscriptions = new Map();
42
43
  this.DEFAULT_STREAMS_CONFIG = {
@@ -506,7 +507,7 @@ class Streams {
506
507
  .then((consumerRegistered) => {
507
508
  if (!consumerRegistered)
508
509
  throw new Error('PUBLISHER: Cannot setup consumer');
509
- const eventStreamClient = registry_1.RedisRegistry.getConnection(`sub-${eventName}`);
510
+ const eventStreamClient = registry_1.RedisRegistry.getConnection(this.redisConnectionId, `sub-${eventName}`);
510
511
  eventStreamClient.subscribe(eventName).then(() => {
511
512
  logger_1.PUBLISHER_LOGGER.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName}`);
512
513
  });
@@ -565,7 +566,7 @@ class Streams {
565
566
  await this._redisPublisher.quit();
566
567
  }
567
568
  for (const eventName of this.eventsListened) {
568
- await registry_1.RedisRegistry.getConnection(`sub-${eventName}`).quit();
569
+ await registry_1.RedisRegistry.getConnection(this.redisConnectionId, `sub-${eventName}`).quit();
569
570
  }
570
571
  if (this._redisGroups) {
571
572
  await this._redisGroups.quit();