@jetit/publisher 5.6.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,7 @@
19
19
  - [Event Filtering](#event-filtering)
20
20
  - [Performance Monitoring](#performance-monitoring)
21
21
  - [Prometheus Integration](#prometheus-integration)
22
+ - [Publisher vs PublisherLite](#publisher-vs-publisherlite)
22
23
  - [Advanced Features](#advanced-features)
23
24
  - [Content-Based Deduplication](#content-based-deduplication)
24
25
  - [Multiple Event Subscriptions](#multiple-event-subscriptions)
@@ -72,6 +73,33 @@ publisher.listen('my-event').subscribe(event => {
72
73
  });
73
74
  ```
74
75
 
76
+ ### PublisherLite Usage
77
+
78
+ For scenarios where a single stream per event type is preferred (allowing multiple consumer groups on that single stream), use `PublisherLite`:
79
+
80
+ ```typescript
81
+ import { PublisherLite, EventData } from '@jetit/publisher'; // Import PublisherLite
82
+
83
+ // Create an instance of PublisherLite
84
+ const publisherLite = new PublisherLite('MyServiceLite');
85
+
86
+ // Publish an event (same as Publisher)
87
+ const eventData: EventData<{ message: string }> = {
88
+ eventName: 'my-lite-event',
89
+ data: { message: 'Hello, Lite world!' }
90
+ };
91
+ await publisherLite.publish(eventData);
92
+
93
+ // Subscribe to an event (same as Publisher)
94
+ publisherLite.listen('my-lite-event').subscribe(event => {
95
+ console.log(`Received lite event: ${event.eventName}`, event.data);
96
+ });
97
+
98
+ // Configuration is also similar, just pass it to PublisherLite constructor
99
+ const liteConfig: Partial<IStreamsConfig> = { /* ... your config ... */ };
100
+ const configuredPublisherLite = new PublisherLite('MyConfiguredServiceLite', liteConfig, 'my-redis-connection');
101
+ ```
102
+
75
103
  ### Configuration
76
104
 
77
105
  The `Publisher` class can be configured with various options, including Circuit Breaker and Backpressure handling:
@@ -240,6 +268,43 @@ app.listen(3000, () => {
240
268
  });
241
269
  ```
242
270
 
271
+ ## Publisher vs PublisherLite
272
+
273
+ This library offers two main publisher implementations: `Publisher` and `PublisherLite`. Choose the one that best fits your architecture and scaling needs.
274
+
275
+ **`Publisher` (Default - Multi-Stream)**
276
+
277
+ * **Mechanism:** Creates a separate Redis Stream for *each* consumer group subscribing to an event type (e.g., `my-event:cg-serviceA`, `my-event:cg-serviceB`).
278
+ * **Pros:**
279
+ * Provides strong isolation between consumer groups. Issues or heavy load in one group's stream don't directly impact others.
280
+ * Potentially simpler cleanup logic per stream.
281
+ * May offer better performance distribution if consumer groups have vastly different processing speeds or volumes.
282
+ * **Cons:**
283
+ * Can lead to a large number of streams in Redis, especially with many event types and consumer groups, increasing memory usage and management overhead.
284
+ * Requires more complex ID generation (like the adaptive Lua script) to handle potential ID collisions across streams when publishing.
285
+ * **Use When:**
286
+ * You have a manageable number of consumer groups per event type.
287
+ * Strong isolation between consumer groups is critical.
288
+ * You anticipate significant differences in load or processing characteristics between consumer groups for the same event.
289
+
290
+ **`PublisherLite` (Single-Stream)**
291
+
292
+ * **Mechanism:** Uses a *single* Redis Stream per event type (prefixed with `sl:`, e.g., `sl:my-event`). All consumer groups for that event read from this single stream.
293
+ * **Pros:**
294
+ * Significantly reduces the number of streams in Redis, lowering memory usage and simplifying management.
295
+ * Simplifies the publishing logic (no need for complex multi-stream ID generation).
296
+ * Aligns more closely with the standard Redis Streams consumer group model.
297
+ * **Cons:**
298
+ * Less isolation between consumer groups; a very slow or problematic consumer group could potentially impact the processing lag for others on the same stream (though Redis handles much of this internally).
299
+ * Cleanup logic (`XTRIM`) is slightly more complex as it needs to consider the progress of *all* consumer groups on the stream before trimming messages.
300
+ * **Use When:**
301
+ * You have a large number of event types or expect many consumer groups per event.
302
+ * Reducing Redis resource consumption (memory, keyspace) is a priority.
303
+ * You prefer a simpler publishing mechanism and are comfortable with the standard Redis consumer group behavior on a shared stream.
304
+ * **Important:** `PublisherLite` currently focuses on the core publish/listen flow and **does not support scheduled publishing** (`scheduledPublish` method). Use the standard `Publisher` if you require scheduled events.
305
+
306
+ **Note:** Both `Publisher` and `PublisherLite` share the same configuration options (`IStreamsConfig`) and core features like DLQ, Circuit Breaker, Metrics, etc. The primary difference lies in the underlying Redis Stream structure they use and the support for scheduled publishing.
307
+
243
308
  ## Advanced Features
244
309
 
245
310
  ### Content-Based Deduplication
@@ -293,6 +358,7 @@ The Circuit Breaker has three states:
293
358
  - Retry logic with exponential backoff for failed operations
294
359
  - Circuit Breaker to prevent overwhelming failed services
295
360
  - Dead Letter Queue (DLQ) for handling subscription failures
361
+ - **Adaptive Redis Stream ID Generation (Publisher only)**: The default `Publisher` automatically switches to an optimized ID generation strategy using a Lua script when publishing to many consumer groups (>10 by default) or when ID conflicts are detected. This prevents `XADD` errors related to non-monotonic IDs in high-throughput scenarios. Configurable via `optimizationThreshold` and `optimizationDurationMs`. (`PublisherLite` does not include this complex logic).
296
362
 
297
363
  ## Cleanup and Graceful Shutdown
298
364
 
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@jetit/publisher",
3
- "version": "5.6.3",
3
+ "version": "6.0.0",
4
4
  "type": "commonjs",
5
+ "peerDependencies": {
6
+ "@jetit/id": ">=0.0.14",
7
+ "ioredis": ">=5.0.0",
8
+ "rxjs": ">=7.0.0"
9
+ },
5
10
  "dependencies": {
6
- "@jetit/id": "^0.0.13",
7
- "ioredis": "^5.3.0",
8
- "rxjs": "^7.8.0",
11
+ "nanoid": "^5.0.9",
9
12
  "tslib": "2.7.0"
10
13
  },
11
14
  "types": "./src/index.d.ts",
@@ -1,5 +1,6 @@
1
1
  export { setRedisConnectionSettings as setRedisConfig } from './redis/registry';
2
2
  export { Streams as Publisher } from './redis/streams';
3
+ export { StreamsLite as PublisherLite } from './redis/streams-lite';
3
4
  export { ScheduledProcessor as __SCHEDULER_INTERNALS__ } from './redis/scheduler';
4
5
  export { UTILS as StreamUtilityFunctions } from './redis/utils';
5
6
  export { PrometheusAdapter } from './monitoring/adapters/prom';
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.publishScheduledBatch = exports.publishBatch = exports.PrometheusAdapter = exports.StreamUtilityFunctions = exports.__SCHEDULER_INTERNALS__ = exports.Publisher = exports.setRedisConfig = void 0;
3
+ exports.publishScheduledBatch = exports.publishBatch = exports.PrometheusAdapter = exports.StreamUtilityFunctions = exports.__SCHEDULER_INTERNALS__ = exports.PublisherLite = exports.Publisher = exports.setRedisConfig = void 0;
4
4
  var registry_1 = require("./redis/registry");
5
5
  Object.defineProperty(exports, "setRedisConfig", { enumerable: true, get: function () { return registry_1.setRedisConnectionSettings; } });
6
6
  var streams_1 = require("./redis/streams");
7
7
  Object.defineProperty(exports, "Publisher", { enumerable: true, get: function () { return streams_1.Streams; } });
8
+ var streams_lite_1 = require("./redis/streams-lite");
9
+ Object.defineProperty(exports, "PublisherLite", { enumerable: true, get: function () { return streams_lite_1.StreamsLite; } });
8
10
  var scheduler_1 = require("./redis/scheduler");
9
11
  Object.defineProperty(exports, "__SCHEDULER_INTERNALS__", { enumerable: true, get: function () { return scheduler_1.ScheduledProcessor; } });
10
12
  var utils_1 = require("./redis/utils");
@@ -63,7 +63,7 @@ class ScheduledProcessor {
63
63
  .xadd(streamName, key, 'data', JSON.stringify(eventData))
64
64
  .catch((e) => logger_1.PUBLISHER_LOGGER.error(`PUBLISHER: Publishing event ${eventData.eventName} to consumer groups: ${consumerGroups.join(', ')} failed with data ${JSON.stringify(eventData)}, ${e} `));
65
65
  if (key === '*')
66
- key = `${generatedKey ?? key}`;
66
+ key = generatedKey || key;
67
67
  }
68
68
  if (eventData.repeatInterval) {
69
69
  const nextEventTime = currentTime + eventData.repeatInterval;
@@ -0,0 +1,187 @@
1
+ import { Observable } from 'rxjs';
2
+ import { IAggregatedMetrics, TQueryableMetrics } from '../monitoring/types';
3
+ import { CircuitState } from '../performance/circuit_breaker';
4
+ import { EventData, IListenOptions, IStreamsConfig, PublishData } from './types';
5
+ export declare class StreamsLite {
6
+ private redisConnectionId;
7
+ private _redisPublisher?;
8
+ private _redisGroups?;
9
+ private config;
10
+ private dlq;
11
+ private metricsCollector;
12
+ private consumerGroupName;
13
+ private instanceId;
14
+ private instanceUniqueId;
15
+ private cleanUpTimer;
16
+ private eventsListened;
17
+ private subscriptions;
18
+ private duplicateChecker;
19
+ private circuitBreaker;
20
+ private get redisPublisher();
21
+ private get redisGroups();
22
+ private DEFAULT_STREAMS_CONFIG;
23
+ /**
24
+ * Creates a new Streams instance for a given service.
25
+ *
26
+ * The constructor initializes the Redis connections for publishers, subscribers, and consumer groups.
27
+ * It also sets up an interval timer for clearing expired messages from Redis and another interval timer
28
+ * for processing scheduled events at regular intervals.
29
+ *
30
+ * @param serviceName - A unique name for the service that will be using this Streams instance.
31
+ *
32
+ * @example
33
+ *
34
+ * // Create a new Streams instance for the "POS" service
35
+ * const streams = new Streams('POS');
36
+ */
37
+ constructor(serviceName: string, config?: Partial<IStreamsConfig>, redisConnectionId?: string);
38
+ private setupCircuitBreakerListeners;
39
+ private runClear;
40
+ /**
41
+ * Publishes an event with the given data to the Redis event stream.
42
+ *
43
+ * The method generates a unique event ID for each event, and adds it to the event data to handle message
44
+ * deduplication.
45
+ *
46
+ * @param data - An EventData<T> object containing the data for the event.
47
+ *
48
+ * @returns A Promise that resolves when the event has been published to the Redis stream.
49
+ *
50
+ * @example
51
+ *
52
+ * // Publish an "order.created" event with the given order data
53
+ * const orderData = { id: '123', customerName: 'John Doe', amount: 100 };
54
+ * const eventData = { eventName: 'order.created', data: orderData };
55
+ * await publisher.publish(eventData);
56
+ */
57
+ publish<TData = unknown, TName extends string = string>(data: PublishData<TData, TName>, multicast?: boolean): Promise<string>;
58
+ /**
59
+ * Listens for events with the given name and returns an Observable that emits an EventData<T> object
60
+ * each time a new event is received.
61
+ *
62
+ * The method uses a BehaviorSubject to emit the events as Observables. The BehaviorSubject ensures
63
+ * that new subscribers receive the last emitted event, even if they subscribe after the event has been emitted.
64
+ *
65
+ * If an error occurs while subscribing, the method logs the error to the PUBLISHER_LOGGER and throws
66
+ * an error. This is done to prevent the service from continuing without a proper event subscription.
67
+ *
68
+ * There is retry logic with exponential backoff to handle error cases. These are also controllable by the
69
+ * calling service
70
+ *
71
+ * @param eventName - The name of the event to listen for.
72
+ *
73
+ * @returns An Observable that emits an EventData<T> object each time a new event is received.
74
+ *
75
+ * @example
76
+ *
77
+ * // Listen for "order.created" events
78
+ * const orderCreated = streams.listen<OrderCreatedEvent>('order.created');
79
+ *
80
+ * // Subscribe to the Observable and log each new event
81
+ * orderCreated.subscribe((event) => {
82
+ * PUBLISHER_LOGGER.log('New order created:', event.data);
83
+ * });
84
+ */
85
+ listen<T = unknown, const TName extends string = string>(eventName: TName, listenerOptions?: IListenOptions<T>): Observable<EventData<T, TName>>;
86
+ private createConsumerAndRegister;
87
+ private listenInternals;
88
+ /**
89
+ * This method allows the possibility of a graceful shutdown by cleaning up the
90
+ * redis connections.
91
+ *
92
+ * In all services where the library is used, its better to implement this method
93
+ *
94
+ * process.on('SIGTERM', shutdown);
95
+ * process.on('SIGINT', shutdown);
96
+ *
97
+ * async function shutdown(): Promise<void> {
98
+ * PUBLISHER_LOGGER.log('Graceful shutdown initiated.');
99
+ * try {
100
+ * await streams.close();
101
+ * PUBLISHER_LOGGER.log('Resources and connections successfully closed.');
102
+ * } catch (error) {
103
+ * PUBLISHER_LOGGER.error('Error during graceful shutdown:', error);
104
+ * }
105
+ * process.exit(0);
106
+ * }
107
+ */
108
+ close(): Promise<void>;
109
+ private cleanupAcknowledgedMessages;
110
+ getDiagnosticData(events: string[]): Promise<{
111
+ status: string;
112
+ message: string;
113
+ data?: undefined;
114
+ } | {
115
+ status: string;
116
+ data: {
117
+ eventName: string;
118
+ streamName: string;
119
+ consumerGroupMap: {
120
+ consumerGroup: string;
121
+ diagnostics: Generator<Promise<unknown[]> | {
122
+ count: number;
123
+ consumers: {
124
+ consumerName: string;
125
+ pendingCount: string;
126
+ }[];
127
+ }, void, [number, string, string, string[][]]>;
128
+ }[];
129
+ }[];
130
+ message: string;
131
+ }>;
132
+ private logPerformance;
133
+ /**
134
+ * @description
135
+ * This method is use to retry an event that has ended in the dead letter queue,
136
+ * which happens after the first retry.
137
+ */
138
+ retryFromDLQ(eventId: string): Promise<boolean>;
139
+ /**
140
+ * @description
141
+ * This returns the number of items and the rate at which events are added
142
+ * to the queue. The queue is global and hence remains as is
143
+ */
144
+ getDLQStats(): Promise<{
145
+ size: number;
146
+ additionRate: number;
147
+ }>;
148
+ private removeSubscription;
149
+ /**
150
+ * @description
151
+ * This is a simple helper utility that can be used externally to create alerts based
152
+ * on thresholds that can be provided into the function. It returns true/false for each
153
+ * key that is provided. Not all keys are required
154
+ */
155
+ checkThresholds(thresholds: Partial<TQueryableMetrics>): Promise<Record<string, boolean>>;
156
+ /**
157
+ * @description
158
+ * This will return you the stats of the publisher for the last 6 hours after cleaning
159
+ */
160
+ getMetrics(startTime: number, endTime: number): Promise<IAggregatedMetrics[]>;
161
+ /**
162
+ * @description
163
+ * This will return you the latest stats of the publisher
164
+ */
165
+ getLatestMetrics(): Promise<IAggregatedMetrics | null>;
166
+ /**
167
+ * @description
168
+ * This returns the status of the performance control setup. This includes
169
+ * the circuit breaker
170
+ */
171
+ getPerformanceControlStatus(): Promise<{
172
+ circuitBreakerState: CircuitState;
173
+ }>;
174
+ /**
175
+ * @description
176
+ * This is a manual control to process stored events in case the
177
+ * circuit is OPEN
178
+ */
179
+ processStoredEvents(): Promise<void>;
180
+ /**
181
+ * Acknowledges a message and updates the last acknowledged message ID for the specific consumer group.
182
+ * This is used to track cleanup progress and ensure we don't delete unprocessed messages.
183
+ */
184
+ acknowledgeMessage(ackKey: string): Promise<void>;
185
+ private frameMessageKey;
186
+ private demergeMessageKey;
187
+ }