@jetit/publisher 5.6.3 → 6.0.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 +68 -0
- package/package.json +7 -5
- package/src/lib/publisher.d.ts +1 -0
- package/src/lib/publisher.js +3 -1
- package/src/lib/redis/scheduler.js +1 -1
- package/src/lib/redis/streams-lite.d.ts +187 -0
- package/src/lib/redis/streams-lite.js +734 -0
- package/src/lib/redis/streams.d.ts +24 -0
- package/src/lib/redis/streams.js +347 -40
- package/src/lib/redis/types.d.ts +3 -0
- package/src/lib/redis/utils.js +1 -1
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:
|
|
@@ -97,6 +125,7 @@ const config: Partial<IStreamsConfig> = {
|
|
|
97
125
|
halfOpenStateMaxAttempts: 10,
|
|
98
126
|
maxStoredEvents: 5000,
|
|
99
127
|
},
|
|
128
|
+
maxPendingTasks: 1000, // Max concurrent pending retry tasks per event per consumer (default: 1000, 0 = unbounded)
|
|
100
129
|
};
|
|
101
130
|
|
|
102
131
|
const publisher = new Publisher('MyService', config);
|
|
@@ -240,6 +269,43 @@ app.listen(3000, () => {
|
|
|
240
269
|
});
|
|
241
270
|
```
|
|
242
271
|
|
|
272
|
+
## Publisher vs PublisherLite
|
|
273
|
+
|
|
274
|
+
This library offers two main publisher implementations: `Publisher` and `PublisherLite`. Choose the one that best fits your architecture and scaling needs.
|
|
275
|
+
|
|
276
|
+
**`Publisher` (Default - Multi-Stream)**
|
|
277
|
+
|
|
278
|
+
* **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`).
|
|
279
|
+
* **Pros:**
|
|
280
|
+
* Provides strong isolation between consumer groups. Issues or heavy load in one group's stream don't directly impact others.
|
|
281
|
+
* Potentially simpler cleanup logic per stream.
|
|
282
|
+
* May offer better performance distribution if consumer groups have vastly different processing speeds or volumes.
|
|
283
|
+
* **Cons:**
|
|
284
|
+
* Can lead to a large number of streams in Redis, especially with many event types and consumer groups, increasing memory usage and management overhead.
|
|
285
|
+
* Requires more complex ID generation (like the adaptive Lua script) to handle potential ID collisions across streams when publishing.
|
|
286
|
+
* **Use When:**
|
|
287
|
+
* You have a manageable number of consumer groups per event type.
|
|
288
|
+
* Strong isolation between consumer groups is critical.
|
|
289
|
+
* You anticipate significant differences in load or processing characteristics between consumer groups for the same event.
|
|
290
|
+
|
|
291
|
+
**`PublisherLite` (Single-Stream)**
|
|
292
|
+
|
|
293
|
+
* **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.
|
|
294
|
+
* **Pros:**
|
|
295
|
+
* Significantly reduces the number of streams in Redis, lowering memory usage and simplifying management.
|
|
296
|
+
* Simplifies the publishing logic (no need for complex multi-stream ID generation).
|
|
297
|
+
* Aligns more closely with the standard Redis Streams consumer group model.
|
|
298
|
+
* **Cons:**
|
|
299
|
+
* 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).
|
|
300
|
+
* Cleanup logic (`XTRIM`) is slightly more complex as it needs to consider the progress of *all* consumer groups on the stream before trimming messages.
|
|
301
|
+
* **Use When:**
|
|
302
|
+
* You have a large number of event types or expect many consumer groups per event.
|
|
303
|
+
* Reducing Redis resource consumption (memory, keyspace) is a priority.
|
|
304
|
+
* You prefer a simpler publishing mechanism and are comfortable with the standard Redis consumer group behavior on a shared stream.
|
|
305
|
+
* **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.
|
|
306
|
+
|
|
307
|
+
**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.
|
|
308
|
+
|
|
243
309
|
## Advanced Features
|
|
244
310
|
|
|
245
311
|
### Content-Based Deduplication
|
|
@@ -293,6 +359,8 @@ The Circuit Breaker has three states:
|
|
|
293
359
|
- Retry logic with exponential backoff for failed operations
|
|
294
360
|
- Circuit Breaker to prevent overwhelming failed services
|
|
295
361
|
- Dead Letter Queue (DLQ) for handling subscription failures
|
|
362
|
+
- **Pending Retry Task Cap**: Limits the number of concurrent in-flight pending message retry tasks per event per consumer instance. Prevents unbounded memory growth and Redis saturation under sustained load. Configurable via `maxPendingTasks` (default: 1000). Set to `0` to disable the cap (unbounded, pre-6.0.1 behavior). When the cap is reached, a rate-limited warning is logged and skipped messages are retried on the next Pub/Sub trigger.
|
|
363
|
+
- **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
364
|
|
|
297
365
|
## Cleanup and Graceful Shutdown
|
|
298
366
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetit/publisher",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
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
|
-
"
|
|
7
|
-
"ioredis": "^5.3.0",
|
|
8
|
-
"rxjs": "^7.8.0",
|
|
9
|
-
"tslib": "2.7.0"
|
|
11
|
+
"tslib": "2.8.1"
|
|
10
12
|
},
|
|
11
13
|
"types": "./src/index.d.ts",
|
|
12
14
|
"main": "./src/index.js"
|
package/src/lib/publisher.d.ts
CHANGED
|
@@ -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';
|
package/src/lib/publisher.js
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|