rabbitmq-with-retry-and-dlq 1.0.24 → 1.0.26

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.
@@ -59,6 +59,8 @@ class RabbitMQConsumer extends events_1.EventEmitter {
59
59
  super(); // Initialize EventEmitter
60
60
  this.isConnected = false;
61
61
  this.activeQueues = [];
62
+ // Store retry configurations for each queue (queue-level config, not message-level)
63
+ this.queueRetryConfigs = new Map();
62
64
  // Support both string URL (backward compatible) and options object
63
65
  if (typeof options === 'string') {
64
66
  this.rabbitUrl = options;
@@ -67,8 +69,10 @@ class RabbitMQConsumer extends events_1.EventEmitter {
67
69
  }
68
70
  else {
69
71
  this.rabbitUrl = options?.url || 'amqp://localhost';
70
- this.reconnectIntervalSeconds = options?.reconnectIntervalSeconds || types_1.RECONNECT_INTERVAL_SECONDS;
71
- this.connectionTimeoutMs = options?.connectionTimeoutMs || types_1.CONNECTION_TIMEOUT_MS;
72
+ this.reconnectIntervalSeconds =
73
+ options?.reconnectIntervalSeconds || types_1.RECONNECT_INTERVAL_SECONDS;
74
+ this.connectionTimeoutMs =
75
+ options?.connectionTimeoutMs || types_1.CONNECTION_TIMEOUT_MS;
72
76
  }
73
77
  }
74
78
  /**
@@ -197,8 +201,16 @@ class RabbitMQConsumer extends events_1.EventEmitter {
197
201
  // Auto-connect on first use (lazy loading)
198
202
  await this.ensureConnection();
199
203
  // Normalize to array
200
- const exchanges = Array.isArray(exchangeNames) ? exchangeNames : [exchangeNames];
204
+ const exchanges = Array.isArray(exchangeNames)
205
+ ? exchangeNames
206
+ : [exchangeNames];
201
207
  const { exchangeType = 'direct', durable = true } = options;
208
+ // Validate exchange names
209
+ for (const exchangeName of exchanges) {
210
+ if (!exchangeName || exchangeName.trim() === '') {
211
+ throw new Error('Exchange name cannot be empty');
212
+ }
213
+ }
202
214
  try {
203
215
  await this.channel.addSetup(async (ch) => {
204
216
  for (const exchangeName of exchanges) {
@@ -229,8 +241,11 @@ class RabbitMQConsumer extends events_1.EventEmitter {
229
241
  * await consumer.assertQueues(['orders-worker', 'payments-worker']);
230
242
  *
231
243
  * @example
232
- * // Assert queue with DLQ setup
244
+ * // Assert queue with exchange binding and retry config
233
245
  * await consumer.assertQueues('orders-worker', {
246
+ * exchangeName: 'orders',
247
+ * exchangeType: 'direct',
248
+ * routingKey: 'order.created',
234
249
  * retryConfig: { maxRetries: 3, retryDelayMs: 5000 }
235
250
  * });
236
251
  */
@@ -239,21 +254,51 @@ class RabbitMQConsumer extends events_1.EventEmitter {
239
254
  await this.ensureConnection();
240
255
  // Normalize to array
241
256
  const queues = Array.isArray(queueNames) ? queueNames : [queueNames];
242
- const { durable = true, exclusive = false, autoDelete = false, retryConfig, } = options;
257
+ const { durable = true, exclusive = false, autoDelete = false, exchangeName, exchangeType = 'direct', routingKey, retryConfig, } = options;
258
+ // Validate queue names
259
+ for (const queueName of queues) {
260
+ if (!queueName || queueName.trim() === '') {
261
+ throw new Error('Queue name cannot be empty');
262
+ }
263
+ }
264
+ // Validate exchange name if provided
265
+ if (exchangeName && exchangeName.trim() === '') {
266
+ throw new Error('Exchange name cannot be empty');
267
+ }
268
+ // Note: If exchangeName is provided without routingKey, queueName will be used as routingKey for each queue
243
269
  // Validate retry configuration if provided
244
270
  if (retryConfig) {
245
271
  (0, validation_1.validateRetryConfig)(retryConfig);
246
272
  }
247
273
  try {
274
+ const defaultExchange = '';
275
+ const dlxName = exchangeName
276
+ ? `${exchangeName}${types_1.DLX_SUFFIX}`
277
+ : `default${types_1.DLX_SUFFIX}`;
278
+ const effectiveExchange = exchangeName || defaultExchange;
279
+ const effectiveRoutingKey = routingKey || (queues.length === 1 ? queues[0] : '');
280
+ // Assert all queues
248
281
  await this.channel.addSetup(async (ch) => {
282
+ // Assert exchange if provided
283
+ if (exchangeName) {
284
+ await ch.assertExchange(exchangeName, exchangeType, { durable });
285
+ console.log(`✓ Exchange asserted: ${exchangeName} (${exchangeType})`);
286
+ }
249
287
  for (const queueName of queues) {
250
288
  const queueArgs = {};
289
+ const effectiveKey = routingKey || queueName;
251
290
  // Setup DLQ arguments if retry config is provided
252
291
  if (retryConfig && retryConfig.maxRetries > 0) {
253
- // Messages that fail will be sent to retry queue
254
- queueArgs['x-dead-letter-exchange'] = '';
255
- queueArgs['x-dead-letter-routing-key'] = `${queueName}.retry`;
292
+ // With retry: failed messages go to retry queue first
293
+ queueArgs['x-dead-letter-exchange'] = effectiveExchange;
294
+ queueArgs['x-dead-letter-routing-key'] = `${effectiveKey}${types_1.RETRY_QUEUE_SUFFIX}`;
295
+ }
296
+ else if (retryConfig) {
297
+ // No retry (maxRetries = 0): go directly to DLQ
298
+ queueArgs['x-dead-letter-exchange'] = dlxName;
299
+ queueArgs['x-dead-letter-routing-key'] = `${effectiveKey}${types_1.DLQ_SUFFIX}`;
256
300
  }
301
+ // Assert queue (idempotent - RabbitMQ won't recreate if exists with same params)
257
302
  await ch.assertQueue(queueName, {
258
303
  durable,
259
304
  exclusive,
@@ -261,12 +306,23 @@ class RabbitMQConsumer extends events_1.EventEmitter {
261
306
  arguments: Object.keys(queueArgs).length > 0 ? queueArgs : undefined,
262
307
  });
263
308
  console.log(`✓ Queue asserted: ${queueName}`);
264
- // Setup retry and DLQ queues if retry config is provided
265
- if (retryConfig) {
266
- await this.setupRetryAndDLQQueues(ch, queueName, retryConfig);
309
+ // Bind queue to exchange if exchangeName is provided (idempotent operation)
310
+ if (exchangeName) {
311
+ await ch.bindQueue(queueName, exchangeName, effectiveKey);
312
+ console.log(`✓ Queue bound to exchange: ${queueName} -> ${exchangeName} (${effectiveKey})`);
267
313
  }
268
314
  }
269
315
  });
316
+ // Setup DLQ infrastructure for all queues if retry config is provided
317
+ if (retryConfig) {
318
+ for (const queueName of queues) {
319
+ // Store retry config for this queue (infrastructure-level configuration)
320
+ this.queueRetryConfigs.set(queueName, retryConfig);
321
+ // Use provided exchange/routingKey or defaults
322
+ const effectiveKey = routingKey || queueName;
323
+ await this.setupDLQInfrastructure(queueName, effectiveExchange, effectiveKey, retryConfig);
324
+ }
325
+ }
270
326
  console.log(`✓ Successfully asserted ${queues.length} queue(s)`);
271
327
  }
272
328
  catch (error) {
@@ -275,30 +331,135 @@ class RabbitMQConsumer extends events_1.EventEmitter {
275
331
  }
276
332
  }
277
333
  /**
278
- * Setup retry queue and DLQ for a main queue
334
+ * Delete queues (Best Practice for cleanup/testing)
335
+ * Can be called with a single queue name or multiple queue names
336
+ * Also deletes associated retry and DLQ queues if they exist
337
+ *
338
+ * @param queueNames - Single queue name (string) or array of queue names
339
+ * @param options - Delete options (includeRetry, includeDLQ)
340
+ *
341
+ * @example
342
+ * // Delete a single queue
343
+ * await consumer.deleteQueues('orders');
344
+ *
345
+ * @example
346
+ * // Delete multiple queues
347
+ * await consumer.deleteQueues(['orders', 'payments', 'notifications']);
348
+ *
349
+ * @example
350
+ * // Delete queue without retry/DLQ queues
351
+ * await consumer.deleteQueues('orders', {
352
+ * includeRetry: false,
353
+ * includeDLQ: false
354
+ * });
279
355
  */
280
- async setupRetryAndDLQQueues(ch, queueName, retryConfig) {
281
- const retryQueueName = `${queueName}.retry`;
282
- const dlqName = `${queueName}.dlq`;
283
- // Create retry queue (messages come back to main queue after TTL)
284
- if (retryConfig.maxRetries > 0) {
285
- await ch.assertQueue(retryQueueName, {
356
+ async deleteQueues(queueNames, options = {}) {
357
+ // Auto-connect on first use (lazy loading)
358
+ await this.ensureConnection();
359
+ // Normalize to array
360
+ const queues = Array.isArray(queueNames) ? queueNames : [queueNames];
361
+ const { includeRetry = true, includeDLQ = true } = options;
362
+ try {
363
+ let deletedCount = 0;
364
+ // Delete all queues
365
+ await this.channel.addSetup(async (ch) => {
366
+ for (const queueName of queues) {
367
+ // Delete main queue
368
+ try {
369
+ await ch.deleteQueue(queueName, { ifEmpty: false });
370
+ console.log(`✓ Deleted queue: ${queueName}`);
371
+ deletedCount++;
372
+ }
373
+ catch (error) {
374
+ // Queue doesn't exist or already deleted
375
+ if (error.code !== 404) {
376
+ console.warn(`Warning: Could not delete queue ${queueName}:`, error.message);
377
+ }
378
+ }
379
+ // Delete retry queue if requested
380
+ if (includeRetry) {
381
+ try {
382
+ await ch.deleteQueue(`${queueName}${types_1.RETRY_QUEUE_SUFFIX}`, {
383
+ ifEmpty: false,
384
+ });
385
+ console.log(`✓ Deleted retry queue: ${queueName}${types_1.RETRY_QUEUE_SUFFIX}`);
386
+ deletedCount++;
387
+ }
388
+ catch (error) {
389
+ // Retry queue doesn't exist, skip
390
+ if (error.code !== 404) {
391
+ console.warn(`Warning: Could not delete retry queue ${queueName}${types_1.RETRY_QUEUE_SUFFIX}:`, error.message);
392
+ }
393
+ }
394
+ }
395
+ // Delete DLQ if requested
396
+ if (includeDLQ) {
397
+ try {
398
+ await ch.deleteQueue(`${queueName}${types_1.DLQ_SUFFIX}`, {
399
+ ifEmpty: false,
400
+ });
401
+ console.log(`✓ Deleted DLQ: ${queueName}${types_1.DLQ_SUFFIX}`);
402
+ deletedCount++;
403
+ }
404
+ catch (error) {
405
+ // DLQ doesn't exist, skip
406
+ if (error.code !== 404) {
407
+ console.warn(`Warning: Could not delete DLQ ${queueName}${types_1.DLQ_SUFFIX}:`, error.message);
408
+ }
409
+ }
410
+ }
411
+ }
412
+ });
413
+ console.log(`✓ Successfully deleted ${deletedCount} queue(s)`);
414
+ }
415
+ catch (error) {
416
+ console.error('Failed to delete queues:', error);
417
+ throw error;
418
+ }
419
+ }
420
+ /**
421
+ * Setup Dead Letter Queue (DLQ) infrastructure for a queue with retry mechanism
422
+ */
423
+ async setupDLQInfrastructure(queueName, exchangeName, routingKey, retryConfig) {
424
+ await this.channel.addSetup(async (ch) => {
425
+ const dlxName = `${exchangeName}${types_1.DLX_SUFFIX}`;
426
+ const dlqName = `${queueName}${types_1.DLQ_SUFFIX}`;
427
+ const retryQueueName = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
428
+ // 1. Create Dead Letter Exchange (DLX) - only if exchangeName is not empty
429
+ if (exchangeName) {
430
+ await ch.assertExchange(dlxName, 'direct', { durable: true });
431
+ }
432
+ // 2. Create Dead Letter Queue (DLQ) - Final resting place for failed messages
433
+ // assertQueue is idempotent - won't recreate if exists with same params
434
+ await ch.assertQueue(dlqName, {
286
435
  durable: true,
287
436
  arguments: {
288
- 'x-dead-letter-exchange': '',
289
- 'x-dead-letter-routing-key': queueName,
437
+ 'x-queue-mode': 'lazy', // Better for long-term storage, saves memory
290
438
  },
291
439
  });
292
- console.log(`✓ Retry queue asserted: ${retryQueueName}`);
293
- }
294
- // Create DLQ (final resting place for failed messages)
295
- await ch.assertQueue(dlqName, {
296
- durable: true,
297
- arguments: {
298
- 'x-queue-mode': 'lazy',
299
- },
440
+ // Bind DLQ to DLX if exchange is used
441
+ if (exchangeName) {
442
+ await ch.bindQueue(dlqName, dlxName, `${routingKey}${types_1.DLQ_SUFFIX}`);
443
+ }
444
+ console.log(`✓ DLQ asserted: ${dlqName}`);
445
+ // 3. Create Retry Queue (if retry is enabled)
446
+ if (retryConfig && retryConfig.maxRetries > 0) {
447
+ // assertQueue is idempotent - won't recreate if exists with same params
448
+ await ch.assertQueue(retryQueueName, {
449
+ durable: true,
450
+ arguments: {
451
+ // NO fixed TTL - we use per-message expiration for exponential backoff
452
+ 'x-dead-letter-exchange': exchangeName || '', // Send back to main exchange after TTL
453
+ 'x-dead-letter-routing-key': routingKey, // Use original routing key
454
+ },
455
+ });
456
+ console.log(`✓ Retry queue asserted: ${retryQueueName}`);
457
+ console.log(`DLQ infrastructure setup complete for queue: ${queueName} (max retries: ${retryConfig.maxRetries}, exponential backoff enabled)`);
458
+ }
459
+ else {
460
+ console.log(`DLQ infrastructure setup complete for queue: ${queueName} (no retries)`);
461
+ }
300
462
  });
301
- console.log(`✓ DLQ asserted: ${dlqName}`);
302
463
  }
303
464
  /**
304
465
  * Bind a queue to an exchange with a routing key (Best Practice for Consumers)
@@ -327,6 +488,16 @@ class RabbitMQConsumer extends events_1.EventEmitter {
327
488
  // Auto-connect on first use (lazy loading)
328
489
  await this.ensureConnection();
329
490
  const { exchangeType, durable = true } = options;
491
+ // Validate inputs
492
+ if (!queueName || queueName.trim() === '') {
493
+ throw new Error('Queue name cannot be empty');
494
+ }
495
+ if (!exchangeName || exchangeName.trim() === '') {
496
+ throw new Error('Exchange name cannot be empty');
497
+ }
498
+ if (!routingKey || routingKey.trim() === '') {
499
+ throw new Error('Routing key cannot be empty');
500
+ }
330
501
  try {
331
502
  await this.channel.addSetup(async (ch) => {
332
503
  // If exchangeType is provided, assert the exchange first (idempotent)
@@ -390,7 +561,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
390
561
  const queueArgs = {};
391
562
  if (retryConfig && retryConfig.maxRetries > 0) {
392
563
  queueArgs['x-dead-letter-exchange'] = '';
393
- queueArgs['x-dead-letter-routing-key'] = `${queueName}.retry`;
564
+ queueArgs['x-dead-letter-routing-key'] = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
394
565
  }
395
566
  // 3. Assert queue
396
567
  await ch.assertQueue(queueName, {
@@ -402,8 +573,10 @@ class RabbitMQConsumer extends events_1.EventEmitter {
402
573
  console.log(`✓ Queue asserted: ${queueName}`);
403
574
  // 4. Setup retry and DLQ queues if retry config is provided
404
575
  if (retryConfig) {
405
- const retryQueueName = `${queueName}.retry`;
406
- const dlqName = `${queueName}.dlq`;
576
+ // Store retry config for this queue (infrastructure-level configuration)
577
+ this.queueRetryConfigs.set(queueName, retryConfig);
578
+ const retryQueueName = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
579
+ const dlqName = `${queueName}${types_1.DLQ_SUFFIX}`;
407
580
  if (retryConfig.maxRetries > 0) {
408
581
  await ch.assertQueue(retryQueueName, {
409
582
  durable: true,
@@ -504,16 +677,50 @@ class RabbitMQConsumer extends events_1.EventEmitter {
504
677
  }
505
678
  /**
506
679
  * Consume messages from a queue with automatic retry and DLQ handling
680
+ *
681
+ * Note: Queue should be set up separately before consuming:
682
+ * - Use assertQueues() to create queue with exchange binding
683
+ * - Use setupQueue() for complete setup with multiple routing keys
684
+ * - Use bindQueue() to add additional routing key bindings
685
+ *
686
+ * @example
687
+ * // Step 1: Setup queue with exchange and routing keys
688
+ * await consumer.setupQueue({
689
+ * queueName: 'orders',
690
+ * exchangeName: 'orders',
691
+ * exchangeType: 'topic',
692
+ * routingKeys: ['order.created', 'order.updated'],
693
+ * retryConfig: { maxRetries: 5 }
694
+ * });
695
+ *
696
+ * // Step 2: Consume (queue already set up)
697
+ * await consumer.consumeQueue({
698
+ * queueName: 'orders',
699
+ * onMessage: async (message) => {
700
+ * await processOrder(message);
701
+ * }
702
+ * });
703
+ *
704
+ * @example
705
+ * // Direct queue consumption (no exchange)
706
+ * await consumer.assertQueues('simple-queue');
707
+ * await consumer.consumeQueue({
708
+ * queueName: 'simple-queue',
709
+ * onMessage: async (message) => { ... }
710
+ * });
507
711
  */
508
712
  async consumeQueue(config) {
509
713
  // Auto-connect on first use (lazy loading)
510
714
  await this.ensureConnection();
511
715
  try {
512
- console.log('Consuming queue:', config);
513
- const { queueName, exchangeName, exchangeType = 'direct', routingKey, onMessage, options = {} } = config;
514
- const { durable = true, prefetch = types_1.DEFAULT_PREFETCH_COUNT, noAck = false, exclusive = false, autoDelete = false } = options;
716
+ const { queueName, onMessage, options = {} } = config;
717
+ const { durable = true, prefetch = types_1.DEFAULT_PREFETCH_COUNT, noAck = false, exclusive = false, autoDelete = false, } = options;
718
+ // Validate inputs
719
+ if (!queueName || queueName.trim() === '') {
720
+ throw new Error('Queue name cannot be empty');
721
+ }
515
722
  await this.channel.addSetup(async (ch) => {
516
- // Set prefetch count
723
+ // Set prefetch count (critical for flow control)
517
724
  await ch.prefetch(prefetch);
518
725
  // Check if queue exists (passive check - doesn't modify queue properties)
519
726
  try {
@@ -521,16 +728,9 @@ class RabbitMQConsumer extends events_1.EventEmitter {
521
728
  }
522
729
  catch (error) {
523
730
  // Queue doesn't exist, create it without DLQ config (for simple cases)
524
- // If using with retry/DLQ, publisher should create the queue first
731
+ // For production, use assertQueues() or setupQueue() first
525
732
  await ch.assertQueue(queueName, { durable, exclusive, autoDelete });
526
- }
527
- // If exchange is specified, assert exchange and bind queue
528
- if (exchangeName && routingKey) {
529
- await ch.assertExchange(exchangeName, exchangeType, { durable });
530
- // Bind queue to exchange (idempotent operation)
531
- await ch.bindQueue(queueName, exchangeName, routingKey);
532
- // Also bind the retry routing key
533
- await ch.bindQueue(queueName, exchangeName, `${routingKey}.retry`);
733
+ console.warn(`Queue ${queueName} was auto-created. Consider using assertQueues() or setupQueue() for proper setup.`);
534
734
  }
535
735
  // Start consuming
536
736
  await ch.consume(queueName, async (msg) => {
@@ -539,18 +739,33 @@ class RabbitMQConsumer extends events_1.EventEmitter {
539
739
  try {
540
740
  const content = JSON.parse(msg.content.toString());
541
741
  const headers = msg.properties.headers || {};
542
- // Extract retry information from headers (only what's needed for success path)
742
+ // Get retry config from queue setup (infrastructure-level config)
743
+ const queueRetryConfig = this.queueRetryConfigs.get(queueName);
744
+ // Extract retry information from headers OR use queue config (on first attempt)
543
745
  const currentRetryCount = headers['x-retry-count'] || 0;
544
- const maxRetries = headers['x-max-retries'] || 0;
746
+ const maxRetries = headers['x-max-retries'] !== undefined
747
+ ? headers['x-max-retries']
748
+ : (queueRetryConfig?.maxRetries || 0);
749
+ const baseRetryDelay = headers['x-base-retry-delay'] !== undefined
750
+ ? headers['x-base-retry-delay']
751
+ : (queueRetryConfig?.retryDelayMs || types_1.DEFAULT_RETRY_DELAY_MS);
752
+ const backoffStrategy = headers['x-backoff-strategy']
753
+ || (queueRetryConfig?.backoffStrategy || 'exponential');
754
+ const maxDelayMs = headers['x-max-delay']
755
+ || (queueRetryConfig?.maxDelayMs || types_1.DEFAULT_MAX_DELAY_MS);
756
+ const jitterMs = headers['x-jitter']
757
+ || (queueRetryConfig?.jitterMs || 0);
545
758
  const firstPublished = headers['x-first-published'] || new Date().toISOString();
546
759
  const messageInfo = {
547
760
  fields: msg.fields,
548
761
  properties: msg.properties,
549
762
  content: msg.content,
550
- originalMessage: msg
763
+ originalMessage: msg,
551
764
  };
552
765
  let timestamp = new Date().toISOString();
553
- const retryInfo = maxRetries > 0 ? ` (attempt ${currentRetryCount + 1}/${maxRetries + 1})` : '';
766
+ const retryInfo = maxRetries > 0
767
+ ? ` (attempt ${currentRetryCount + 1}/${maxRetries + 1})`
768
+ : '';
554
769
  console.log(`[${timestamp}] Processing from ${queueName}${retryInfo}:`, content);
555
770
  // Try to process the message
556
771
  await onMessage(content, messageInfo);
@@ -567,33 +782,47 @@ class RabbitMQConsumer extends events_1.EventEmitter {
567
782
  const errorMessage = error instanceof Error ? error.message : String(error);
568
783
  console.error(`[${timestamp}] ✗ Failed from ${queueName}:`, errorMessage);
569
784
  if (!noAck) {
785
+ // Re-extract retry config for error handling (same as in try block)
570
786
  const headers = msg.properties.headers || {};
787
+ const queueRetryConfig = this.queueRetryConfigs.get(queueName);
571
788
  const currentRetryCount = headers['x-retry-count'] || 0;
572
- const maxRetries = headers['x-max-retries'] || 0;
573
- const baseRetryDelay = headers['x-base-retry-delay'] || types_1.DEFAULT_RETRY_DELAY_MS;
574
- const backoffStrategy = headers['x-backoff-strategy'] || 'exponential';
575
- const maxDelayMs = headers['x-max-delay'] || types_1.DEFAULT_MAX_DELAY_MS;
576
- const jitterMs = headers['x-jitter'] || 0;
577
- // Check if we should retry
789
+ const maxRetries = headers['x-max-retries'] !== undefined
790
+ ? headers['x-max-retries']
791
+ : (queueRetryConfig?.maxRetries || 0);
792
+ const baseRetryDelay = headers['x-base-retry-delay'] !== undefined
793
+ ? headers['x-base-retry-delay']
794
+ : (queueRetryConfig?.retryDelayMs || types_1.DEFAULT_RETRY_DELAY_MS);
795
+ const backoffStrategy = headers['x-backoff-strategy']
796
+ || (queueRetryConfig?.backoffStrategy || 'exponential');
797
+ const maxDelayMs = headers['x-max-delay']
798
+ || (queueRetryConfig?.maxDelayMs || types_1.DEFAULT_MAX_DELAY_MS);
799
+ const jitterMs = headers['x-jitter']
800
+ || (queueRetryConfig?.jitterMs || 0);
801
+ const firstPublished = headers['x-first-published'] || new Date().toISOString();
802
+ // Check if we should retry (using queue-level config)
578
803
  if (currentRetryCount < maxRetries) {
579
804
  const nextRetryCount = currentRetryCount + 1;
580
805
  // Calculate delay with backoff strategy
581
- const actualDelay = this.calculateRetryDelay(baseRetryDelay, // Use base delay, not the previous calculated delay
582
- currentRetryCount, backoffStrategy, maxDelayMs, jitterMs);
806
+ const actualDelay = this.calculateRetryDelay(baseRetryDelay, currentRetryCount, backoffStrategy, maxDelayMs, jitterMs);
583
807
  const timestamp = new Date().toISOString();
584
808
  console.log(`[${timestamp}] 🔄 Retry ${nextRetryCount}/${maxRetries} in ${actualDelay}ms`);
585
809
  // Publish to retry queue with updated retry count
586
- const retryQueueName = `${queueName}.retry`;
810
+ const retryQueueName = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
587
811
  try {
588
812
  await ch.sendToQueue(retryQueueName, msg.content, {
589
813
  ...msg.properties,
590
- expiration: actualDelay.toString(), // Set per-message TTL for exponential backoff
814
+ expiration: actualDelay.toString(),
591
815
  headers: {
592
816
  ...headers,
593
817
  'x-retry-count': nextRetryCount,
594
818
  'x-retry-timestamp': new Date().toISOString(),
595
- 'x-base-retry-delay': baseRetryDelay, // Store the BASE delay (don't compound!)
596
- }
819
+ 'x-base-retry-delay': baseRetryDelay,
820
+ 'x-max-retries': maxRetries,
821
+ 'x-backoff-strategy': backoffStrategy,
822
+ 'x-max-delay': maxDelayMs,
823
+ 'x-jitter': jitterMs,
824
+ 'x-first-published': firstPublished,
825
+ },
597
826
  });
598
827
  // Acknowledge original message after successful retry queue publish
599
828
  ch.ack(msg);
@@ -624,7 +853,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
624
853
  const timestamp = new Date().toISOString();
625
854
  console.log(`[${timestamp}] 💀 Max retries exceeded, moving to DLQ`);
626
855
  // Manually send to DLQ (can't rely on nack because main queue's DLX routes to retry)
627
- const dlqName = `${queueName}.dlq`;
856
+ const dlqName = `${queueName}${types_1.DLQ_SUFFIX}`;
628
857
  // Extract error code for categorization
629
858
  const errorCode = this.extractErrorCode(error);
630
859
  try {
@@ -636,8 +865,10 @@ class RabbitMQConsumer extends events_1.EventEmitter {
636
865
  'x-death-timestamp': new Date().toISOString(),
637
866
  'x-last-error': errorMessage,
638
867
  'x-last-error-code': errorCode,
639
- 'x-is-retryable': this.isRetryableError(errorCode) ? 'true' : 'false',
640
- }
868
+ 'x-is-retryable': this.isRetryableError(errorCode)
869
+ ? 'true'
870
+ : 'false',
871
+ },
641
872
  });
642
873
  // Acknowledge original message after successful DLQ send
643
874
  ch.ack(msg);
@@ -681,17 +912,34 @@ class RabbitMQConsumer extends events_1.EventEmitter {
681
912
  }
682
913
  /**
683
914
  * Consume from multiple queues with the same connection
915
+ * Note: Queues should be set up separately before consuming
916
+ *
917
+ * @example
918
+ * // Setup queues first
919
+ * await consumer.setupQueue({ queueName: 'orders', ... });
920
+ * await consumer.setupQueue({ queueName: 'payments', ... });
921
+ *
922
+ * // Then consume from all
923
+ * await consumer.consumeMultipleQueues({
924
+ * queues: [
925
+ * {
926
+ * queueName: 'orders',
927
+ * onMessage: async (msg) => { ... }
928
+ * },
929
+ * {
930
+ * queueName: 'payments',
931
+ * onMessage: async (msg) => { ... }
932
+ * }
933
+ * ]
934
+ * });
684
935
  */
685
936
  async consumeMultipleQueues(config) {
686
937
  const { queues, options = {} } = config;
687
938
  for (const queueConfig of queues) {
688
939
  await this.consumeQueue({
689
940
  queueName: queueConfig.queueName,
690
- exchangeName: queueConfig.exchangeName,
691
- exchangeType: queueConfig.exchangeType,
692
- routingKey: queueConfig.routingKey,
693
941
  onMessage: queueConfig.onMessage,
694
- options
942
+ options,
695
943
  });
696
944
  }
697
945
  }
@@ -723,7 +971,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
723
971
  fields: msg.fields,
724
972
  properties: msg.properties,
725
973
  content: msg.content,
726
- originalMessage: msg
974
+ originalMessage: msg,
727
975
  };
728
976
  await onMessage(content, routingKey, messageInfo);
729
977
  ch.ack(msg);
@@ -751,7 +999,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
751
999
  getConsumerStats() {
752
1000
  return {
753
1001
  rabbitUrl: this.rabbitUrl,
754
- activeQueues: [...this.activeQueues]
1002
+ activeQueues: [...this.activeQueues],
755
1003
  };
756
1004
  }
757
1005
  /**
@@ -763,7 +1011,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
763
1011
  if (this.channel) {
764
1012
  // Give time for in-flight messages to complete processing
765
1013
  // In production, you might want to implement a more sophisticated drain mechanism
766
- await new Promise(resolve => setTimeout(resolve, 1000));
1014
+ await new Promise((resolve) => setTimeout(resolve, 1000));
767
1015
  await this.channel.close();
768
1016
  this.channel = undefined;
769
1017
  }