@jetit/publisher 2.0.2 → 3.1.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": "2.0.2",
3
+ "version": "3.1.0",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "0.0.11",
@@ -8,7 +8,7 @@
8
8
  "rxjs": "^7.8.0"
9
9
  },
10
10
  "peerDependencies": {
11
- "tslib": "2.5.0"
11
+ "tslib": "2.5.3"
12
12
  },
13
13
  "main": "./src/index.js",
14
14
  "types": "./src/index.d.ts"
@@ -1,3 +1,4 @@
1
1
  export { Streams as Publisher } from './redis/streams';
2
2
  export { setRedisConnectionSettings as setRedisConfig } from './redis/registry';
3
3
  export { ScheduledProcessor as __SCHEDULER_INTERNALS__ } from './redis/scheduler';
4
+ export { UTILS as StreamUtilityFunctions } from './redis/utils';
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.__SCHEDULER_INTERNALS__ = exports.setRedisConfig = exports.Publisher = void 0;
3
+ exports.StreamUtilityFunctions = exports.__SCHEDULER_INTERNALS__ = exports.setRedisConfig = exports.Publisher = void 0;
4
4
  var streams_1 = require("./redis/streams");
5
5
  Object.defineProperty(exports, "Publisher", { enumerable: true, get: function () { return streams_1.Streams; } });
6
6
  var registry_1 = require("./redis/registry");
7
7
  Object.defineProperty(exports, "setRedisConfig", { enumerable: true, get: function () { return registry_1.setRedisConnectionSettings; } });
8
8
  var scheduler_1 = require("./redis/scheduler");
9
9
  Object.defineProperty(exports, "__SCHEDULER_INTERNALS__", { enumerable: true, get: function () { return scheduler_1.ScheduledProcessor; } });
10
+ var utils_1 = require("./redis/utils");
11
+ Object.defineProperty(exports, "StreamUtilityFunctions", { enumerable: true, get: function () { return utils_1.UTILS; } });
@@ -1,17 +1,24 @@
1
1
  "use strict";
2
- var _a, _b;
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
3
  exports.setRedisConnectionSettings = exports.RedisRegistry = void 0;
5
- const tslib_1 = require("tslib");
6
4
  const ioredis_1 = require("ioredis");
7
5
  class RedisRegistry {
8
6
  static attemptConnection(connectionKey, storeRef = 0) {
9
7
  let ref;
10
8
  if (RedisRegistry.options.cluster) {
11
- ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes, Object.assign(Object.assign({}, RedisRegistry.options.cluster.options), { redisOptions: Object.assign(Object.assign({}, RedisRegistry.options.cluster.options.redisOptions), { db: storeRef }) }));
9
+ ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes, {
10
+ ...RedisRegistry.options.cluster.options,
11
+ redisOptions: {
12
+ ...RedisRegistry.options.cluster.options.redisOptions,
13
+ db: storeRef,
14
+ },
15
+ });
12
16
  }
13
17
  else
14
- ref = new ioredis_1.default(Object.assign(Object.assign({}, RedisRegistry.options.redis), { db: storeRef }));
18
+ ref = new ioredis_1.default({
19
+ ...RedisRegistry.options.redis,
20
+ db: storeRef,
21
+ });
15
22
  RedisRegistry.registry.set(connectionKey, ref);
16
23
  RedisRegistry.handleDisconnects(ref, connectionKey, storeRef);
17
24
  return ref;
@@ -24,8 +31,8 @@ class RedisRegistry {
24
31
  });
25
32
  }
