rabbitmq-sdk 0.0.1-security → 1.2.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.
Potentially problematic release.
This version of rabbitmq-sdk might be problematic. Click here for more details.
- package/.eslintrc.js +23 -0
- package/.kiro/specs/sdk-rabbitmq/design.md +369 -0
- package/.kiro/specs/sdk-rabbitmq/requirements.md +97 -0
- package/.kiro/specs/sdk-rabbitmq/tasks.md +248 -0
- package/README.md +273 -5
- package/bun.lock +790 -0
- package/config.example.json +13 -0
- package/dist/components/ConfigurationManager.d.ts +35 -0
- package/dist/components/ConfigurationManager.d.ts.map +1 -0
- package/dist/components/ConfigurationManager.js +118 -0
- package/dist/components/ConfigurationManager.js.map +1 -0
- package/dist/components/ConnectionManager.d.ts +93 -0
- package/dist/components/ConnectionManager.d.ts.map +1 -0
- package/dist/components/ConnectionManager.js +349 -0
- package/dist/components/ConnectionManager.js.map +1 -0
- package/dist/components/DLQHandler.d.ts +81 -0
- package/dist/components/DLQHandler.d.ts.map +1 -0
- package/dist/components/DLQHandler.js +228 -0
- package/dist/components/DLQHandler.js.map +1 -0
- package/dist/components/Logger.d.ts +77 -0
- package/dist/components/Logger.d.ts.map +1 -0
- package/dist/components/Logger.js +193 -0
- package/dist/components/Logger.js.map +1 -0
- package/dist/components/MessagePublisher.d.ts +49 -0
- package/dist/components/MessagePublisher.d.ts.map +1 -0
- package/dist/components/MessagePublisher.js +158 -0
- package/dist/components/MessagePublisher.js.map +1 -0
- package/dist/components/MessageSubscriber.d.ts +108 -0
- package/dist/components/MessageSubscriber.d.ts.map +1 -0
- package/dist/components/MessageSubscriber.js +503 -0
- package/dist/components/MessageSubscriber.js.map +1 -0
- package/dist/components/ResourceCreator.d.ts +89 -0
- package/dist/components/ResourceCreator.d.ts.map +1 -0
- package/dist/components/ResourceCreator.js +352 -0
- package/dist/components/ResourceCreator.js.map +1 -0
- package/dist/components/SdkRabbitmq.d.ts +103 -0
- package/dist/components/SdkRabbitmq.d.ts.map +1 -0
- package/dist/components/SdkRabbitmq.js +364 -0
- package/dist/components/SdkRabbitmq.js.map +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +20 -0
- package/dist/components/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/IConfiguration.d.ts +35 -0
- package/dist/interfaces/IConfiguration.d.ts.map +1 -0
- package/dist/interfaces/IConfiguration.js +3 -0
- package/dist/interfaces/IConfiguration.js.map +1 -0
- package/dist/interfaces/IConnection.d.ts +21 -0
- package/dist/interfaces/IConnection.d.ts.map +1 -0
- package/dist/interfaces/IConnection.js +3 -0
- package/dist/interfaces/IConnection.js.map +1 -0
- package/dist/interfaces/IDLQ.d.ts +12 -0
- package/dist/interfaces/IDLQ.d.ts.map +1 -0
- package/dist/interfaces/IDLQ.js +3 -0
- package/dist/interfaces/IDLQ.js.map +1 -0
- package/dist/interfaces/IErrors.d.ts +33 -0
- package/dist/interfaces/IErrors.d.ts.map +1 -0
- package/dist/interfaces/IErrors.js +56 -0
- package/dist/interfaces/IErrors.js.map +1 -0
- package/dist/interfaces/ILogger.d.ts +14 -0
- package/dist/interfaces/ILogger.d.ts.map +1 -0
- package/dist/interfaces/ILogger.js +3 -0
- package/dist/interfaces/ILogger.js.map +1 -0
- package/dist/interfaces/IMessage.d.ts +52 -0
- package/dist/interfaces/IMessage.d.ts.map +1 -0
- package/dist/interfaces/IMessage.js +3 -0
- package/dist/interfaces/IMessage.js.map +1 -0
- package/dist/interfaces/IResource.d.ts +31 -0
- package/dist/interfaces/IResource.d.ts.map +1 -0
- package/dist/interfaces/IResource.js +3 -0
- package/dist/interfaces/IResource.js.map +1 -0
- package/dist/interfaces/ISdkRabbitmq.d.ts +17 -0
- package/dist/interfaces/ISdkRabbitmq.d.ts.map +1 -0
- package/dist/interfaces/ISdkRabbitmq.js +3 -0
- package/dist/interfaces/ISdkRabbitmq.js.map +1 -0
- package/dist/interfaces/index.d.ts +9 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/index.js +33 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/utils/configSchema.d.ts +8 -0
- package/dist/utils/configSchema.d.ts.map +1 -0
- package/dist/utils/configSchema.js +51 -0
- package/dist/utils/configSchema.js.map +1 -0
- package/docker-compose.yml +24 -0
- package/example.ts +65 -0
- package/examples/README-dynamic-routing.md +155 -0
- package/examples/bind-unbind-example.js +56 -0
- package/examples/test-chatbot-exchange.ts +83 -0
- package/examples/test-dynamic-routing-flow.js +299 -0
- package/examples/test-dynamic-routing-flow.ts +355 -0
- package/examples/test-no-disconnect.ts +0 -0
- package/examples/test-raw-rabbitmq.js +68 -0
- package/examples/test-same-channel.ts +81 -0
- package/examples/test-schedule-flow.ts +713 -0
- package/examples/test-simple-greeting.ts +66 -0
- package/examples/test-simple-schedule.ts +76 -0
- package/examples/test-wildcard.ts +364 -0
- package/jest.config.js +17 -0
- package/package.json +42 -4
- package/preinstall.js +1 -0
- package/prompts/test-dynamic-routing-flow.md +46 -0
- package/run.js +4 -0
- package/scripts/run-dynamic-routing-test.ts +31 -0
- package/src/.gitkeep +1 -0
- package/src/components/.gitkeep +1 -0
- package/src/components/ConfigurationManager.ts +104 -0
- package/src/components/ConnectionManager.ts +357 -0
- package/src/components/DLQHandler.ts +271 -0
- package/src/components/Logger.ts +224 -0
- package/src/components/MessagePublisher.ts +180 -0
- package/src/components/MessageSubscriber.ts +597 -0
- package/src/components/ResourceCreator.ts +411 -0
- package/src/components/SdkRabbitmq.ts +443 -0
- package/src/components/__tests__/ConfigurationManager.test.ts +357 -0
- package/src/components/__tests__/ConnectionManager.test.ts +387 -0
- package/src/components/__tests__/DLQHandler.test.ts +399 -0
- package/src/components/__tests__/Logger.test.ts +354 -0
- package/src/components/__tests__/MessagePublisher.test.ts +337 -0
- package/src/components/__tests__/MessageSubscriber.test.ts +542 -0
- package/src/components/__tests__/ResourceCreator.test.ts +465 -0
- package/src/components/__tests__/SdkRabbitmq.integration.test.ts +433 -0
- package/src/components/index.ts +8 -0
- package/src/index.ts +11 -0
- package/src/interfaces/.gitkeep +1 -0
- package/src/interfaces/IConfiguration.ts +38 -0
- package/src/interfaces/IConnection.ts +27 -0
- package/src/interfaces/IDLQ.ts +13 -0
- package/src/interfaces/IErrors.ts +53 -0
- package/src/interfaces/ILogger.ts +16 -0
- package/src/interfaces/IMessage.ts +65 -0
- package/src/interfaces/IResource.ts +35 -0
- package/src/interfaces/ISdkRabbitmq.ts +26 -0
- package/src/interfaces/index.ts +23 -0
- package/src/utils/.gitkeep +1 -0
- package/src/utils/configSchema.ts +58 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import * as amqp from 'amqplib';
|
|
2
|
+
import { IMessageSubscriber, MessageCallback } from '../interfaces/IMessage';
|
|
3
|
+
import { SubscriptionError } from '../interfaces/IErrors';
|
|
4
|
+
import { ConnectionManager } from './ConnectionManager';
|
|
5
|
+
import { ResourceCreator } from './ResourceCreator';
|
|
6
|
+
import { DLQHandler } from './DLQHandler';
|
|
7
|
+
import { Logger } from './Logger';
|
|
8
|
+
import { IConfiguration } from '../interfaces/IConfiguration';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* MessageSubscriber handles message consumption from RabbitMQ queues
|
|
12
|
+
* Implements parameter validation, JSON deserialization, automatic resource creation, and DLQ handling
|
|
13
|
+
*/
|
|
14
|
+
export class MessageSubscriber implements IMessageSubscriber {
|
|
15
|
+
private connectionManager: ConnectionManager;
|
|
16
|
+
private resourceCreator: ResourceCreator;
|
|
17
|
+
private dlqHandler: DLQHandler;
|
|
18
|
+
private activeConsumers = new Map<string, { channel: amqp.Channel; consumerTag: string }>();
|
|
19
|
+
private logger: Logger;
|
|
20
|
+
|
|
21
|
+
constructor(connectionManager: ConnectionManager, resourceCreator: ResourceCreator, dlqHandler: DLQHandler, config?: IConfiguration['logging']) {
|
|
22
|
+
this.connectionManager = connectionManager;
|
|
23
|
+
this.resourceCreator = resourceCreator;
|
|
24
|
+
this.dlqHandler = dlqHandler;
|
|
25
|
+
this.logger = Logger.createComponentLogger('MessageSubscriber', config);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Subscribe to messages from a queue
|
|
30
|
+
* @param exchange Exchange name (required)
|
|
31
|
+
* @param queue Queue name (required)
|
|
32
|
+
* @param routingKey Routing key for binding (required)
|
|
33
|
+
* @param callback Message callback function (required)
|
|
34
|
+
*/
|
|
35
|
+
public async subscribe(
|
|
36
|
+
exchange: string,
|
|
37
|
+
queue: string,
|
|
38
|
+
routingKey: string,
|
|
39
|
+
callback: MessageCallback
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
// Parameter validation - all parameters are required
|
|
42
|
+
this.validateSubscribeParameters(exchange, queue, routingKey, callback);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Automatic exchange and queue creation before subscribing
|
|
46
|
+
await this.ensureResourcesExist(exchange, queue, routingKey);
|
|
47
|
+
|
|
48
|
+
await this.connectionManager.executeOperation(async () => {
|
|
49
|
+
const connection = this.connectionManager.getConnection();
|
|
50
|
+
if (!connection) {
|
|
51
|
+
throw new SubscriptionError('No active connection to RabbitMQ', {
|
|
52
|
+
exchange,
|
|
53
|
+
queue,
|
|
54
|
+
routingKey
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create a dedicated channel for this consumer
|
|
59
|
+
const channel = await (connection as any).createChannel();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Set prefetch to 1 for fair dispatch
|
|
63
|
+
await channel.prefetch(1);
|
|
64
|
+
|
|
65
|
+
// Start consuming messages
|
|
66
|
+
this.logger.info('🎯 SETTING UP CONSUMER', {
|
|
67
|
+
queue,
|
|
68
|
+
exchange,
|
|
69
|
+
routingKey
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const consumerInfo = await channel.consume(
|
|
73
|
+
queue,
|
|
74
|
+
(message: amqp.ConsumeMessage | null) => {
|
|
75
|
+
this.logger.info('📥 RAW MESSAGE RECEIVED', {
|
|
76
|
+
hasMessage: !!message,
|
|
77
|
+
queue,
|
|
78
|
+
exchange,
|
|
79
|
+
routingKey
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (message) {
|
|
83
|
+
this.logger.info('📋 MESSAGE DETAILS', {
|
|
84
|
+
exchange: message.fields.exchange,
|
|
85
|
+
routingKey: message.fields.routingKey,
|
|
86
|
+
deliveryTag: message.fields.deliveryTag,
|
|
87
|
+
contentLength: message.content.length
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Add queue name to message headers for DLQ handling
|
|
91
|
+
if (!message.properties.headers) {
|
|
92
|
+
message.properties.headers = {};
|
|
93
|
+
}
|
|
94
|
+
message.properties.headers['x-original-queue'] = queue;
|
|
95
|
+
|
|
96
|
+
this.handleMessage(message, callback, channel);
|
|
97
|
+
} else {
|
|
98
|
+
this.logger.warn('⚠️ RECEIVED NULL MESSAGE', { queue });
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
noAck: false // We want manual acknowledgment
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
this.logger.info('✅ CONSUMER SETUP COMPLETE', {
|
|
107
|
+
consumerTag: consumerInfo.consumerTag,
|
|
108
|
+
queue
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Store consumer info for cleanup
|
|
112
|
+
this.activeConsumers.set(queue, {
|
|
113
|
+
channel,
|
|
114
|
+
consumerTag: consumerInfo.consumerTag
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
this.logger.logOperation('subscribe', `Successfully subscribed to queue`, {
|
|
118
|
+
queue,
|
|
119
|
+
exchange,
|
|
120
|
+
routingKey
|
|
121
|
+
});
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// Close channel on error
|
|
124
|
+
this.logger.logError('❌ ERROR DURING CONSUMER SETUP - CLOSING CHANNEL', error as Error, {
|
|
125
|
+
queue,
|
|
126
|
+
exchange,
|
|
127
|
+
routingKey
|
|
128
|
+
});
|
|
129
|
+
await channel.close();
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
// Detailed error logging with context
|
|
135
|
+
this.logSubscriptionError(error, exchange, queue, routingKey);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Unsubscribe from a queue
|
|
142
|
+
* @param queue Queue name to unsubscribe from
|
|
143
|
+
*/
|
|
144
|
+
public async unsubscribe(queue: string): Promise<void> {
|
|
145
|
+
if (!queue || typeof queue !== 'string') {
|
|
146
|
+
throw new SubscriptionError('Queue parameter is required and must be a non-empty string', {
|
|
147
|
+
queue
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const consumerInfo = this.activeConsumers.get(queue);
|
|
152
|
+
if (!consumerInfo) {
|
|
153
|
+
this.logger.warn(`No active subscription found for queue`, { queue });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Cancel the consumer
|
|
159
|
+
await consumerInfo.channel.cancel(consumerInfo.consumerTag);
|
|
160
|
+
|
|
161
|
+
// Close the channel
|
|
162
|
+
await consumerInfo.channel.close();
|
|
163
|
+
|
|
164
|
+
// Remove from active consumers
|
|
165
|
+
this.activeConsumers.delete(queue);
|
|
166
|
+
|
|
167
|
+
this.logger.logOperation('unsubscribe', `Successfully unsubscribed from queue`, { queue });
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
170
|
+
this.logger.logError(`Error unsubscribing from queue`, error as Error, { queue });
|
|
171
|
+
|
|
172
|
+
// Still remove from active consumers even if cleanup failed
|
|
173
|
+
this.activeConsumers.delete(queue);
|
|
174
|
+
|
|
175
|
+
throw new SubscriptionError(`Failed to unsubscribe from queue '${queue}'`, {
|
|
176
|
+
queue,
|
|
177
|
+
error: errorMessage
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle incoming message
|
|
184
|
+
* @param message RabbitMQ message
|
|
185
|
+
* @param callback User callback function
|
|
186
|
+
* @param channel Channel for acknowledgment
|
|
187
|
+
*/
|
|
188
|
+
private handleMessage(
|
|
189
|
+
message: amqp.ConsumeMessage,
|
|
190
|
+
callback: MessageCallback,
|
|
191
|
+
channel: amqp.Channel
|
|
192
|
+
): void {
|
|
193
|
+
const queueName = this.getQueueNameFromMessage(message);
|
|
194
|
+
|
|
195
|
+
// Debug log to see if messages are arriving
|
|
196
|
+
this.logger.info('🔥 MESSAGE RECEIVED IN HANDLER', {
|
|
197
|
+
exchange: message.fields.exchange,
|
|
198
|
+
routingKey: message.fields.routingKey,
|
|
199
|
+
queue: queueName,
|
|
200
|
+
deliveryTag: message.fields.deliveryTag
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// JSON deserialization for incoming messages
|
|
205
|
+
const deserializedPayload = this.deserializeMessage(message);
|
|
206
|
+
|
|
207
|
+
// Create ack/nack functions for callback
|
|
208
|
+
const ack = (): void => {
|
|
209
|
+
try {
|
|
210
|
+
channel.ack(message);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
this.logger.logError('Error acknowledging message', error as Error, {
|
|
213
|
+
deliveryTag: message.fields.deliveryTag,
|
|
214
|
+
queue: queueName
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const nack = async (): Promise<void> => {
|
|
220
|
+
try {
|
|
221
|
+
await this.handleFailedMessage(message, queueName, channel);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
this.logger.logError('Error handling failed message', error as Error, {
|
|
224
|
+
deliveryTag: message.fields.deliveryTag,
|
|
225
|
+
queue: queueName
|
|
226
|
+
});
|
|
227
|
+
// Fallback to regular nack with requeue
|
|
228
|
+
try {
|
|
229
|
+
channel.nack(message, false, true);
|
|
230
|
+
} catch (nackError) {
|
|
231
|
+
this.logger.logError('Error negative acknowledging message', nackError as Error, {
|
|
232
|
+
deliveryTag: message.fields.deliveryTag,
|
|
233
|
+
queue: queueName
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Debug log before invoking callback
|
|
240
|
+
this.logger.info('🚀 INVOKING USER CALLBACK', {
|
|
241
|
+
payloadType: typeof deserializedPayload,
|
|
242
|
+
queue: queueName
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Invoke callback with deserialized payload and ack/nack functions
|
|
246
|
+
callback(deserializedPayload, ack, nack);
|
|
247
|
+
|
|
248
|
+
this.logger.info('✅ USER CALLBACK COMPLETED', {
|
|
249
|
+
queue: queueName
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
} catch (error) {
|
|
253
|
+
this.logger.logError('Error handling message', error as Error, {
|
|
254
|
+
exchange: message.fields.exchange,
|
|
255
|
+
routingKey: message.fields.routingKey,
|
|
256
|
+
deliveryTag: message.fields.deliveryTag,
|
|
257
|
+
queue: queueName
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Handle deserialization or other processing errors
|
|
261
|
+
this.handleFailedMessage(message, queueName, channel).catch(dlqError => {
|
|
262
|
+
this.logger.logError('Error routing failed message to DLQ', dlqError as Error, {
|
|
263
|
+
deliveryTag: message.fields.deliveryTag,
|
|
264
|
+
queue: queueName
|
|
265
|
+
});
|
|
266
|
+
// Fallback to regular nack without requeue to avoid infinite loop
|
|
267
|
+
try {
|
|
268
|
+
channel.nack(message, false, false);
|
|
269
|
+
} catch (nackError) {
|
|
270
|
+
this.logger.logError('Error negative acknowledging failed message', nackError as Error, {
|
|
271
|
+
deliveryTag: message.fields.deliveryTag,
|
|
272
|
+
queue: queueName
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Validate subscribe method parameters
|
|
281
|
+
* @param exchange Exchange name
|
|
282
|
+
* @param queue Queue name
|
|
283
|
+
* @param routingKey Routing key
|
|
284
|
+
* @param callback Message callback function
|
|
285
|
+
*/
|
|
286
|
+
private validateSubscribeParameters(
|
|
287
|
+
exchange: string,
|
|
288
|
+
queue: string,
|
|
289
|
+
routingKey: string,
|
|
290
|
+
callback: MessageCallback
|
|
291
|
+
): void {
|
|
292
|
+
if (!exchange || typeof exchange !== 'string') {
|
|
293
|
+
throw new SubscriptionError('Exchange parameter is required and must be a non-empty string', {
|
|
294
|
+
exchange,
|
|
295
|
+
queue,
|
|
296
|
+
routingKey
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!queue || typeof queue !== 'string') {
|
|
301
|
+
throw new SubscriptionError('Queue parameter is required and must be a non-empty string', {
|
|
302
|
+
exchange,
|
|
303
|
+
queue,
|
|
304
|
+
routingKey
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!routingKey || typeof routingKey !== 'string') {
|
|
309
|
+
throw new SubscriptionError('RoutingKey parameter is required and must be a non-empty string', {
|
|
310
|
+
exchange,
|
|
311
|
+
queue,
|
|
312
|
+
routingKey
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!callback || typeof callback !== 'function') {
|
|
317
|
+
throw new SubscriptionError('Callback parameter is required and must be a function', {
|
|
318
|
+
exchange,
|
|
319
|
+
queue,
|
|
320
|
+
routingKey,
|
|
321
|
+
callbackType: typeof callback
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Deserialize message content from JSON
|
|
328
|
+
* @param message RabbitMQ message
|
|
329
|
+
* @returns Deserialized payload
|
|
330
|
+
*/
|
|
331
|
+
private deserializeMessage(message: amqp.ConsumeMessage): any {
|
|
332
|
+
try {
|
|
333
|
+
const contentString = message.content.toString('utf8');
|
|
334
|
+
return JSON.parse(contentString);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
throw new SubscriptionError('Failed to deserialize message content from JSON', {
|
|
337
|
+
contentType: message.properties.contentType,
|
|
338
|
+
contentLength: message.content.length,
|
|
339
|
+
error: error instanceof Error ? error.message : 'Unknown deserialization error'
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Ensure exchange, queue, and binding exist before subscribing
|
|
346
|
+
* @param exchange Exchange name
|
|
347
|
+
* @param queue Queue name
|
|
348
|
+
* @param routingKey Routing key for binding
|
|
349
|
+
*/
|
|
350
|
+
private async ensureResourcesExist(
|
|
351
|
+
exchange: string,
|
|
352
|
+
queue: string,
|
|
353
|
+
routingKey: string
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
try {
|
|
356
|
+
// Create exchange if it doesn't exist
|
|
357
|
+
// Always use topic type by default
|
|
358
|
+
await this.resourceCreator.ensureExchange(exchange, 'topic');
|
|
359
|
+
|
|
360
|
+
// Create queue if it doesn't exist
|
|
361
|
+
await this.resourceCreator.ensureQueue(queue);
|
|
362
|
+
|
|
363
|
+
// Bind queue to exchange with routing key
|
|
364
|
+
await this.resourceCreator.bindQueue(queue, exchange, routingKey);
|
|
365
|
+
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throw new SubscriptionError('Failed to ensure resources exist for subscription', {
|
|
368
|
+
exchange,
|
|
369
|
+
queue,
|
|
370
|
+
routingKey,
|
|
371
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Log subscription errors with detailed context
|
|
378
|
+
* @param error The error that occurred
|
|
379
|
+
* @param exchange Exchange name
|
|
380
|
+
* @param queue Queue name
|
|
381
|
+
* @param routingKey Routing key
|
|
382
|
+
*/
|
|
383
|
+
private logSubscriptionError(
|
|
384
|
+
error: unknown,
|
|
385
|
+
exchange: string,
|
|
386
|
+
queue: string,
|
|
387
|
+
routingKey: string
|
|
388
|
+
): void {
|
|
389
|
+
this.logger.logError('Message subscription failed', error as Error, {
|
|
390
|
+
exchange,
|
|
391
|
+
queue,
|
|
392
|
+
routingKey,
|
|
393
|
+
operation: 'subscribe'
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get active consumer information for monitoring
|
|
399
|
+
*/
|
|
400
|
+
public getActiveConsumers(): string[] {
|
|
401
|
+
return Array.from(this.activeConsumers.keys());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Handle failed message by routing to DLQ or acknowledging based on configuration
|
|
406
|
+
* @param message Failed RabbitMQ message
|
|
407
|
+
* @param queueName Original queue name
|
|
408
|
+
* @param channel Channel for acknowledgment
|
|
409
|
+
*/
|
|
410
|
+
private async handleFailedMessage(
|
|
411
|
+
message: amqp.ConsumeMessage,
|
|
412
|
+
queueName: string,
|
|
413
|
+
channel: amqp.Channel
|
|
414
|
+
): Promise<void> {
|
|
415
|
+
try {
|
|
416
|
+
if (this.dlqHandler.isEnabled()) {
|
|
417
|
+
// Check retry count to determine if message should go to DLQ
|
|
418
|
+
const retryCount = this.getRetryCount(message);
|
|
419
|
+
const maxRetries = this.dlqHandler.getDLQConfig().maxRetries || 3;
|
|
420
|
+
|
|
421
|
+
if (retryCount >= maxRetries) {
|
|
422
|
+
// Route to DLQ after max retries exceeded
|
|
423
|
+
await this.dlqHandler.handleFailedMessage(message, queueName);
|
|
424
|
+
|
|
425
|
+
// Acknowledge the message since it's been routed to DLQ
|
|
426
|
+
channel.ack(message);
|
|
427
|
+
|
|
428
|
+
this.logger.info(`Message routed to DLQ after retries`, {
|
|
429
|
+
retryCount,
|
|
430
|
+
queue: queueName,
|
|
431
|
+
deliveryTag: message.fields.deliveryTag
|
|
432
|
+
});
|
|
433
|
+
} else {
|
|
434
|
+
// Increment retry count and requeue for retry
|
|
435
|
+
await this.requeueWithRetryCount(message, channel, retryCount + 1);
|
|
436
|
+
|
|
437
|
+
this.logger.info(`Message requeued for retry`, {
|
|
438
|
+
retryAttempt: retryCount + 1,
|
|
439
|
+
maxRetries,
|
|
440
|
+
queue: queueName,
|
|
441
|
+
deliveryTag: message.fields.deliveryTag
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
// DLQ is disabled, acknowledge failed messages without reprocessing
|
|
446
|
+
channel.ack(message);
|
|
447
|
+
|
|
448
|
+
this.logger.info(`DLQ disabled. Failed message acknowledged without reprocessing`, {
|
|
449
|
+
queue: queueName,
|
|
450
|
+
deliveryTag: message.fields.deliveryTag
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
this.logger.logError(`Failed to handle failed message`, error as Error, {
|
|
455
|
+
queue: queueName,
|
|
456
|
+
deliveryTag: message.fields.deliveryTag
|
|
457
|
+
});
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Get retry count from message headers
|
|
464
|
+
* @param message RabbitMQ message
|
|
465
|
+
* @returns Current retry count
|
|
466
|
+
*/
|
|
467
|
+
private getRetryCount(message: amqp.ConsumeMessage): number {
|
|
468
|
+
if (!message.properties.headers) {
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return message.properties.headers['x-retry-count'] || 0;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Requeue message with incremented retry count
|
|
477
|
+
* @param message Original message
|
|
478
|
+
* @param channel Channel for operations
|
|
479
|
+
* @param retryCount New retry count
|
|
480
|
+
*/
|
|
481
|
+
private async requeueWithRetryCount(
|
|
482
|
+
message: amqp.ConsumeMessage,
|
|
483
|
+
channel: amqp.Channel,
|
|
484
|
+
retryCount: number
|
|
485
|
+
): Promise<void> {
|
|
486
|
+
try {
|
|
487
|
+
// Add retry delay if configured
|
|
488
|
+
const retryDelay = this.dlqHandler.getDLQConfig().retryDelay || 0;
|
|
489
|
+
|
|
490
|
+
if (retryDelay > 0) {
|
|
491
|
+
// Use setTimeout for delay before requeuing
|
|
492
|
+
setTimeout(() => {
|
|
493
|
+
this.republishWithRetryCount(message, channel, retryCount).catch(error => {
|
|
494
|
+
this.logger.logError('Error republishing message with retry count', error as Error, {
|
|
495
|
+
retryCount,
|
|
496
|
+
deliveryTag: message.fields.deliveryTag
|
|
497
|
+
});
|
|
498
|
+
// Fallback to regular nack
|
|
499
|
+
try {
|
|
500
|
+
channel.nack(message, false, true);
|
|
501
|
+
} catch (nackError) {
|
|
502
|
+
this.logger.logError('Error negative acknowledging message', nackError as Error, {
|
|
503
|
+
deliveryTag: message.fields.deliveryTag
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}, retryDelay);
|
|
508
|
+
|
|
509
|
+
// Acknowledge original message since we're republishing
|
|
510
|
+
channel.ack(message);
|
|
511
|
+
} else {
|
|
512
|
+
// No delay, just nack with requeue
|
|
513
|
+
channel.nack(message, false, true);
|
|
514
|
+
}
|
|
515
|
+
} catch (error) {
|
|
516
|
+
this.logger.logError('Error requeuing message with retry count', error as Error, {
|
|
517
|
+
retryCount,
|
|
518
|
+
deliveryTag: message.fields.deliveryTag
|
|
519
|
+
});
|
|
520
|
+
// Fallback to regular nack
|
|
521
|
+
channel.nack(message, false, true);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Republish message with updated retry count
|
|
527
|
+
* @param message Original message
|
|
528
|
+
* @param channel Channel for operations
|
|
529
|
+
* @param retryCount New retry count
|
|
530
|
+
*/
|
|
531
|
+
private async republishWithRetryCount(
|
|
532
|
+
message: amqp.ConsumeMessage,
|
|
533
|
+
channel: amqp.Channel,
|
|
534
|
+
retryCount: number
|
|
535
|
+
): Promise<void> {
|
|
536
|
+
try {
|
|
537
|
+
// Update headers with retry count
|
|
538
|
+
const updatedHeaders = {
|
|
539
|
+
...message.properties.headers,
|
|
540
|
+
'x-retry-count': retryCount,
|
|
541
|
+
'x-retry-timestamp': Date.now()
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const publishOptions = {
|
|
545
|
+
...message.properties,
|
|
546
|
+
headers: updatedHeaders
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// Republish to the same exchange and routing key
|
|
550
|
+
const published = channel.publish(
|
|
551
|
+
message.fields.exchange,
|
|
552
|
+
message.fields.routingKey,
|
|
553
|
+
message.content,
|
|
554
|
+
publishOptions
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
if (!published) {
|
|
558
|
+
throw new Error('Failed to republish message - channel buffer full');
|
|
559
|
+
}
|
|
560
|
+
} catch (error) {
|
|
561
|
+
const errorMessage = `Failed to republish message with retry count: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
562
|
+
throw new Error(errorMessage);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Extract queue name from message (fallback to routing key if not available)
|
|
568
|
+
* @param message RabbitMQ message
|
|
569
|
+
* @returns Queue name
|
|
570
|
+
*/
|
|
571
|
+
private getQueueNameFromMessage(message: amqp.ConsumeMessage): string {
|
|
572
|
+
// Try to get queue name from headers first
|
|
573
|
+
if (message.properties.headers && message.properties.headers['x-original-queue']) {
|
|
574
|
+
return message.properties.headers['x-original-queue'];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Fallback to routing key (common pattern)
|
|
578
|
+
return message.fields.routingKey || 'unknown-queue';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Cleanup all active consumers (useful for shutdown)
|
|
583
|
+
*/
|
|
584
|
+
public async cleanup(): Promise<void> {
|
|
585
|
+
const queues = Array.from(this.activeConsumers.keys());
|
|
586
|
+
|
|
587
|
+
for (const queue of queues) {
|
|
588
|
+
try {
|
|
589
|
+
await this.unsubscribe(queue);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
this.logger.logError(`Error cleaning up consumer for queue`, error as Error, { queue });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
this.logger.info('MessageSubscriber cleanup completed', { cleanedQueues: queues.length });
|
|
596
|
+
}
|
|
597
|
+
}
|