@jetit/publisher 4.1.1 → 5.1.1

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
@@ -1,203 +1,281 @@
1
- # publisher
2
-
3
- publisher is a library for implementing an event-driven architecture using Redis PUB/SUB and Redis Streams. It provides a simple and scalable mechanism for publishing and consuming events in real-time, and supports features such as message deduplication, consumer group management, and scheduled event publishing.
4
-
5
- ## IMPORTANT NOTE
6
- This project currently does not have a means to clean up inactive consumers. This means that if you have a consumer that is no longer active, it will continue to receive events until it is removed from the consumer group. This is a known issue and will be addressed in a future release. A workaround is to use the following code to remove inactive consumers from the consumer group as part of your process cleanup:
7
-
8
- ```javascript
9
- const ioredis = require(`ioredis`);
10
-
11
- const env = process.env;
12
-
13
- async function bootstrap() {
14
- const connection = new ioredis.Redis({
15
- host: env.REDIS_HOST,
16
- port: parseInt(env.REDIS_PORT),
17
- });
18
- console.log(`Redis Connection Status ${connection.status}`);
19
- await waitForSeconds(0.3);
20
- console.log(`Redis Connection Status ${connection.status}`);
21
- const instanceUniqueId = env.INSTANCE_ID;
22
- if (!instanceUniqueId) {
23
- console.log(`Unique instance ID is not available`);
24
- return;
25
- }
26
- console.log(`Instance Unique ID : ${instanceUniqueId}`);
27
- const consumerGroupName = await getConsumerGroupName(connection, instanceUniqueId);
28
- if (!consumerGroupName) {
29
- console.log(`Consumer is not available, so graceful shutdown is not necessary.`);
30
- return;
31
- }
32
- console.log(`Consumer Group Name : ${consumerGroupName}`);
33
- const instanceId = getInstanceId(consumerGroupName.slice(3), instanceUniqueId);
34
- console.log(`Instance ID : ${instanceId}`);
35
- const subscribedEvents = await getAllEventsForInstance(connection, instanceId);
36
- console.log(`Subscribed Events : ${JSON.stringify(subscribedEvents)}`);
37
- await clearSubscribedEvents(connection, consumerGroupName, instanceId, subscribedEvents);
38
- await deleteConsumerGroupNameForInstance(connection, instanceId);
39
- await deleteAllEventsFroInstance(connection, instanceId);
40
- }
1
+ # @jetit/publisher
2
+
3
+ `@jetit/publisher` is a robust and feature-rich library for implementing an event-driven architecture using Redis PUB/SUB and Redis Streams. It provides a scalable mechanism for publishing and consuming events in real-time, with support for advanced features such as message deduplication, consumer group management, scheduled event publishing, and more.
4
+
5
+ ## Table of Contents
6
+
7
+ - [@jetit/publisher](#jetitpublisher)
8
+ - [Table of Contents](#table-of-contents)
9
+ - [Installation](#installation)
10
+ - [Key Features](#key-features)
11
+ - [Usage](#usage)
12
+ - [Basic Example](#basic-example)
13
+ - [Configuration](#configuration)
14
+ - [Publishing Events](#publishing-events)
15
+ - [Subscribing to Events](#subscribing-to-events)
16
+ - [Scheduled Publishing](#scheduled-publishing)
17
+ - [Batch Publishing](#batch-publishing)
18
+ - [Dead Letter Queue (DLQ)](#dead-letter-queue-dlq)
19
+ - [Event Filtering](#event-filtering)
20
+ - [Performance Monitoring](#performance-monitoring)
21
+ - [Prometheus Integration](#prometheus-integration)
22
+ - [Advanced Features](#advanced-features)
23
+ - [Content-Based Deduplication](#content-based-deduplication)
24
+ - [Multiple Event Subscriptions](#multiple-event-subscriptions)
25
+ - [Circuit Breaker](#circuit-breaker)
26
+ - [Performance Optimizations](#performance-optimizations)
27
+ - [Cleanup and Graceful Shutdown](#cleanup-and-graceful-shutdown)
28
+ - [Troubleshooting](#troubleshooting)
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install @jetit/publisher
34
+ ```
41
35
 