26
33
  static handlePing(connection) {
27
- setInterval(() => tslib_1.__awaiter(this, void 0, void 0, function* () {
28
- const res = yield Promise.race([
34
+ setInterval(async () => {
35
+ const res = await Promise.race([
29
36
  connection.ping(),
30
37
  new Promise((res) => {
31
38
  setTimeout(() => {
@@ -38,17 +45,26 @@ class RedisRegistry {
38
45
  console.error('PUBLISHER: failed to ping redis, disconnecting and restarting service.');
39
46
  process.exit(0);
40
47
  }
41
- }), 2000);
48
+ }, 2000);
42
49
  }
43
50
  static getConnection(connectionType = 'primary', storeRef = 0) {
44
51
  const connectionKey = `${connectionType}${storeRef}`;
45
52
  let ref = this.registry.get(connectionKey);
46
53
  if (!ref) {
47
54
  if (RedisRegistry.options.cluster) {
48
- ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes, Object.assign(Object.assign({}, RedisRegistry.options.cluster.options), { redisOptions: Object.assign(Object.assign({}, RedisRegistry.options.cluster.options.redisOptions), { db: storeRef }) }));
55
+ ref = new ioredis_1.Cluster(RedisRegistry.options.cluster.nodes, {
56
+ ...RedisRegistry.options.cluster.options,
57
+ redisOptions: {
58
+ ...RedisRegistry.options.cluster.options.redisOptions,
59
+ db: storeRef,
60
+ },
61
+ });
49
62
  }
50
63
  else
51
- ref = new ioredis_1.default(Object.assign(Object.assign({}, RedisRegistry.options.redis), { db: storeRef }));
64
+ ref = new ioredis_1.default({
65
+ ...RedisRegistry.options.redis,
66
+ db: storeRef,
67
+ });
52
68
  }
53
69
  return ref;
54
70
  }
@@ -59,14 +75,14 @@ class RedisRegistry {
59
75
  return RedisRegistry.options;
60
76
  }
61
77
  }
78
+ exports.RedisRegistry = RedisRegistry;
62
79
  RedisRegistry.registry = new Map();
63
80
  RedisRegistry.options = {
64
81
  redis: {
65
- port: parseInt((_a = process.env['REDIS_PORT']) !== null && _a !== void 0 ? _a : '6379'),
66
- host: (_b = process.env['REDIS_HOST']) !== null && _b !== void 0 ? _b : 'localhost',
82
+ port: parseInt(process.env['REDIS_PORT'] ?? '6379'),
83
+ host: process.env['REDIS_HOST'] ?? 'localhost',
67
84
  },
68
85
  };
69
- exports.RedisRegistry = RedisRegistry;
70
86
  /**
71
87
  * This function is used to set Redis Connection options per instance. If no
72
88
  * options are provided, then the service connects as to a single instance
@@ -1,11 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ScheduledProcessor = void 0;
4
- const tslib_1 = require("tslib");
5
4
  const id_1 = require("@jetit/id");
6
5
  const rxjs_1 = require("rxjs");
7
6
  const registry_1 = require("./registry");
8
- const groups_1 = require("./groups");
7
+ const utils_1 = require("./utils");
9
8
  /**
10
9
  * DO NOT USE THIS CLASS IF YOU DON'T KNOW WHAT YOU ARE DOING. This class is
11
10
  * meant to be used internally by the scheduler application
@@ -36,48 +35,49 @@ class ScheduledProcessor {
36
35
  }
37
36
  });
38
37
  }
39
- processScheduledEvents() {
40
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
41
- const currentTime = new Date().getTime();
42
- const events = yield this.redisPublisher.zrangebyscore('se', 0, currentTime);
43
- console.log('Events to process:', events.length);
44
- for (const eventString of events) {
45
- const eventData = JSON.parse(eventString);
46
- /**
47
- * Remove the event from the Redis Sorted Set first. Please note that
48
- * there is a chance of failure here if the process crashes before
49
- * the event is published. In that case, the event will be lost.
50
- *
51
- * Instead of using the publish method directly, the entire logic is
52
- * copy pasted to reduce the case of failure.
53
- */
54
- eventData.eventId = (0, id_1.generateID)('HEX', 'FF');
55
- yield this.redisPublisher.zrem('se', eventString);
56
- const consumerGroups = yield (0, groups_1.getAllConsumerGroups)(eventData.eventName, this.redisPublisher);
57
- console.log('Scheduled Publishing to consumer groups: ', consumerGroups, 'with id ', eventData.eventId, '...');
58
- for (const consumerGroup of consumerGroups) {
59
- // Publish the event to each consumer group's stream
60
- const streamName = `${eventData.eventName}:${consumerGroup}`;
61
- yield this.redisPublisher.xadd(streamName, '*', 'data', JSON.stringify(eventData));
62
- }
63
- if (eventData.repeatInterval) {
64
- const nextEventTime = currentTime + eventData.repeatInterval;
65
- const nextEventString = JSON.stringify(Object.assign({}, eventData));
66
- yield this.redisPublisher.zadd('se', nextEventTime, nextEventString);
67
- }
68
- yield this.redisPublisher.publish(eventData.eventName, '');
38
+ async processScheduledEvents() {
39
+ const currentTime = new Date().getTime();
40
+ const events = await this.redisPublisher.zrangebyscore('se', 0, currentTime);
41
+ console.log('Events to process:', events.length);
42
+ for (const eventString of events) {
43
+ const eventData = JSON.parse(eventString);
44
+ /**
45
+ * Remove the event from the Redis Sorted Set first. Please note that
46
+ * there is a chance of failure here if the process crashes before
47
+ * the event is published. In that case, the event will be lost.
48
+ *
49
+ * Instead of using the publish method directly, the entire logic is
50
+ * copy pasted to reduce the case of failure.
51
+ */
52
+ eventData.eventId = (0, id_1.generateID)('HEX', 'FF');
53
+ await this.redisPublisher.zrem('se', eventString);
54
+ 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
+ let key = '*';
57
+ for (const consumerGroup of consumerGroups) {
58
+ // Publish the event to each consumer group's stream
59
+ const streamName = `${eventData.eventName}:${consumerGroup}`;
60
+ const generatedKey = await this.redisPublisher
61
+ .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
+ if (key === '*')
64
+ key = generatedKey ?? key;
69
65
  }
70
- });
66
+ if (eventData.repeatInterval) {
67
+ const nextEventTime = currentTime + eventData.repeatInterval;
68
+ const nextEventString = JSON.stringify({ ...eventData });
69
+ await this.redisPublisher.zadd('se', nextEventTime, nextEventString);
70
+ }
71
+ await (0, utils_1.notifySubscribers)(this.redisPublisher, eventData.eventName, key);
72
+ }
71
73
  }
72
74
  getAllScheduledEvents() {
73
75
  return this.redisPublisher.zrange('se', 0, -1);
74
76
  }
75
- close() {
76
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
77
- if (this.scheduledMessagesTimer) {
78
- this.scheduledMessagesTimer.unsubscribe();
79
- }
80
- });
77
+ async close() {
78
+ if (this.scheduledMessagesTimer) {
79
+ this.scheduledMessagesTimer.unsubscribe();
80
+ }
81
81
  }
82
82
  }
83
83
  exports.ScheduledProcessor = ScheduledProcessor;
@@ -26,8 +26,6 @@ export declare class Streams {
26
26
  */
27
27
  constructor(serviceName: string);
28
28
  private runClear;
29
- private isDuplicateMessage;
30
- private clearDuplicationCheckKeys;
31
29
  /**
32
30
  * Publishes an event with the given data to the Redis event stream.
33
31
  *
@@ -121,6 +119,5 @@ export declare class Streams {
121
119
  * }
122
120
  */
123
121
  close(): Promise<void>;
124
- private scanAndClaimAUnclaimedMessage;
125
122
  private cleanupAcknowledgedMessages;
126
123
  }
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Streams = void 0;
4
- const tslib_1 = require("tslib");
5
4
  const registry_1 = require("./registry");
6
5
  const rxjs_1 = require("rxjs");
7
6
  const id_1 = require("@jetit/id");
8
- const groups_1 = require("./groups");
7
+ const utils_1 = require("./utils");
9
8
  function publisherErrorHandler(error) {
10
- console.error('PUBLISHER UNHANDLED ERROR: ', error);
9
+ console.error('PUBLISHER UNHANDLED ERROR: ', JSON.stringify(error));
11
10
  }
12
11
  class Streams {
13
12
  get redisPublisher() {
@@ -35,114 +34,78 @@ class Streams {
35
34
  * const streams = new Streams('POS');
36
35
  */
37
36
  constructor(serviceName) {
38
- var _a, _b;
39
37
  this.eventsListened = [];
40
- this.instanceUniqueId = (_a = process.env['INSTANCE_ID']) !== null && _a !== void 0 ? _a : (0, id_1.generateID)('HEX', 'FE');
38
+ this.instanceUniqueId = process.env['INSTANCE_ID'] ?? (0, id_1.generateID)('HEX', 'FE');
41
39
  this.instanceId = `${serviceName}:${this.instanceUniqueId}`;
42
40
  this.consumerGroupName = `cg-${serviceName}`;
43
41
  console.log(`PUBLISHER: Instance ID: ${this.instanceId}`);
44
- const cleanUpInterval = (_b = parseInt(process.env['CLEANUP_INTERVAL'] || `${1000 * 60 * 60}`, 10)) !== null && _b !== void 0 ? _b : 1000 * 60 * 60;
45
- setTimeout(() => this.runClear(cleanUpInterval), 60000);
42
+ const cleanUpInterval = parseInt(process.env['CLEANUP_INTERVAL'] || `${1000 * 60 * 60}`, 10) ?? 1000 * 60 * 60;
46
43
  this.cleanUpTimer = setInterval(() => {
47
44
  this.runClear(cleanUpInterval);
48
45
  }, cleanUpInterval);
49
46
  }
50
- runClear(cleanUpInterval) {
51
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
52
- console.log('PUBLISHER: Running Clearance', this.eventsListened);
53
- this.clearDuplicationCheckKeys();
54
- for (const eventName of this.eventsListened) {
55
- process.nextTick(() => tslib_1.__awaiter(this, void 0, void 0, function* () {
56
- /** This removes the messages from the stream after they have been processed according to cleanup interval */
57
- yield this.cleanupAcknowledgedMessages(eventName, cleanUpInterval).catch(publisherErrorHandler);
58
- console.log(`Cleanup process for Acknowledged messages completed for ${eventName}`);
59
- /**
60
- * This process scans and claims any unclaimed message according to the cleanup interval.
61
- * This triggers a cascaded reaction down the chain as message by message is claimed and processed
62
- */
63
- const streamName = `${eventName}:${this.consumerGroupName}`;
64
- yield this.scanAndClaimAUnclaimedMessage(streamName).catch(publisherErrorHandler);
65
- console.log(`Unclaimed messages for ${streamName}`);
66
- }));
67
- }
68
- });
69
- }
70
- isDuplicateMessage(streamName, messageId) {
71
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
72
- const processedMessagesKey = `pm:${this.consumerGroupName}:${streamName}`;
73
- const temp = yield Promise.race([
74
- this.redisGroups.zscore(processedMessagesKey, messageId),
75
- /** ioRedis doesnt seem to return the nil event. So waiting for 100ms before moving on */
76
- new Promise((res) => setTimeout(() => res(null), 100)),
77
- ]);
78
- return temp !== null;
79
- });
80
- }
81
- clearDuplicationCheckKeys() {
82
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
83
- const processedMessagesKeyPattern = `pm:${this.consumerGroupName}:*`;
84
- let cursor = '0';
85
- do {
86
- const [nextCursor, keys] = yield this.redisGroups.scan(cursor, 'MATCH', processedMessagesKeyPattern);
87
- cursor = nextCursor;
88
- for (const key of keys) {
89
- const oneHourAgo = Date.now() - 60 * 60 * 1000;
90
- yield this.redisGroups.zremrangebyscore(key, '-inf', oneHourAgo);
91
- }
92
- } while (cursor !== '0');
93
- });
47
+ async runClear(cleanUpInterval) {
48
+ console.log('PUBLISHER: Running Clearance', this.eventsListened);
49
+ for (const eventName of this.eventsListened) {
50
+ process.nextTick(async () => {
51
+ /** This removes the messages from the stream after they have been processed according to cleanup interval */
52
+ await this.cleanupAcknowledgedMessages(eventName, cleanUpInterval).catch(publisherErrorHandler);
53
+ console.log(`Cleanup process for Acknowledged messages completed for ${eventName}`);
54
+ });
55
+ }
94
56
  }
