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.

Files changed (140) hide show
  1. package/.eslintrc.js +23 -0
  2. package/.kiro/specs/sdk-rabbitmq/design.md +369 -0
  3. package/.kiro/specs/sdk-rabbitmq/requirements.md +97 -0
  4. package/.kiro/specs/sdk-rabbitmq/tasks.md +248 -0
  5. package/README.md +273 -5
  6. package/bun.lock +790 -0
  7. package/config.example.json +13 -0
  8. package/dist/components/ConfigurationManager.d.ts +35 -0
  9. package/dist/components/ConfigurationManager.d.ts.map +1 -0
  10. package/dist/components/ConfigurationManager.js +118 -0
  11. package/dist/components/ConfigurationManager.js.map +1 -0
  12. package/dist/components/ConnectionManager.d.ts +93 -0
  13. package/dist/components/ConnectionManager.d.ts.map +1 -0
  14. package/dist/components/ConnectionManager.js +349 -0
  15. package/dist/components/ConnectionManager.js.map +1 -0
  16. package/dist/components/DLQHandler.d.ts +81 -0
  17. package/dist/components/DLQHandler.d.ts.map +1 -0
  18. package/dist/components/DLQHandler.js +228 -0
  19. package/dist/components/DLQHandler.js.map +1 -0
  20. package/dist/components/Logger.d.ts +77 -0
  21. package/dist/components/Logger.d.ts.map +1 -0
  22. package/dist/components/Logger.js +193 -0
  23. package/dist/components/Logger.js.map +1 -0
  24. package/dist/components/MessagePublisher.d.ts +49 -0
  25. package/dist/components/MessagePublisher.d.ts.map +1 -0
  26. package/dist/components/MessagePublisher.js +158 -0
  27. package/dist/components/MessagePublisher.js.map +1 -0
  28. package/dist/components/MessageSubscriber.d.ts +108 -0
  29. package/dist/components/MessageSubscriber.d.ts.map +1 -0
  30. package/dist/components/MessageSubscriber.js +503 -0
  31. package/dist/components/MessageSubscriber.js.map +1 -0
  32. package/dist/components/ResourceCreator.d.ts +89 -0
  33. package/dist/components/ResourceCreator.d.ts.map +1 -0
  34. package/dist/components/ResourceCreator.js +352 -0
  35. package/dist/components/ResourceCreator.js.map +1 -0
  36. package/dist/components/SdkRabbitmq.d.ts +103 -0
  37. package/dist/components/SdkRabbitmq.d.ts.map +1 -0
  38. package/dist/components/SdkRabbitmq.js +364 -0
  39. package/dist/components/SdkRabbitmq.js.map +1 -0
  40. package/dist/components/index.d.ts +9 -0
  41. package/dist/components/index.d.ts.map +1 -0
  42. package/dist/components/index.js +20 -0
  43. package/dist/components/index.js.map +1 -0
  44. package/dist/index.d.ts +5 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +27 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/interfaces/IConfiguration.d.ts +35 -0
  49. package/dist/interfaces/IConfiguration.d.ts.map +1 -0
  50. package/dist/interfaces/IConfiguration.js +3 -0
  51. package/dist/interfaces/IConfiguration.js.map +1 -0
  52. package/dist/interfaces/IConnection.d.ts +21 -0
  53. package/dist/interfaces/IConnection.d.ts.map +1 -0
  54. package/dist/interfaces/IConnection.js +3 -0
  55. package/dist/interfaces/IConnection.js.map +1 -0
  56. package/dist/interfaces/IDLQ.d.ts +12 -0
  57. package/dist/interfaces/IDLQ.d.ts.map +1 -0
  58. package/dist/interfaces/IDLQ.js +3 -0
  59. package/dist/interfaces/IDLQ.js.map +1 -0
  60. package/dist/interfaces/IErrors.d.ts +33 -0
  61. package/dist/interfaces/IErrors.d.ts.map +1 -0
  62. package/dist/interfaces/IErrors.js +56 -0
  63. package/dist/interfaces/IErrors.js.map +1 -0
  64. package/dist/interfaces/ILogger.d.ts +14 -0
  65. package/dist/interfaces/ILogger.d.ts.map +1 -0
  66. package/dist/interfaces/ILogger.js +3 -0
  67. package/dist/interfaces/ILogger.js.map +1 -0
  68. package/dist/interfaces/IMessage.d.ts +52 -0
  69. package/dist/interfaces/IMessage.d.ts.map +1 -0
  70. package/dist/interfaces/IMessage.js +3 -0
  71. package/dist/interfaces/IMessage.js.map +1 -0
  72. package/dist/interfaces/IResource.d.ts +31 -0
  73. package/dist/interfaces/IResource.d.ts.map +1 -0
  74. package/dist/interfaces/IResource.js +3 -0
  75. package/dist/interfaces/IResource.js.map +1 -0
  76. package/dist/interfaces/ISdkRabbitmq.d.ts +17 -0
  77. package/dist/interfaces/ISdkRabbitmq.d.ts.map +1 -0
  78. package/dist/interfaces/ISdkRabbitmq.js +3 -0
  79. package/dist/interfaces/ISdkRabbitmq.js.map +1 -0
  80. package/dist/interfaces/index.d.ts +9 -0
  81. package/dist/interfaces/index.d.ts.map +1 -0
  82. package/dist/interfaces/index.js +33 -0
  83. package/dist/interfaces/index.js.map +1 -0
  84. package/dist/utils/configSchema.d.ts +8 -0
  85. package/dist/utils/configSchema.d.ts.map +1 -0
  86. package/dist/utils/configSchema.js +51 -0
  87. package/dist/utils/configSchema.js.map +1 -0
  88. package/docker-compose.yml +24 -0
  89. package/example.ts +65 -0
  90. package/examples/README-dynamic-routing.md +155 -0
  91. package/examples/bind-unbind-example.js +56 -0
  92. package/examples/test-chatbot-exchange.ts +83 -0
  93. package/examples/test-dynamic-routing-flow.js +299 -0
  94. package/examples/test-dynamic-routing-flow.ts +355 -0
  95. package/examples/test-no-disconnect.ts +0 -0
  96. package/examples/test-raw-rabbitmq.js +68 -0
  97. package/examples/test-same-channel.ts +81 -0
  98. package/examples/test-schedule-flow.ts +713 -0
  99. package/examples/test-simple-greeting.ts +66 -0
  100. package/examples/test-simple-schedule.ts +76 -0
  101. package/examples/test-wildcard.ts +364 -0
  102. package/jest.config.js +17 -0
  103. package/package.json +42 -4
  104. package/preinstall.js +1 -0
  105. package/prompts/test-dynamic-routing-flow.md +46 -0
  106. package/run.js +4 -0
  107. package/scripts/run-dynamic-routing-test.ts +31 -0
  108. package/src/.gitkeep +1 -0
  109. package/src/components/.gitkeep +1 -0
  110. package/src/components/ConfigurationManager.ts +104 -0
  111. package/src/components/ConnectionManager.ts +357 -0
  112. package/src/components/DLQHandler.ts +271 -0
  113. package/src/components/Logger.ts +224 -0
  114. package/src/components/MessagePublisher.ts +180 -0
  115. package/src/components/MessageSubscriber.ts +597 -0
  116. package/src/components/ResourceCreator.ts +411 -0
  117. package/src/components/SdkRabbitmq.ts +443 -0
  118. package/src/components/__tests__/ConfigurationManager.test.ts +357 -0
  119. package/src/components/__tests__/ConnectionManager.test.ts +387 -0
  120. package/src/components/__tests__/DLQHandler.test.ts +399 -0
  121. package/src/components/__tests__/Logger.test.ts +354 -0
  122. package/src/components/__tests__/MessagePublisher.test.ts +337 -0
  123. package/src/components/__tests__/MessageSubscriber.test.ts +542 -0
  124. package/src/components/__tests__/ResourceCreator.test.ts +465 -0
  125. package/src/components/__tests__/SdkRabbitmq.integration.test.ts +433 -0
  126. package/src/components/index.ts +8 -0
  127. package/src/index.ts +11 -0
  128. package/src/interfaces/.gitkeep +1 -0
  129. package/src/interfaces/IConfiguration.ts +38 -0
  130. package/src/interfaces/IConnection.ts +27 -0
  131. package/src/interfaces/IDLQ.ts +13 -0
  132. package/src/interfaces/IErrors.ts +53 -0
  133. package/src/interfaces/ILogger.ts +16 -0
  134. package/src/interfaces/IMessage.ts +65 -0
  135. package/src/interfaces/IResource.ts +35 -0
  136. package/src/interfaces/ISdkRabbitmq.ts +26 -0
  137. package/src/interfaces/index.ts +23 -0
  138. package/src/utils/.gitkeep +1 -0
  139. package/src/utils/configSchema.ts +58 -0
  140. 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
+ }