42
- /**
43
- *
44
- * @param {ioredis.Redis|ioredis.Cluster} connection
45
- * @param {string} consumerGroupName
46
- * @param {string} instanceId
47
- * @param {Array<string>} events
48
- */
49
- async function clearSubscribedEvents(connection, consumerGroupName, instanceId, events) {
50
- return Promise.all(
51
- events.map(async (eventName) => {
52
- console.log(`${eventName} is being cleared in publisher`);
53
- const streamName = `${eventName}:${consumerGroupName}`;
54
- console.log(`${streamName} is being removed.`);
55
- await connection.srem(`${eventName}`, consumerGroupName);
56
- console.log(`${eventName} is removed from ${consumerGroupName}`);
57
- // Releasing all claims based on info from: https://redis.io/commands/xgroup-delconsumer/
58
- await releaseAllClaims(connection, streamName, consumerGroupName, instanceId);
59
- console.log(`${eventName} removes all claims`);
60
- await connection.xgroup(`DELCONSUMER`, streamName, consumerGroupName, instanceId);
61
- console.log(`${eventName} is deleted as a consumer from ${consumerGroupName}, ${instanceId}`);
62
- })
63
- );
64
- }
36
+ ## Key Features
65
37
 
66
- /**
67
- *
68
- * @param {ioredis.Redis|ioredis.Cluster} connection
69
- * @param {string} streamName
70
- * @param {string} consumerGroupName
71
- * @param {string} instanceId
72
- */
73
- async function releaseAllClaims(connection, streamName, consumerGroupName, instanceId) {
74
- /**
75
- * Retrieve the pending messages for the consumer. Note this only fetches the last
76
- * 10000 events assigned to this consumer. This function has been modified to make sure
77
- * that there is a temp instance that claims all this messages
78
- */
79
- const pendingMessages = await connection.xpending(streamName, consumerGroupName, `-`, `+`, 10000, instanceId);
80
-
81
- if (pendingMessages && pendingMessages.length > 0) {
82
- console.log(`${pendingMessages.length} messages to clean up.`);
83
- const transaction = connection.multi({ pipeline: true });
84
- const tempConsumerId = `${consumerGroupName}-temp`;
85
- for (const [messageId] of pendingMessages) {
86
- transaction.xclaim(streamName, consumerGroupName, tempConsumerId, 10, messageId);
87
- }
88
- await transaction.exec();
89
- }
90
- }
38
+ - Real-time event publishing and subscribing
39
+ - Configurable Streams class for flexible usage
40
+ - Improved error handling and reliability
41
+ - Performance tracking with Redis time and operation time metrics
42
+ - Dead Letter Queue (DLQ) for handling subscription failures
43
+ - Event filtering for specialized subscriptions
44
+ - Support for multiple event subscriptions from the same service
45
+ - Batch publishing (regular and scheduled)
46
+ - Basic monitoring with Prometheus export support
47
+ - Content-based one-time guarantee (0-1 semantics support)
48
+ - Optimized cleanup processes for improved performance
49
+ - Circuit Breaker pattern for fault tolerance
91
50
 
92
- /**
93
- *
94
- * @param {string} serviceName
95
- * @param {string} instanceUniqueId
96
- */
97
- function getInstanceId(serviceName, instanceUniqueId) {
98
- const instanceId = `${serviceName}:${instanceUniqueId}`;
99
- console.log(`Generated Instance ID : ${instanceId}`);
100
- return instanceId;
101
- }
51
+ ## Usage
102
52
 
103
- /**
104
- *
105
- * @param {ioredis.Redis|ioredis.Cluster} serviceName
106
- * @param {string} instanceId
107
- * @returns {Promise<string>} consumer group name
108
- */
109
- async function getConsumerGroupName(connection, instanceId) {
110
- const key = `instance:${instanceId}:consumerGroupName`;
111
- console.log(`Get consumer group name called for key : ${key}`);
112
- return await connection.get(key);
113
- }
53
+ ### Basic Example
114
54
 