95
- publish(data) {
96
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
97
- if (data.eventId)
98
- data.republishEvent = data.eventId;
99
- data.eventId = (0, id_1.generateID)('HEX', 'FF');
100
- if (!data.createdAt)
101
- data.createdAt = Date.now();
102
- const consumerGroups = yield (0, groups_1.getAllConsumerGroups)(data.eventName, this.redisPublisher);
103
- if (consumerGroups.length > 0) {
104
- console.log(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')}`);
105
- try {
106
- for (const consumerGroup of consumerGroups) {
107
- // Publish the event to each consumer group's stream
108
- const streamName = `${data.eventName}:${consumerGroup}`;
109
- yield this.redisPublisher.xadd(streamName, '*', 'data', JSON.stringify(data));
110
- }
111
- yield this.redisPublisher.publish(data.eventName, '');
112
- }
113
- catch (error) {
114
- console.error(`PUBLISHER: Error while publishing event for service ${this.consumerGroupName} with instance ${this.instanceId}: `, error);
115
- throw new Error('Publisher Error');
57
+ async publish(data) {
58
+ if (data.eventId)
59
+ data.republishEvent = data.eventId;
60
+ data.eventId = (0, id_1.generateID)('HEX', 'FF');
61
+ if (!data.createdAt)
62
+ data.createdAt = Date.now();
63
+ const consumerGroups = await (0, utils_1.getAllConsumerGroups)(data.eventName, this.redisPublisher);
64
+ let key = '*';
65
+ if (consumerGroups.length > 0) {
66
+ console.log(`PUBLISHER: Publishing event ${data.eventName} to consumer groups: ${consumerGroups.join(', ')}`);
67
+ try {
68
+ for (const consumerGroup of consumerGroups) {
69
+ // Publish the event to each consumer group's stream
70
+ const streamName = `${data.eventName}:${consumerGroup}`;
71
+ const generatedKey = await this.redisPublisher
72
+ .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} `));
74
+ if (key === '*')
75
+ key = generatedKey ?? key;
116
76
  }
77
+ await (0, utils_1.notifySubscribers)(this.redisPublisher, data.eventName, key);
117
78
  }
118
- else
119
- console.log(`PUBLISHER: Event publish failed for event ${data.eventName}, reason: no consumers ${consumerGroups}`);
120
- });
121
- }
122
- scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0) {
123
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
124
- const currentTime = new Date();
125
- delete eventData.repeatInterval;
126
- if (repeatInterval > 0) {
127
- eventData.repeatInterval = repeatInterval;
128
- }
129
- if (scheduledTime < currentTime) {
130
- throw new Error('PUBLISHER: Cannot schedule an event in the past');
79
+ catch (error) {
80
+ console.error(`PUBLISHER: Error while publishing event for service ${this.consumerGroupName} with instance ${this.instanceId}: `, error);
81
+ throw new Error('Publisher Error');
131
82
  }
132
- else if (Math.abs(scheduledTime.getTime() - currentTime.getTime()) <= 500) {
133
- yield this.publish(eventData);
134
- }
135
- else {
136
- if (uniquePerInstance === true) {
137
- const existingJob = yield this.redisPublisher.zscore('se', JSON.stringify(eventData));
138
- if (existingJob) {
139
- console.log(`PUBLISHER: Job with data '${eventData}' already exists. Skipping.`);
140
- return;
141
- }
83
+ }
84
+ else
85
+ console.log(`PUBLISHER: Event publish failed for event ${data.eventName}, reason: no consumers ${consumerGroups}`);
86
+ }
87
+ async scheduledPublish(scheduledTime, eventData, uniquePerInstance = false, repeatInterval = 0) {
88
+ const currentTime = new Date();
89
+ delete eventData.repeatInterval;
90
+ if (repeatInterval > 0) {
91
+ eventData.repeatInterval = repeatInterval;
92
+ }
93
+ if (scheduledTime < currentTime) {
94
+ throw new Error('PUBLISHER: Cannot schedule an event in the past');
95
+ }
96
+ else if (Math.abs(scheduledTime.getTime() - currentTime.getTime()) <= 500) {
97
+ await this.publish(eventData);
98
+ }
99
+ else {
100
+ if (uniquePerInstance === true) {
101
+ const existingJob = await this.redisPublisher.zscore('se', JSON.stringify(eventData));
102
+ if (existingJob) {
103
+ console.log(`PUBLISHER: Job with data '${eventData}' already exists. Skipping.`);
104
+ return;
142
105
  }
143
- yield this.redisPublisher.zadd('se', scheduledTime.getTime(), JSON.stringify(eventData));
144
106
  }
145
- });
107
+ await this.redisPublisher.zadd('se', scheduledTime.getTime(), JSON.stringify(eventData));
108
+ }
146
109
  }
147
110
  /**
148
111
  * Listens for events with the given name and returns an Observable that emits an EventData<T> object
@@ -184,98 +147,80 @@ class Streams {
184
147
  return (0, rxjs_1.throwError)(() => new Error(error.message));
185
148
  }));
186
149
  }
187
- createConsumerAndRegister(eventName) {
188
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
189
- const streamName = `${eventName}:${this.consumerGroupName}`;
190
- const key = `instance:${this.instanceId}:subscribedEvents`;
191
- const setKeyForK8sHandling = `instance:${this.instanceUniqueId}:consumerGroupName`;
192
- this.eventsListened.push(eventName);
193
- yield this.redisGroups
194
- .xgroup('CREATE', streamName, this.consumerGroupName, '0', 'MKSTREAM')
195
- .then((v) => {
196
- console.log(`Group created created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
197
- })
198
- .catch((e) => {
199
- console.log(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
200
- });
201
- const createConsumerStatus = (yield this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId));
202
- yield this.redisGroups.sadd(key, eventName);
203
- const addToCGSet = yield this.redisGroups.sadd(`${eventName}`, this.consumerGroupName);
204
- const addToFlushSet = yield this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName);
205
- 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 })}`);
206
- return createConsumerStatus === 0 || createConsumerStatus === 1;
150
+ async createConsumerAndRegister(eventName) {
151
+ const streamName = `${eventName}:${this.consumerGroupName}`;
152
+ const key = `instance:${this.instanceId}:subscribedEvents`;
153
+ const setKeyForK8sHandling = `instance:${this.instanceUniqueId}:consumerGroupName`;
154
+ this.eventsListened.push(eventName);
155
+ await this.redisGroups
156
+ .xgroup('CREATE', streamName, this.consumerGroupName, '0', 'MKSTREAM')
157
+ .then(() => {
158
+ console.log(`Group created created for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
159
+ })
160
+ .catch((e) => {
161
+ console.error(`PUBLISHER: Group creation failed with error ${e.message} for ${JSON.stringify({ streamName, cgn: this.consumerGroupName })}`);
207
162
  });
