@jetit/publisher 4.1.0 → 5.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.
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsCollector = void 0;
4
+ class MetricsCollector {
5
+ constructor(config, dlq) {
6
+ this.metrics = [];
7
+ this.metricsKey = 'publisher:metrics';
8
+ this.redisClient = config.redisClient;
9
+ this.collectionInterval = config.collectionInterval;
10
+ this.retentionPeriod = config.retentionPeriod;
11
+ this.dlq = dlq;
12
+ setInterval(() => this.aggregateAndStoreMetrics(), this.collectionInterval);
13
+ }
14
+ addMetrics(metrics) {
15
+ this.metrics.push(metrics);
16
+ }
17
+ async aggregateAndStoreMetrics() {
18
+ if (this.metrics.length === 0)
19
+ return;
20
+ const queueDepths = await this.getQueueDepth();
21
+ const aggregated = {
22
+ timestamp: Date.now(),
23
+ totalTime: 0,
24
+ redisOperationTime: 0,
25
+ processingTime: 0,
26
+ eventCount: 0,
27
+ subscribeErrorCount: 0,
28
+ publishErrorCount: 0,
29
+ queueDepth: queueDepths.total,
30
+ individualQueueDepts: queueDepths.individual,
31
+ dlqSize: 0,
32
+ dlqRate: 0,
33
+ operationCount: this.metrics.length,
34
+ duplicateEventsCount: 0,
35
+ };
36
+ for (const metric of this.metrics) {
37
+ aggregated.totalTime += metric.totalTime;
38
+ aggregated.redisOperationTime += metric.redisOperationTime;
39
+ aggregated.processingTime += metric.processingTime;
40
+ aggregated.eventCount += metric.eventCount;
41
+ aggregated.publishErrorCount += metric.publishErrorCount;
42
+ aggregated.subscribeErrorCount += metric.subscribeErrorCount;
43
+ aggregated.duplicateEventsCount += metric.duplicateEventsCount;
44
+ }
45
+ // Calculate averages for time-based metrics
46
+ aggregated.totalTime /= aggregated.operationCount;
47
+ aggregated.redisOperationTime /= aggregated.operationCount;
48
+ aggregated.processingTime /= aggregated.operationCount;
49
+ const dlqStats = await this.dlq.getDLQStats();
50
+ aggregated.dlqSize = dlqStats.size;
51
+ aggregated.dlqRate = dlqStats.additionRate;
52
+ await this.storeMetrics(aggregated);
53
+ this.metrics = [];
54
+ }
55
+ async getQueueDepth() {
56
+ let totalDepth = 0;
57
+ const individualDepths = {};
58
+ let cursor = '0';
59
+ do {
60
+ const [nextCursor, keys] = await this.redisClient.scan(cursor, 'MATCH', '*:cg-*', 'COUNT', 100);
61
+ cursor = nextCursor;
62
+ if (keys.length > 0) {
63
+ const pipeline = this.redisClient.pipeline();
64
+ keys.forEach((key) => {
65
+ pipeline.xlen(key);
66
+ pipeline.zcard(`ack:${key}`);
67
+ });
68
+ const results = await pipeline.exec();
69
+ if (!results) {
70
+ continue;
71
+ }
72
+ for (let i = 0; i < results.length; i += 2) {
73
+ const key = keys[i / 2];
74
+ const [streamLengthErr, streamLength] = results[i];
75
+ const [ackCountErr, ackCount] = results[i + 1];
76
+ if (streamLengthErr) {
77
+ console.error(`Error getting length for key: ${streamLengthErr}`);
78
+ continue;
79
+ }
80
+ if (ackCountErr) {
81
+ console.error(`Error getting ack count for key: ${ackCountErr}`);
82
+ continue;
83
+ }
84
+ const queueDepth = Math.max(0, streamLength - ackCount);
85
+ totalDepth += queueDepth;
86
+ individualDepths[key] = queueDepth;
87
+ }
88
+ }
89
+ } while (cursor !== '0');
90
+ return { total: totalDepth, individual: individualDepths };
91
+ }
92
+ async storeMetrics(metrics) {
93
+ const score = metrics.timestamp;
94
+ const member = JSON.stringify(metrics);
95
+ await this.redisClient.zadd(this.metricsKey, score, member);
96
+ // Remove old metrics based on retention period
97
+ const cutoffTime = Date.now() - this.retentionPeriod;
98
+ await this.redisClient.zremrangebyscore(this.metricsKey, 0, cutoffTime);
99
+ }
100
+ async getMetrics(startTime, endTime) {
101
+ const metricsData = await this.redisClient.zrangebyscore(this.metricsKey, startTime, endTime);
102
+ return metricsData.map((data) => JSON.parse(data));
103
+ }
104
+ async getLatestMetrics() {
105
+ const latestMetrics = await this.redisClient.zrevrange(this.metricsKey, 0, 0);
106
+ return latestMetrics.length > 0 ? JSON.parse(latestMetrics[0]) : null;
107
+ }
108
+ }
109
+ exports.MetricsCollector = MetricsCollector;
@@ -0,0 +1,15 @@
1
+ import { IPerformanceMetrics } from './types';
2
+ export declare class MetricsTracker {
3
+ private startTime;
4
+ private metrics;
5
+ constructor();
6
+ startRedisOperation(): void;
7
+ endRedisOperation(): void;
8
+ startProcessing(): void;
9
+ endProcessing(): void;
10
+ incrementEventCount(): void;
11
+ incrementDuplicateEvent(): void;
12
+ incrementErrorCount(type: 'publish' | 'subscribe'): void;
13
+ getMetrics(): IPerformanceMetrics;
14
+ reset(): void;
15
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MetricsTracker = void 0;
4
+ const node_process_1 = require("node:process");
5
+ class MetricsTracker {
6
+ constructor() {
7
+ this.metrics = {
8
+ totalTime: 0,
9
+ redisOperationTime: 0,
10
+ processingTime: 0,
11
+ eventCount: 0,
12
+ publishErrorCount: 0,
13
+ subscribeErrorCount: 0,
14
+ duplicateEventsCount: 0,
15
+ };
16
+ this.startTime = (0, node_process_1.hrtime)();
17
+ }
18
+ startRedisOperation() {
19
+ this.startTime = (0, node_process_1.hrtime)();
20
+ }
21
+ endRedisOperation() {
22
+ const [seconds, nanoseconds] = (0, node_process_1.hrtime)(this.startTime);
23
+ this.metrics.redisOperationTime += seconds * 1000 + nanoseconds / 1e6;
24
+ }
25
+ startProcessing() {
26
+ this.startTime = (0, node_process_1.hrtime)();
27
+ }
28
+ endProcessing() {
29
+ const [seconds, nanoseconds] = (0, node_process_1.hrtime)(this.startTime);
30
+ this.metrics.processingTime += seconds * 1000 + nanoseconds / 1e6;
31
+ }
32
+ incrementEventCount() {
33
+ this.metrics.eventCount++;
34
+ }
35
+ incrementDuplicateEvent() {
36
+ this.metrics.duplicateEventsCount++;
37
+ }
38
+ incrementErrorCount(type) {
39
+ this.metrics[`${type}ErrorCount`]++;
40
+ }
41
+ getMetrics() {
42
+ const [totalSeconds, totalNanoseconds] = (0, node_process_1.hrtime)(this.startTime);
43
+ this.metrics.totalTime = totalSeconds * 1000 + totalNanoseconds / 1e6;
44
+ return { ...this.metrics };
45
+ }
46
+ reset() {
47
+ this.startTime = (0, node_process_1.hrtime)();
48
+ this.metrics = {
49
+ totalTime: 0,
50
+ redisOperationTime: 0,
51
+ processingTime: 0,
52
+ eventCount: 0,
53
+ publishErrorCount: 0,
54
+ subscribeErrorCount: 0,
55
+ duplicateEventsCount: 0,
56
+ };
57
+ }
58
+ }
59
+ exports.MetricsTracker = MetricsTracker;
@@ -0,0 +1,37 @@
1
+ import { RedisType } from '../redis/registry';
2
+ export interface IAggregatedMetrics extends IPerformanceMetrics {
3
+ timestamp: number;
4
+ queueDepth: number;
5
+ dlqSize: number;
6
+ dlqRate: number;
7
+ operationCount: number;
8
+ totalTime: number;
9
+ redisOperationTime: number;
10
+ processingTime: number;
11
+ eventCount: number;
12
+ publishErrorCount: number;
13
+ subscribeErrorCount: number;
14
+ individualQueueDepts: IQueueDepths['individual'];
15
+ duplicateEventsCount: number;
16
+ }
17
+ export interface IMetricsCollectorConfig {
18
+ redisClient: RedisType;
19
+ collectionInterval: number;
20
+ retentionPeriod: number;
21
+ }
22
+ export type TQueryableMetrics = Omit<IAggregatedMetrics, 'timestamp' | 'individualQueueDepts'>;
23
+ export interface IQueueDepths {
24
+ total: number;
25
+ individual: {
26
+ [key: string]: number;
27
+ };
28
+ }
29
+ export interface IPerformanceMetrics {
30
+ totalTime: number;
31
+ redisOperationTime: number;
32
+ processingTime: number;
33
+ eventCount: number;
34
+ publishErrorCount: number;
35
+ subscribeErrorCount: number;
36
+ duplicateEventsCount: number;
37
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,29 @@
1
+ /// <reference types="node" />
2
+ import { EventEmitter } from 'events';
3
+ import { RedisType } from '../redis/registry';
4
+ import { EventData, IStreamsConfig } from '../redis/types';
5
+ export declare enum CircuitState {
6
+ CLOSED = 0,
7
+ OPEN = 1,
8
+ HALF_OPEN = 2
9
+ }
10
+ export declare class CircuitBreaker extends EventEmitter {
11
+ private readonly config;
12
+ private state;
13
+ private failureCount;
14
+ private successCount;
15
+ private lastStateChange;
16
+ private readonly redisClient;
17
+ constructor(config: IStreamsConfig['circuitBreaker'], redisClient: RedisType);
18
+ recordSuccess(): Promise<void>;
19
+ recordFailure(): Promise<void>;
20
+ isAllowed(): Promise<boolean>;
21
+ private openCircuit;
22
+ private closeCircuit;
23
+ private halfOpenCircuit;
24
+ getState(): CircuitState;
25
+ storeEvent(event: EventData<unknown, string>): Promise<void>;
26
+ getStoredEvents(): Promise<EventData<unknown, string>[]>;
27
+ clearStoredEvents(): Promise<void>;
28
+ private startStateCheck;
29
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CircuitBreaker = exports.CircuitState = void 0;
4
+ const events_1 = require("events");
5
+ var CircuitState;
6
+ (function (CircuitState) {
7
+ CircuitState[CircuitState["CLOSED"] = 0] = "CLOSED";
8
+ CircuitState[CircuitState["OPEN"] = 1] = "OPEN";
9
+ CircuitState[CircuitState["HALF_OPEN"] = 2] = "HALF_OPEN";
10
+ })(CircuitState || (exports.CircuitState = CircuitState = {}));
11
+ class CircuitBreaker extends events_1.EventEmitter {
12
+ constructor(config, redisClient) {
13
+ super();
14
+ this.config = config;
15
+ this.state = CircuitState.CLOSED;
16
+ this.failureCount = 0;
17
+ this.successCount = 0;
18
+ this.lastStateChange = Date.now();
19
+ this.redisClient = redisClient;
20
+ this.startStateCheck();
21
+ }
22
+ async recordSuccess() {
23
+ this.successCount++;
24
+ if (this.state === CircuitState.HALF_OPEN) {
25
+ if (this.successCount >= this.config.halfOpenStateMaxAttempts) {
26
+ await this.closeCircuit();
27
+ }
28
+ }
29
+ }
30
+ async recordFailure() {
31
+ this.failureCount++;
32
+ if (this.state === CircuitState.CLOSED) {
33
+ const totalAttempts = this.failureCount + this.successCount;
34
+ const failureRate = (this.failureCount / totalAttempts) * 100;
35
+ if (this.failureCount >= this.config.errorThreshold || failureRate >= this.config.errorThresholdPercentage) {
36
+ await this.openCircuit();
37
+ }
38
+ }
39
+ else if (this.state === CircuitState.HALF_OPEN) {
40
+ await this.openCircuit();
41
+ }
42
+ }
43
+ async isAllowed() {
44
+ const now = Date.now();
45
+ if (this.state === CircuitState.OPEN) {
46
+ if (now - this.lastStateChange >= this.config.openStateDuration) {
47
+ await this.halfOpenCircuit();
48
+ }
49
+ else {
50
+ return false;
51
+ }
52
+ }
53
+ return true;
54
+ }
55
+ openCircuit() {
56
+ if (this.state !== CircuitState.OPEN) {
57
+ this.state = CircuitState.OPEN;
58
+ this.lastStateChange = Date.now();
59
+ this.emit('stateChange', CircuitState.OPEN);
60
+ }
61
+ }
62
+ closeCircuit() {
63
+ if (this.state !== CircuitState.CLOSED) {
64
+ this.state = CircuitState.CLOSED;
65
+ this.lastStateChange = Date.now();
66
+ this.failureCount = 0;
67
+ this.successCount = 0;
68
+ this.emit('stateChange', CircuitState.CLOSED);
69
+ }
70
+ }
71
+ halfOpenCircuit() {
72
+ if (this.state !== CircuitState.HALF_OPEN) {
73
+ this.state = CircuitState.HALF_OPEN;
74
+ this.lastStateChange = Date.now();
75
+ this.failureCount = 0;
76
+ this.successCount = 0;
77
+ this.emit('stateChange', CircuitState.HALF_OPEN);
78
+ }
79
+ }
80
+ getState() {
81
+ return this.state;
82
+ }
83
+ async storeEvent(event) {
84
+ const key = `circuit_breaker:stored_events`;
85
+ await this.redisClient.lpush(key, JSON.stringify(event));
86
+ await this.redisClient.ltrim(key, 0, this.config.maxStoredEvents - 1);
87
+ }
88
+ async getStoredEvents() {
89
+ const key = `circuit_breaker:stored_events`;
90
+ const events = await this.redisClient.lrange(key, 0, -1);
91
+ return events.map((event) => JSON.parse(event));
92
+ }
93
+ async clearStoredEvents() {
94
+ const key = `circuit_breaker:stored_events`;
95
+ await this.redisClient.del(key);
96
+ }
97
+ startStateCheck() {
98
+ setInterval(() => {
99
+ this.isAllowed();
100
+ }, 2000);
101
+ }
102
+ }
103
+ exports.CircuitBreaker = CircuitBreaker;
@@ -2,3 +2,6 @@ export { setRedisConnectionSettings as setRedisConfig } from './redis/registry';
2
2
  export { Streams as Publisher } from './redis/streams';
3
3
  export { ScheduledProcessor as __SCHEDULER_INTERNALS__ } from './redis/scheduler';
4
4
  export { UTILS as StreamUtilityFunctions } from './redis/utils';
5
+ export { PrometheusAdapter } from './monitoring/adapters/prom';
6
+ export { publishBatch, publishScheduledBatch } from './redis/batch';
7
+ export { IListenOptions, IStreamsConfig, TEventFilter } from './redis/types';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StreamUtilityFunctions = exports.__SCHEDULER_INTERNALS__ = exports.Publisher = exports.setRedisConfig = void 0;
3
+ exports.publishScheduledBatch = exports.publishBatch = exports.PrometheusAdapter = exports.StreamUtilityFunctions = exports.__SCHEDULER_INTERNALS__ = 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");
@@ -9,3 +9,8 @@ var scheduler_1 = require("./redis/scheduler");
9
9
  Object.defineProperty(exports, "__SCHEDULER_INTERNALS__", { enumerable: true, get: function () { return scheduler_1.ScheduledProcessor; } });
10
10
  var utils_1 = require("./redis/utils");
11
11
  Object.defineProperty(exports, "StreamUtilityFunctions", { enumerable: true, get: function () { return utils_1.UTILS; } });
12
+ var prom_1 = require("./monitoring/adapters/prom");
13
+ Object.defineProperty(exports, "PrometheusAdapter", { enumerable: true, get: function () { return prom_1.PrometheusAdapter; } });
14
+ var batch_1 = require("./redis/batch");
15
+ Object.defineProperty(exports, "publishBatch", { enumerable: true, get: function () { return batch_1.publishBatch; } });
16
+ Object.defineProperty(exports, "publishScheduledBatch", { enumerable: true, get: function () { return batch_1.publishScheduledBatch; } });
@@ -0,0 +1,6 @@
1
+ import { Streams as Publisher } from './streams';
2
+ import { BatchPublishOptions, BatchPublishResult, PublishData } from './types';
3
+ export declare function publishBatch<TData, TName extends string>(streams: Publisher, events: Array<PublishData<TData, TName>>, options?: BatchPublishOptions): Promise<BatchPublishResult<TData, TName>>;
4
+ export declare function publishScheduledBatch<TData, TName extends string>(streams: Publisher, events: Array<PublishData<TData, TName>>, options: BatchPublishOptions & {
5
+ scheduledTime: Date;
6
+ }): Promise<BatchPublishResult<TData, TName>>;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.publishScheduledBatch = exports.publishBatch = void 0;
4
+ const id_1 = require("@jetit/id");
5
+ const tracker_1 = require("../monitoring/tracker");
6
+ const logger_1 = require("./logger");
7
+ async function delay(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+ async function publishSingleEvent(streams, event, batchId, tracker, scheduledTime) {
11
+ try {
12
+ tracker.startRedisOperation();
13
+ let eventId = '';
14
+ if (scheduledTime) {
15
+ await streams.scheduledPublish(scheduledTime, { ...event, batchId });
16
+ }
17
+ else {
18
+ eventId = await streams.publish({ ...event, batchId });
19
+ }
20
+ tracker.endRedisOperation();
21
+ return { eventName: event.eventName, eventId, success: true };
22
+ }
23
+ catch (error) {
24
+ return { eventName: event.eventName, eventId: '', success: false, error: error.message };
25
+ }
26
+ }
27
+ async function publishBatchWithRetry(streams, events, options) {
28
+ const batchId = (0, id_1.generateID)('HEX');
29
+ const tracker = new tracker_1.MetricsTracker();
30
+ let results = [];
31
+ const batchSize = options.batchSize || 100;
32
+ const delayBetweenBatches = options.delayBetweenBatches || 1000;
33
+ /** First attempt: publish all events in chunks */
34
+ for (let i = 0; i < events.length; i += batchSize) {
35
+ const chunk = events.slice(i, i + batchSize);
36
+ tracker.startProcessing();
37
+ const chunkResults = await Promise.all(chunk.map((event) => publishSingleEvent(streams, event, batchId, tracker, options.scheduledTime)));
38
+ tracker.endProcessing();
39
+ results.push(...chunkResults);
40
+ if (i + batchSize < events.length) {
41
+ await delay(delayBetweenBatches);
42
+ }
43
+ }
44
+ /** Second attempt: retry failed events once */
45
+ const failedEvents = results.filter((result) => !result.success);
46
+ if (failedEvents.length > 0) {
47
+ logger_1.PERFORMANCE_LOGGER.log(`${options.scheduledTime ? 'SCHEDULED_' : ''}BATCH_RETRY;${batchId};${failedEvents.length}`);
48
+ const retryResults = await Promise.all(failedEvents.map((result) => {
49
+ const event = events.find((event) => event.eventName === result.eventName);
50
+ return publishSingleEvent(streams, event, batchId, tracker, options.scheduledTime);
51
+ }));
52
+ results = results.map((result) => {
53
+ if (!result.success) {
54
+ const retryResult = retryResults.find((retry) => retry.eventName === result.eventName);
55
+ return retryResult || result;
56
+ }
57
+ return result;
58
+ });
59
+ }
60
+ const metrics = tracker.getMetrics();
61
+ const averageEventTime = metrics.totalTime / events.length;
62
+ logger_1.PERFORMANCE_LOGGER.log(`${options.scheduledTime ? 'SCHEDULED_' : ''}BATCH;${batchId};${events.length};${metrics.totalTime};${metrics.redisOperationTime};${metrics.processingTime};${averageEventTime}${options.scheduledTime ? `;${options.scheduledTime.getTime()}` : ''}`);
63
+ return {
64
+ batchId,
65
+ results,
66
+ performanceMetrics: {
67
+ ...metrics,
68
+ averageEventTime,
69
+ },
70
+ };
71
+ }
72
+ function publishBatch(streams, events, options = {}) {
73
+ return publishBatchWithRetry(streams, events, options);
74
+ }
75
+ exports.publishBatch = publishBatch;
76
+ function publishScheduledBatch(streams, events, options) {
77
+ return publishBatchWithRetry(streams, events, options);
78
+ }
79
+ exports.publishScheduledBatch = publishScheduledBatch;
@@ -0,0 +1,18 @@
1
+ import { RedisType } from './registry';
2
+ import { IDLQEvent } from './types';
3
+ export declare class DeadLetterQueue {
4
+ private redisClient;
5
+ private maxEventsThreshold;
6
+ constructor(redisClient: RedisType, maxEventsThreshold?: number);
7
+ addToDLQ(event: IDLQEvent): Promise<void>;
8
+ getFromDLQ(start?: number, end?: number): Promise<IDLQEvent[]>;
9
+ retryFromDLQ(eventId: string): Promise<boolean>;
10
+ removeFromDLQ(eventId: string): Promise<boolean>;
11
+ cleanupDLQ(): Promise<void>;
12
+ getDLQStats(): Promise<{
13
+ size: number;
14
+ additionRate: number;
15
+ }>;
16
+ private isRateLimitExceeded;
17
+ private incrementRateLimit;
18
+ }
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DeadLetterQueue = void 0;
4
+ const logger_1 = require("./logger");
5
+ const DLQ_HASH_KEY = 'dlq_events';
6
+ const DLQ_ZSET_KEY = 'dlq_order';
7
+ const DLQ_RATE_LIMIT_KEY = 'dlq_rate_limit';
8
+ const DLQ_EXPIRATION = 24 * 60 * 60 * 1000; // 24 hours
9
+ class DeadLetterQueue {
10
+ constructor(redisClient, maxEventsThreshold = 1000) {
11
+ this.redisClient = redisClient;
12
+ this.maxEventsThreshold = maxEventsThreshold;
13
+ }
14
+ async addToDLQ(event) {
15
+ try {
16
+ if (await this.isRateLimitExceeded()) {
17
+ logger_1.PUBLISHER_LOGGER.warn('DLQ: Rate limit exceeded, not adding event to DLQ');
18
+ return;
19
+ }
20
+ const eventJson = JSON.stringify(event);
21
+ await this.redisClient.multi().hset(DLQ_HASH_KEY, event.eventId, eventJson).zadd(DLQ_ZSET_KEY, event.timestamp, event.eventId).exec();
22
+ await this.incrementRateLimit();
23
+ logger_1.PUBLISHER_LOGGER.log(`DLQ: Added event ${event.eventId} to Dead Letter Queue`);
24
+ }
25
+ catch (error) {
26
+ logger_1.PUBLISHER_LOGGER.error('DLQ: Error adding event to Dead Letter Queue', error);
27
+ }
28
+ }
29
+ async getFromDLQ(start = 0, end = -1) {
30
+ try {
31
+ const eventIds = await this.redisClient.zrange(DLQ_ZSET_KEY, start, end);
32
+ const events = await this.redisClient.hmget(DLQ_HASH_KEY, ...eventIds);
33
+ return events
34
+ .filter((x) => !!x)
35
+ .map((event) => JSON.parse(event ?? ''))
36
+ .filter((event) => event !== null);
37
+ }
38
+ catch (error) {
39
+ logger_1.PUBLISHER_LOGGER.error('DLQ: Error retrieving events from Dead Letter Queue', error);
40
+ return [];
41
+ }
42
+ }
43
+ async retryFromDLQ(eventId) {
44
+ try {
45
+ const eventJson = await this.redisClient.hget(DLQ_HASH_KEY, eventId);
46
+ if (!eventJson) {
47
+ logger_1.PUBLISHER_LOGGER.warn(`DLQ: Event with ID ${eventId} not found in Dead Letter Queue`);
48
+ return false;
49
+ }
50
+ const event = JSON.parse(eventJson);
51
+ // Attempt to republish the event to its original stream
52
+ const streamName = event.originalStream;
53
+ const result = await this.redisClient.xadd(streamName, '*', 'data', JSON.stringify(event));
54
+ if (result) {
55
+ // If successful, remove the event from the DLQ
56
+ await this.removeFromDLQ(eventId);
57
+ logger_1.PUBLISHER_LOGGER.log(`DLQ: Successfully retried and removed event ${eventId} from Dead Letter Queue`);
58
+ return true;
59
+ }
60
+ else {
61
+ logger_1.PUBLISHER_LOGGER.error(`DLQ: Failed to republish event ${eventId} to stream ${streamName}`);
62
+ return false;
63
+ }
64
+ }
65
+ catch (error) {
66
+ logger_1.PUBLISHER_LOGGER.error(`DLQ: Error retrying event ${eventId} from Dead Letter Queue`, error);
67
+ return false;
68
+ }
69
+ }
70
+ async removeFromDLQ(eventId) {
71
+ try {
72
+ const removed = await this.redisClient.multi().hdel(DLQ_HASH_KEY, eventId).zrem(DLQ_ZSET_KEY, eventId).exec();
73
+ if (removed && removed[0][1] === 1 && removed[1][1] === 1) {
74
+ logger_1.PUBLISHER_LOGGER.log(`DLQ: Successfully removed event ${eventId} from Dead Letter Queue`);
75
+ return true;
76
+ }
77
+ else {
78
+ logger_1.PUBLISHER_LOGGER.warn(`DLQ: Event with ID ${eventId} not found in Dead Letter Queue for removal`);
79
+ return false;
80
+ }
81
+ }
82
+ catch (error) {
83
+ logger_1.PUBLISHER_LOGGER.error(`DLQ: Error removing event ${eventId} from Dead Letter Queue`, error);
84
+ return false;
85
+ }
86
+ }
87
+ async cleanupDLQ() {
88
+ const now = Date.now();
89
+ const cutoffTime = now - DLQ_EXPIRATION;
90
+ try {
91
+ const expiredEventIds = await this.redisClient.zrangebyscore(DLQ_ZSET_KEY, 0, cutoffTime);
92
+ if (expiredEventIds.length > 0) {
93
+ await this.redisClient
94
+ .multi()
95
+ .hdel(DLQ_HASH_KEY, ...expiredEventIds)
96
+ .zremrangebyscore(DLQ_ZSET_KEY, 0, cutoffTime)
97
+ .exec();
98
+ }
99
+ logger_1.PUBLISHER_LOGGER.log(`DLQ: Cleaned up ${expiredEventIds.length} expired events from Dead Letter Queue`);
100
+ }
101
+ catch (error) {
102
+ logger_1.PUBLISHER_LOGGER.error('DLQ: Error cleaning up Dead Letter Queue', error);
103
+ }
104
+ }
105
+ async getDLQStats() {
106
+ try {
107
+ const results = await this.redisClient.multi().zcard(DLQ_ZSET_KEY).get(DLQ_RATE_LIMIT_KEY).exec();
108
+ if (!results) {
109
+ throw new Error('Failed to execute Redis commands');
110
+ }
111
+ const [sizeResult, additionRateResult] = results;
112
+ if (sizeResult[0] || additionRateResult[0]) {
113
+ throw new Error('Error executing Redis commands');
114
+ }
115
+ const size = sizeResult[1];
116
+ const additionRate = parseInt(additionRateResult[1] || '0', 10);
117
+ return { size, additionRate };
118
+ }
119
+ catch (error) {
120
+ logger_1.PUBLISHER_LOGGER.error('DLQ: Error getting Dead Letter Queue stats', error);
121
+ return { size: 0, additionRate: 0 };
122
+ }
123
+ }
124
+ async isRateLimitExceeded() {
125
+ const currentRate = (await this.redisClient.get(DLQ_RATE_LIMIT_KEY)) || '0';
126
+ return parseInt(currentRate, 10) >= this.maxEventsThreshold;
127
+ }
128
+ async incrementRateLimit() {
129
+ await this.redisClient.incr(DLQ_RATE_LIMIT_KEY);
130
+ await this.redisClient.expire(DLQ_RATE_LIMIT_KEY, 60); // Reset rate limit after 1 minute
131
+ }
132
+ }
133
+ exports.DeadLetterQueue = DeadLetterQueue;
@@ -0,0 +1,10 @@
1
+ import { RedisType } from './registry';
2
+ import { EventData } from './types';
3
+ export declare class ContentBasedDeduplication {
4
+ private redisClient;
5
+ private ttl;
6
+ private hmacSecret;
7
+ constructor(redisClient: RedisType, ttl?: number);
8
+ private calculateEventHash;
9
+ isDuplicate(event: EventData<any, string>, consumerGroupName: string): Promise<boolean>;
10
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContentBasedDeduplication = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const logger_1 = require("./logger");
6
+ class ContentBasedDeduplication {
7
+ constructor(redisClient, ttl = 86400) {
8
+ this.redisClient = redisClient;
9
+ this.ttl = ttl;
10
+ this.hmacSecret = 'content-hash-calculation';
11
+ }
12
+ calculateEventHash(eventData) {
13
+ const { eventId, timestamp, createdAt, ...hashableData } = eventData;
14
+ const hmac = (0, crypto_1.createHmac)('sha256', this.hmacSecret);
15
+ return hmac.update(JSON.stringify(hashableData)).digest('hex');
16
+ }
17
+ async isDuplicate(event, consumerGroupName) {
18
+ const eventHash = this.calculateEventHash(event.data);
19
+ const key = `processed:${consumerGroupName}:${eventHash}`;
20
+ const result = await this.redisClient.set(key, '1', 'EX', this.ttl, 'NX');
21
+ const isDuplicate = result === null;
22
+ if (isDuplicate) {
23
+ logger_1.PUBLISHER_LOGGER.log(`Duplicate event detected for consumer group ${consumerGroupName}`);
24
+ }
25
+ return isDuplicate;
26
+ }
27
+ }
28
+ exports.ContentBasedDeduplication = ContentBasedDeduplication;