115
- /**
116
- *
117
- * @param {ioredis.Redis|ioredis.Cluster} connection
118
- * @param {string} instanceId
119
- * @returns
120
- */
121
- async function deleteConsumerGroupNameForInstance(connection, instanceId) {
122
- return await connection.del(`instance:${instanceId}:consumerGroupName`);
123
- }
55
+ ```typescript
56
+ import { Publisher, EventData } from '@jetit/publisher';
124
57
 
125
- /**
126
- *
127
- * @param {ioredis.Redis|ioredis.Cluster} connection
128
- * @param {string} instanceId
129
- * @returns {Promise<Array<events>>} subscribed events for this instance
130
- */
131
- async function getAllEventsForInstance(connection, instanceId) {
132
- const key = `instance:${instanceId}:subscribedEvents`;
133
- console.log(`Get consumer group events : ${key}`);
134
- return (await connection.sscan(key, 0))[1];
135
- }
58
+ // Create an instance of the publisher
59
+ const publisher = new Publisher('MyService');
136
60
 
137
- /**
138
- *
139
- * @param {ioredis.Redis|ioredis.Cluster} connection
140
- * @param {string} instanceId
141
- */
142
- async function deleteAllEventsFroInstance(connection, instanceId) {
143
- return await connection.del(`instance:${instanceId}:subscribedEvents`);
144
- }
61
+ // Publish an event
62
+ const eventData: EventData<{ message: string }> = {
63
+ eventName: 'my-event',
64
+ data: { message: 'Hello, world!' }
65
+ };
145
66
 
146
- async function waitForSeconds(seconds = 10) {
147
- return new Promise((res, _) => setTimeout(() => res(), seconds * 1000));
148
- }
67
+ await publisher.publish(eventData);
149
68
 
150
- /**
151
- * Start
152
- */
69
+ // Subscribe to an event
70
+ publisher.listen('my-event').subscribe(event => {
71
+ console.log(`Received event: ${event.eventName}`, event.data);
72
+ });
73
+ ```
153
74
 
154
- bootstrap()
155
- .then(() => process.exit(0))
156
- .catch((e) => {
157
- console.error(e);
158
- process.exit(1);
159
- });
75
+ ### Configuration
160
76
 
77
+ The `Publisher` class can be configured with various options, including Circuit Breaker and Backpressure handling:
78
+
79
+ ```typescript
80
+ import { Publisher, IStreamsConfig } from '@jetit/publisher';
81
+
82
+ const config: Partial<IStreamsConfig> = {
83
+ cleanUpInterval: 3600000, // 1 hour
84
+ maxRetries: 5,
85
+ initialRetryDelay: 1000,
86
+ immediatePublishThreshold: 500,
87
+ unprocessedMessageThreshold: 25,
88
+ acknowledgedMessageCleanupInterval: 3600000, // 1 hour
89
+ dlqEventThreshold: 2000,
90
+ filterKeepAlive: 86400000, // 24 hours
91
+ duplicationCheckWindow: 86400, // 24 hours
92
+ circuitBreaker: {
93
+ enabled: true,
94
+ errorThreshold: 50,
95
+ errorThresholdPercentage: 50,
96
+ openStateDuration: 30000, // 30s
97
+ halfOpenStateMaxAttempts: 10,
98
+ maxStoredEvents: 5000,
99
+ },
100
+ };
101
+
102
+ const publisher = new Publisher('MyService', config);
161
103
  ```
162
104
 
163
- ## Simple Example
105
+ ### Publishing Events
164
106
 
165
107
  ```typescript
166
- import { Publisher, EventData } from '@jetit/streams';
108
+ const eventData = {
109
+ eventName: 'user-registered',
110
+ data: { userId: '123', email: 'user@example.com' }
111
+ };
167
112
 
168
- // Create an instance of the publisher
169
- const streams = new Streams('Websockets');
113
+ await publisher.publish(eventData);
114
+ ```
170
115
 