163
+ const createConsumerStatus = (await this.redisGroups.xgroup('CREATECONSUMER', streamName, this.consumerGroupName, this.instanceId));
164
+ await this.redisGroups.sadd(key, eventName);
165
+ const addToCGSet = await this.redisGroups.sadd(`${eventName}`, this.consumerGroupName);
166
+ const addToFlushSet = await this.redisGroups.set(setKeyForK8sHandling, this.consumerGroupName);
167
+ 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 })}`);
168
+ return createConsumerStatus === 0 || createConsumerStatus === 1;
208
169
  }
209
170
  listenInternals(eventName) {
210
- /** Create the return observable */
211
171
  const bs = new rxjs_1.BehaviorSubject(null);
212
172
  const observable = bs.asObservable().pipe((0, rxjs_1.skip)(1));
213
- /** This gets called the first time the stream is registered to pickup any messages from the previous subscription */
214
173
  const streamName = `${eventName}:${this.consumerGroupName}`;
215
- const processMessage = (redisClient) => tslib_1.__awaiter(this, void 0, void 0, function* () {
216
- const racePr = new Promise((resolve, _) => {
217
- setTimeout(resolve, 1300, 'RACE');
218
- });
219
- console.log(`PUBLISHER: processMessage called for ${streamName} cgn: ${this.consumerGroupName} inst: ${this.instanceId}`);
174
+ const processMessage = async (redisClient, messageId, processPending = false) => {
175
+ console.log(`PUBLISHER: Processing message ${messageId} for ${streamName}`);
220
176
  try {
221
- const prResult = yield Promise.race([
222
- redisClient.xreadgroup('GROUP', this.consumerGroupName, this.instanceId, 'COUNT', 1, 'BLOCK', 1000, 'STREAMS', streamName, '>'),
223
- racePr,
224
- ]);
225
- console.log(`PUBLISHER: Promise race resolved with ${JSON.stringify(prResult)}`);
226
- if (prResult && prResult === 'RACE')
177
+ const messages = await redisClient.xrange(streamName, messageId, messageId);
178
+ const pendingMessage = (await redisClient.xpending(streamName, this.consumerGroupName, messageId, messageId, 1, this.instanceId));
179
+ if (!pendingMessage || pendingMessage.length === 0 || !pendingMessage[0] || pendingMessage[0].length === 0) {
180
+ console.log(`PUBLISHER: Published Message is already processed for ${streamName} with message id ${messageId} `);
227
181
  return;
228
- const result = prResult;
229
- console.log(`PUBLISHER: XREADGROUP returned with ${JSON.stringify(result[0])}`);
230
- if (result) {
231
- const [, streamMessages] = result[0];
232
- for (const [id, data] of streamMessages) {
233
- const eventData = JSON.parse(data[1]);
234
- const messageId = eventData.eventId;
235
- const isDuplicate = yield this.isDuplicateMessage(streamName, messageId);
236
- if (isDuplicate) {
237
- console.warn(`Duplicate message detected: ${messageId}`);
238
- yield redisClient.xack(streamName, this.consumerGroupName, id);
239
- continue;
240
- }
241
- bs.next(eventData);
242
- const pmKey = `pm:${this.consumerGroupName}:${streamName}`;
243
- const currentTime = Date.now();
244
- yield redisClient.zadd(pmKey, currentTime, messageId);
245
- yield redisClient.xack(streamName, this.consumerGroupName, id);
246
- yield redisClient.zadd(`ack:${streamName}`, Date.now(), id);
182
+ }
183
+ if (messages && messages.length) {
184
+ const eventData = JSON.parse(messages[0][1][1]);
185
+ bs.next(eventData);
186
+ await redisClient.xack(streamName, this.consumerGroupName, messageId);
187
+ await redisClient.zadd(`ack:${streamName}`, Date.now().toString(), messageId);
188
+ }
189
+ else {
190
+ console.warn(`PUBLISHER: Message ${messageId} not found for ${streamName}`);
191
+ }
192
+ /** Process Unprocessed Message if this is a main tree, otherwise limit to processing 100 messages that are unacknowledged */
193
+ if (!processPending) {
194
+ const unprocessedMessageIds = await (0, utils_1.getUnacknowledgedMessages)(redisClient, this.consumerGroupName, streamName, 25);
195
+ if (unprocessedMessageIds.count > 25) {
196
+ console.error(`PUBLISHER: Too many unprocessed events for ${streamName}: count: ${unprocessedMessageIds.count}`);
197
+ }
198
+ for (const id of unprocessedMessageIds.messageIds) {
199
+ console.log(`PUBLISHER: Reporcessing unprocessed message with id: ${id}`);
200
+ await processMessage(redisClient, id, true);
247
201
  }
248
202
  }
249
- this.scanAndClaimAUnclaimedMessage(streamName)
250
- .then()
251
- .catch((e) => console.log('PUBLISHER: Err in handling unclaimed Messages ' + e.message));
252
203
  }
253
204
  catch (e) {
254
- console.error(`PUBLISHER: ${JSON.stringify(e)}`);
205
+ console.error(`PUBLISHER: Error processing message ${messageId} for ${streamName}`, e);
255
206
  }
256
- });
207
+ };
208
+ /** Register the consumer and setup the Observable */
257
209
  this.createConsumerAndRegister(eventName)
258
210
  .then((consumerRegistered) => {
259
211
  if (!consumerRegistered)
260
212
  throw new Error('PUBLISHER: Cannot setup consumer');
261
- /** Create new REDIS connection and subscribe */
262
213
  const eventStreamClient = registry_1.RedisRegistry.getConnection(`sub-${eventName}`);
263
214
  eventStreamClient.subscribe(eventName).then(() => {
264
- console.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName} with ${JSON.stringify({
265
- cluster: eventStreamClient.isCluster,
266
- })}`);
215
+ console.log(`PUBLISHER: Redis Subscription connection initiated for ${eventName}`);
216
+ });
217
+ eventStreamClient.on('message', async (channel, messageId) => {
218
+ console.log(`PUBLISHER: Stream Notification Received for event ${eventName} with message ID ${messageId}`);
219
+ await processMessage(this.redisGroups, messageId);
267
220
  });
268
- eventStreamClient.on('message', () => tslib_1.__awaiter(this, void 0, void 0, function* () {
269
- console.log(`PUBLISHER: Stream Notification Received for event ${eventName}`);
270
- yield processMessage(this.redisGroups);
271
- }));
272
- this.scanAndClaimAUnclaimedMessage(streamName)
273
- .then()
274
- .catch((e) => console.log('PUBLISHER: Err in handling unclaimed Messages ' + e.message));
275
221
  })
276
222
  .catch((e) => {
277
- console.error(`PUBLISHER: ${JSON.stringify(e)}`);
278
- throw e;
223
+ console.error(`PUBLISHER: Error during consumer registration for ${eventName}`, e);
279
224
  });
280
225
  return observable;
281
226
  }
@@ -299,50 +244,32 @@ class Streams {
299
244
  * process.exit(0);
300
245
  * }
301
246
  */
302
- close() {
303
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
304
- if (this.cleanUpTimer) {
305
- clearInterval(this.cleanUpTimer);
306
- }
307
- if (this.redisPublisher) {
308
- yield this.redisPublisher.quit();
309
- }
310
- for (const eventName of this.eventsListened) {
311
- registry_1.RedisRegistry.getConnection(`sub-${eventName}`).quit();
312
- }
313
- if (this.redisGroups) {
314
- yield this.redisGroups.quit();
315
- }
316
- });
317
- }
318
- scanAndClaimAUnclaimedMessage(streamName) {
319
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
320
- const rows = yield this.redisGroups.xautoclaim(streamName, this.consumerGroupName, this.instanceId, 500, '0-0', 'COUNT', 1);
321
- if (rows && rows[0] !== '0-0') {
322
- console.log(`PUBLISHER: Handling pending unclaimed Message from ${streamName} for ${this.instanceId}`);
323
- yield this.redisPublisher.publish(streamName.split(':')[0], '');
324
- return this.scanAndClaimAUnclaimedMessage(streamName);
325
- }
326
- else {
327
- console.log(`PUBLISHER: No previous messages found for ${streamName}`);
328
- }
329
- return;
330
- });
247
+ async close() {
248
+ if (this.cleanUpTimer) {
249
+ clearInterval(this.cleanUpTimer);
250
+ }
251
+ if (this.redisPublisher) {
252
+ await this.redisPublisher.quit();
253
+ }
254
+ for (const eventName of this.eventsListened) {
255
+ registry_1.RedisRegistry.getConnection(`sub-${eventName}`).quit();
256
+ }
257
+ if (this.redisGroups) {
258
+ await this.redisGroups.quit();
259
+ }
331
260
  }