171
- // Publish an event
172
- const eventData: EventData<{ message: string }> = {
173
- eventName: 'my-event',
174
- data: { message: 'Hello, world!' }
116
+ ### Subscribing to Events
117
+
118
+ ```typescript
119
+ publisher.listen('user-registered').subscribe(event => {
120
+ console.log('New user registered:', event.data);
121
+ });
122
+ ```
123
+
124
+ ### Scheduled Publishing
125
+
126
+ ```typescript
127
+ const futureDate = new Date(Date.now() + 60000); // 1 minute from now
128
+ await publisher.scheduledPublish(futureDate, eventData);
129
+ ```
130
+
131
+ ### Batch Publishing
132
+
133
+ ```typescript
134
+ import { publishBatch } from '@jetit/publisher';
135
+
136
+ const events = [
137
+ { eventName: 'event1', data: { /* ... */ } },
138
+ { eventName: 'event2', data: { /* ... */ } },
139
+ // ...
140
+ ];
141
+
142
+ const result = await publishBatch(publisher, events, { batchSize: 100, delayBetweenBatches: 1000 });
143
+ console.log('Batch publish result:', result);
144
+ ```
145
+
146
+ ### Dead Letter Queue (DLQ)
147
+
148
+ ```typescript
149
+ // Retry an event from DLQ
150
+ const success = await publisher.retryFromDLQ('eventId');
151
+
152
+ // Get DLQ stats
153
+ const stats = await publisher.getDLQStats();
154
+ console.log('DLQ stats:', stats);
155
+ ```
156
+
157
+ ### Event Filtering
158
+
159
+ ```typescript
160
+ const options = {
161
+ eventFilter: (event) => event.data.userId === '123',
162
+ filterKeepAlive: 3600000 // 1 hour
175
163
  };
176
164
 
177
- await streams.publish(eventData);
165
+ publisher.listen('user-action', options).subscribe(event => {
166
+ console.log('Filtered user action:', event);
167
+ });
168
+ ```
178
169
 
179
- // Subscribe to an event
180
- streams.listen('my-event').subscribe(event => {
181
- console.log(`Received event: ${event.eventName}`, event.data);
170
+ ### Performance Monitoring
171
+
172
+ ```typescript
173
+ // Get metrics for a specific time range
174
+ const metrics = await publisher.getMetrics(startTime, endTime);
175
+ console.log('Performance metrics:', metrics);
176
+
177
+ // Get latest metrics
178
+ const latestMetrics = await publisher.getLatestMetrics();
179
+ console.log('Latest metrics:', latestMetrics);
180
+ ```
181
+
182
+ ### Prometheus Integration
183
+
184
+ ```typescript
185
+ import { PrometheusAdapter } from '@jetit/publisher';
186
+ import promClient from 'prom-client';
187
+ import express from 'express';
188
+
189
+ const app = express();
190
+ const prometheusAdapter = new PrometheusAdapter(publisher, promClient);
191
+
192
+ prometheusAdapter.setupEndpoint(app, '/metrics');
193
+
194
+ app.listen(3000, () => {
195
+ console.log('Metrics server listening on port 3000');
196
+ });
197
+ ```
198
+
199
+ ## Advanced Features
200
+
201
+ ### Content-Based Deduplication
202
+
203
+ The library supports content-based deduplication to ensure that each unique event is processed only once:
204
+
205
+ ```typescript
206
+ const options = {
207
+ publishOnceGuarantee: true
208
+ };
209
+
210
+ publisher.listen('important-event', options).subscribe(event => {
211
+ console.log('Guaranteed unique event:', event);
182
212
  });
183
213
  ```
184
214
 
185
- ## Possible use cases
215
+ ### Multiple Event Subscriptions
186
216
 
187
- 1. Microservices communication: If your system is composed of multiple microservices, the publisher can be used to facilitate communication between them by publishing and listening to events.
217
+ You can subscribe to multiple events from the same service:
188
218
 
189
- 2. Event sourcing and CQRS: In an event-sourced system, the publisher can be used to store and process events that represent the state changes of the system, enabling Command Query Responsibility Segregation (CQRS) by separating the read and write models.
219
+ ```typescript
220
+ const subscription1 = publisher.listen('event1').subscribe(/* ... */);
221
+ const subscription2 = publisher.listen('event2').subscribe(/* ... */);
222
+ ```
190
223
 
191
- 3. Task queues: The publisher can be used to create task queues for distributing workloads among different worker instances, ensuring that tasks are processed in the order they were created.
224
+ ### Circuit Breaker
192
225
 
193
- 4. Data streaming and processing: The publisher can be used to ingest and process large volumes of data in real-time, such as log files, clickstream data, or other event-based data.
226
+ The Circuit Breaker pattern is implemented to prevent cascading failures in a distributed system. It helps to gracefully handle failures and allows the system to recover without overwhelming failed services.
194
227
 
195
- 5. Distributed system coordination: In a distributed system, the publisher can be used for coordination between different components, such as managing leader election or maintaining configuration information.
228
+ Configuration options:
196
229
 
197
- 6. Real-time analytics and monitoring: The publisher can be used to collect and process real-time analytics data, such as user behavior, application performance metrics, or system monitoring information.
230
+ - `enabled`: Enable or disable the Circuit Breaker.
231
+ - `errorThreshold`: Number of errors before opening the circuit.
232
+ - `errorThresholdPercentage`: Percentage of errors to total calls before opening the circuit.
233
+ - `timeWindow`: Time window for error rate calculation (in milliseconds).
234
+ - `openStateDuration`: Duration to keep the circuit open before moving to half-open state (in milliseconds).
235
+ - `halfOpenStateMaxAttempts`: Maximum number of attempts allowed in half-open state.
236
+
237
+ The Circuit Breaker has three states:
238
+
239
+ 1. Closed: Normal operation, calls pass through.
240
+ 2. Open: Calls are immediately rejected without reaching the service.
241
+ 3. Half-Open: A limited number of calls are allowed to test if the service has recovered.
242
+
243
+
244
+ ## Performance Optimizations
245
+
246
+ - Batched `xdel` operations for improved cleanup performance
247
+ - Configurable cleanup intervals and thresholds
248
+ - Efficient event filtering at the subscription level
249
+ - Retry logic with exponential backoff for failed operations
250
+ - Circuit Breaker to prevent overwhelming failed services
251
+ - Dead Letter Queue (DLQ) for handling subscription failures
252
+
253
+ ## Cleanup and Graceful Shutdown
254
+
255
+ To ensure proper cleanup of resources, implement a graceful shutdown:
256
+
257
+ ```typescript
258
+ process.on('SIGTERM', shutdown);
259
+ process.on('SIGINT', shutdown);
260
+
261
+ async function shutdown() {
262
+ console.log('Graceful shutdown initiated.');
263
+ try {
264
+ await publisher.close();
265
+ console.log('Resources and connections successfully closed.');
266
+ } catch (error) {
267
+ console.error('Error during graceful shutdown:', error);
268
+ }
269
+ process.exit(0);
270
+ }
271
+ ```
198
272
 
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.
273
+ ## Troubleshooting
200
274
 
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.
275
+ If you encounter issues:
202
276
 
203
- 9. Multicast Publishing: This is the existing PUB/SUB implementation but with the event data being stored into streams for additional processing
277
+ 1. Check the Redis connection settings
278
+ 2. Verify that consumer groups are correctly created
279
+ 3. Monitor the DLQ for failed events
280
+ 4. Review the performance metrics for any anomalies
281
+ 5. Check the logs for detailed error messages
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "4.1.1",
3
+ "version": "5.1.1",
4
4
  "type": "commonjs",
5
5
  "dependencies": {
6
6
  "@jetit/id": "^0.0.12",
7
+ "events": "3.3.0",
7
8
  "ioredis": "^5.3.0",
8
9
  "rxjs": "^7.8.0",
9
- "tslib": "1.14.1"
10
+ "tslib": "2.5.0"
10
11
  },
11
12
  "main": "./src/index.js"
12
13
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * One of the horribly typed code I have written. But I dont see
3
+ * another option without importing other libraries in. So this
4
+ * works well when the right stuff is passed in for the promClient
5
+ * and the web server.
6
+ */
7
+ import { Streams as Publisher } from '../../redis/streams';
8
+ export declare class PrometheusAdapter {
9
+ private streams;
10
+ private promClient;
11
+ private registry;
12
+ private queueDepth;
13
+ private dlqSize;
14
+ private dlqRate;
15
+ private operationCount;
16
+ private totalTime;
17
+ private redisOperationTime;
18
+ private processingTime;
19
+ private eventCount;
20
+ private publishErrorCount;
21
+ private subscribeErrorCount;
22
+ private individualQueueDepth;
23
+ private duplicateEventCount;
24
+ private messageRatePublish;
25
+ private messageRateSubscribe;
26
+ private processingTimeHistogram;
27
+ private redisCommandLatency;
28
+ private consumerLag;
29
+ /**
30
+ *
31
+ * @param streams [Publisher]
32
+ * @param promClient [Prom Client] This needs to be an instance of prom-client
33
+ */
34
+ constructor(streams: Publisher, promClient: any);
35
+ private initializeMetrics;
36
+ updateMetrics(): Promise<void>;
37
+ private updatePrometheusMetrics;
38
+ /**
39
+ * @param app This needs to be an instance of express or fastify something that supports the express api
40
+ */
41
+ setupEndpoint(app: any, endPoint?: string): void;
42
+ }