332
- cleanupAcknowledgedMessages(eventName, interval = 60 * 60 * 1000) {
333
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
334
- const streamName = `${eventName}:${this.consumerGroupName}`;
335
- const cleanupThreshold = Date.now() - interval;
336
- const acknowledgedMessages = yield this.redisGroups.zrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
337
- if (acknowledgedMessages && acknowledgedMessages.length > 0) {
338
- // Remove acknowledged messages from the stream
339
- for (const messageId of acknowledgedMessages) {
340
- yield this.redisGroups.xdel(streamName, messageId);
341
- }
342
- // Remove acknowledged messages from the Sorted Set
343
- yield this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
261
+ async cleanupAcknowledgedMessages(eventName, interval = 60 * 60 * 1000) {
262
+ const streamName = `${eventName}:${this.consumerGroupName}`;
263
+ const cleanupThreshold = Date.now() - interval;
264
+ const acknowledgedMessages = await this.redisGroups.zrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
265
+ if (acknowledgedMessages && acknowledgedMessages.length > 0) {
266
+ // Remove acknowledged messages from the stream
267
+ for (const messageId of acknowledgedMessages) {
268
+ await this.redisGroups.xdel(streamName, messageId);
344
269
  }
345
- });
270
+ // Remove acknowledged messages from the Sorted Set
271
+ await this.redisGroups.zremrangebyscore(`ack:${streamName}`, '-inf', cleanupThreshold);
272
+ }
346
273
  }
347
274
  }
348
275
  exports.Streams = Streams;
@@ -0,0 +1,18 @@
1
+ import { RedisType } from './registry';
2
+ export declare function getAllConsumerGroups(eventName: string, redisConnection: RedisType): Promise<string[]>;
3
+ export declare function getUnacknowledgedMessages(redisClient: RedisType, consumerGroupName: string, streamName: string, count?: number): Promise<{
4
+ count: number;
5
+ messageIds: string[];
6
+ messages?: unknown[];
7
+ }>;
8
+ export declare function getMessageStatesCount(redisClient: RedisType, streamName: string, consumerGroup: string): Promise<{
9
+ acknowledged: number;
10
+ unacknowledged: number;
11
+ }>;
12
+ export declare function notifySubscribers(redisClient: RedisType, eventName: string, messageId: string): Promise<void>;
13
+ export declare const UTILS: {
14
+ getMessageStatesCount: typeof getMessageStatesCount;
15
+ getUnacknowledgedMessages: typeof getUnacknowledgedMessages;
16
+ getAllConsumerGroupsForEvent: typeof getAllConsumerGroups;
17
+ notifySubscribers: typeof notifySubscribers;
18
+ };
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UTILS = exports.notifySubscribers = exports.getMessageStatesCount = exports.getUnacknowledgedMessages = exports.getAllConsumerGroups = void 0;
4
+ async function getAllConsumerGroups(eventName, redisConnection) {
5
+ const consumerGroups = await redisConnection.smembers(`${eventName}`);
6
+ return consumerGroups;
7
+ }
8
+ exports.getAllConsumerGroups = getAllConsumerGroups;
9
+ async function getUnacknowledgedMessages(redisClient, consumerGroupName, streamName, count = 500) {
10
+ try {
11
+ // Get pending messages summary
12
+ const summary = await redisClient.xpending(streamName, consumerGroupName);
13
+ if (!summary || summary[1] === 0) {
14
+ // If count is zero
15
+ return { count: 0, messageIds: [] };
16
+ }
17
+ // Use the smallest and largest IDs to get a detailed range
18
+ const pendingMessageCount = summary[1];
19
+ // Get detailed information in the range
20
+ const pendingMessages = (await redisClient.xpending(streamName, consumerGroupName, '-', '+', count));
21
+ return {
22
+ count: pendingMessageCount,
23
+ messageIds: pendingMessages.map((message) => message[0]),
24
+ messages: pendingMessages,
25
+ };
26
+ }
27
+ catch (error) {
28
+ console.error(`PUBLISHER: Error fetching unacknowledged messages for ${streamName}`, error);
29
+ return { count: 0, messageIds: [] };
30
+ }
31
+ }
32
+ exports.getUnacknowledgedMessages = getUnacknowledgedMessages;
33
+ async function getMessageStatesCount(redisClient, streamName, consumerGroup) {
34
+ try {
35
+ const pendingInfo = (await redisClient.xpending(streamName, consumerGroup));
36
+ const totalCount = await redisClient.xlen(streamName);
37
+ return {
38
+ acknowledged: totalCount - pendingInfo[1],
39
+ unacknowledged: pendingInfo[1],
40
+ };
41
+ }
42
+ catch (error) {
43
+ console.error(`PUBLISHER: Error fetching message states count for ${streamName}`, error);
44
+ return { acknowledged: 0, unacknowledged: 0 };
45
+ }
46
+ }
47
+ exports.getMessageStatesCount = getMessageStatesCount;
48
+ async function notifySubscribers(redisClient, eventName, messageId) {
49
+ await redisClient.publish(eventName, messageId);
50
+ }
51
+ exports.notifySubscribers = notifySubscribers;
52
+ exports.UTILS = {
53
+ getMessageStatesCount,
54
+ getUnacknowledgedMessages,
55
+ getAllConsumerGroupsForEvent: getAllConsumerGroups,
56
+ notifySubscribers,
57
+ };
@@ -1,2 +0,0 @@
1
- import { RedisType } from './registry';
2
- export declare function getAllConsumerGroups(eventName: string, redisConnection: RedisType): Promise<string[]>;
@@ -1,11 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getAllConsumerGroups = void 0;
4
- const tslib_1 = require("tslib");
5
- function getAllConsumerGroups(eventName, redisConnection) {
6
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
7
- const consumerGroups = yield redisConnection.smembers(`${eventName}`);
8
- return consumerGroups;
9
- });
10
- }
11
- exports.getAllConsumerGroups = getAllConsumerGroups;