rabbitmq-with-retry-and-dlq 1.0.23 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +247 -134
- package/dist/consumerMq.d.ts +126 -3
- package/dist/consumerMq.d.ts.map +1 -1
- package/dist/consumerMq.js +363 -56
- package/dist/consumerMq.js.map +1 -1
- package/dist/publisherMq.d.ts +2 -70
- package/dist/publisherMq.d.ts.map +1 -1
- package/dist/publisherMq.js +76 -235
- package/dist/publisherMq.js.map +1 -1
- package/dist/types.d.ts +13 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/consumerMq.js
CHANGED
|
@@ -67,8 +67,10 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
67
67
|
}
|
|
68
68
|
else {
|
|
69
69
|
this.rabbitUrl = options?.url || 'amqp://localhost';
|
|
70
|
-
this.reconnectIntervalSeconds =
|
|
71
|
-
|
|
70
|
+
this.reconnectIntervalSeconds =
|
|
71
|
+
options?.reconnectIntervalSeconds || types_1.RECONNECT_INTERVAL_SECONDS;
|
|
72
|
+
this.connectionTimeoutMs =
|
|
73
|
+
options?.connectionTimeoutMs || types_1.CONNECTION_TIMEOUT_MS;
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
@@ -197,8 +199,16 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
197
199
|
// Auto-connect on first use (lazy loading)
|
|
198
200
|
await this.ensureConnection();
|
|
199
201
|
// Normalize to array
|
|
200
|
-
const exchanges = Array.isArray(exchangeNames)
|
|
202
|
+
const exchanges = Array.isArray(exchangeNames)
|
|
203
|
+
? exchangeNames
|
|
204
|
+
: [exchangeNames];
|
|
201
205
|
const { exchangeType = 'direct', durable = true } = options;
|
|
206
|
+
// Validate exchange names
|
|
207
|
+
for (const exchangeName of exchanges) {
|
|
208
|
+
if (!exchangeName || exchangeName.trim() === '') {
|
|
209
|
+
throw new Error('Exchange name cannot be empty');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
202
212
|
try {
|
|
203
213
|
await this.channel.addSetup(async (ch) => {
|
|
204
214
|
for (const exchangeName of exchanges) {
|
|
@@ -229,8 +239,11 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
229
239
|
* await consumer.assertQueues(['orders-worker', 'payments-worker']);
|
|
230
240
|
*
|
|
231
241
|
* @example
|
|
232
|
-
* // Assert queue with
|
|
242
|
+
* // Assert queue with exchange binding and retry config
|
|
233
243
|
* await consumer.assertQueues('orders-worker', {
|
|
244
|
+
* exchangeName: 'orders',
|
|
245
|
+
* exchangeType: 'direct',
|
|
246
|
+
* routingKey: 'order.created',
|
|
234
247
|
* retryConfig: { maxRetries: 3, retryDelayMs: 5000 }
|
|
235
248
|
* });
|
|
236
249
|
*/
|
|
@@ -239,21 +252,51 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
239
252
|
await this.ensureConnection();
|
|
240
253
|
// Normalize to array
|
|
241
254
|
const queues = Array.isArray(queueNames) ? queueNames : [queueNames];
|
|
242
|
-
const { durable = true, exclusive = false, autoDelete = false, retryConfig, } = options;
|
|
255
|
+
const { durable = true, exclusive = false, autoDelete = false, exchangeName, exchangeType = 'direct', routingKey, retryConfig, } = options;
|
|
256
|
+
// Validate queue names
|
|
257
|
+
for (const queueName of queues) {
|
|
258
|
+
if (!queueName || queueName.trim() === '') {
|
|
259
|
+
throw new Error('Queue name cannot be empty');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Validate exchange name if provided
|
|
263
|
+
if (exchangeName && exchangeName.trim() === '') {
|
|
264
|
+
throw new Error('Exchange name cannot be empty');
|
|
265
|
+
}
|
|
266
|
+
// Note: If exchangeName is provided without routingKey, queueName will be used as routingKey for each queue
|
|
243
267
|
// Validate retry configuration if provided
|
|
244
268
|
if (retryConfig) {
|
|
245
269
|
(0, validation_1.validateRetryConfig)(retryConfig);
|
|
246
270
|
}
|
|
247
271
|
try {
|
|
272
|
+
const defaultExchange = '';
|
|
273
|
+
const dlxName = exchangeName
|
|
274
|
+
? `${exchangeName}${types_1.DLX_SUFFIX}`
|
|
275
|
+
: `default${types_1.DLX_SUFFIX}`;
|
|
276
|
+
const effectiveExchange = exchangeName || defaultExchange;
|
|
277
|
+
const effectiveRoutingKey = routingKey || (queues.length === 1 ? queues[0] : '');
|
|
278
|
+
// Assert all queues
|
|
248
279
|
await this.channel.addSetup(async (ch) => {
|
|
280
|
+
// Assert exchange if provided
|
|
281
|
+
if (exchangeName) {
|
|
282
|
+
await ch.assertExchange(exchangeName, exchangeType, { durable });
|
|
283
|
+
console.log(`✓ Exchange asserted: ${exchangeName} (${exchangeType})`);
|
|
284
|
+
}
|
|
249
285
|
for (const queueName of queues) {
|
|
250
286
|
const queueArgs = {};
|
|
287
|
+
const effectiveKey = routingKey || queueName;
|
|
251
288
|
// Setup DLQ arguments if retry config is provided
|
|
252
289
|
if (retryConfig && retryConfig.maxRetries > 0) {
|
|
253
|
-
//
|
|
254
|
-
queueArgs['x-dead-letter-exchange'] =
|
|
255
|
-
queueArgs['x-dead-letter-routing-key'] = `${
|
|
290
|
+
// With retry: failed messages go to retry queue first
|
|
291
|
+
queueArgs['x-dead-letter-exchange'] = effectiveExchange;
|
|
292
|
+
queueArgs['x-dead-letter-routing-key'] = `${effectiveKey}${types_1.RETRY_QUEUE_SUFFIX}`;
|
|
256
293
|
}
|
|
294
|
+
else if (retryConfig) {
|
|
295
|
+
// No retry (maxRetries = 0): go directly to DLQ
|
|
296
|
+
queueArgs['x-dead-letter-exchange'] = dlxName;
|
|
297
|
+
queueArgs['x-dead-letter-routing-key'] = `${effectiveKey}${types_1.DLQ_SUFFIX}`;
|
|
298
|
+
}
|
|
299
|
+
// Assert queue (idempotent - RabbitMQ won't recreate if exists with same params)
|
|
257
300
|
await ch.assertQueue(queueName, {
|
|
258
301
|
durable,
|
|
259
302
|
exclusive,
|
|
@@ -261,12 +304,21 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
261
304
|
arguments: Object.keys(queueArgs).length > 0 ? queueArgs : undefined,
|
|
262
305
|
});
|
|
263
306
|
console.log(`✓ Queue asserted: ${queueName}`);
|
|
264
|
-
//
|
|
265
|
-
if (
|
|
266
|
-
await
|
|
307
|
+
// Bind queue to exchange if exchangeName is provided (idempotent operation)
|
|
308
|
+
if (exchangeName) {
|
|
309
|
+
await ch.bindQueue(queueName, exchangeName, effectiveKey);
|
|
310
|
+
console.log(`✓ Queue bound to exchange: ${queueName} -> ${exchangeName} (${effectiveKey})`);
|
|
267
311
|
}
|
|
268
312
|
}
|
|
269
313
|
});
|
|
314
|
+
// Setup DLQ infrastructure for all queues if retry config is provided
|
|
315
|
+
if (retryConfig) {
|
|
316
|
+
for (const queueName of queues) {
|
|
317
|
+
// Use provided exchange/routingKey or defaults
|
|
318
|
+
const effectiveKey = routingKey || queueName;
|
|
319
|
+
await this.setupDLQInfrastructure(queueName, effectiveExchange, effectiveKey, retryConfig);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
270
322
|
console.log(`✓ Successfully asserted ${queues.length} queue(s)`);
|
|
271
323
|
}
|
|
272
324
|
catch (error) {
|
|
@@ -275,30 +327,135 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
275
327
|
}
|
|
276
328
|
}
|
|
277
329
|
/**
|
|
278
|
-
*
|
|
330
|
+
* Delete queues (Best Practice for cleanup/testing)
|
|
331
|
+
* Can be called with a single queue name or multiple queue names
|
|
332
|
+
* Also deletes associated retry and DLQ queues if they exist
|
|
333
|
+
*
|
|
334
|
+
* @param queueNames - Single queue name (string) or array of queue names
|
|
335
|
+
* @param options - Delete options (includeRetry, includeDLQ)
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* // Delete a single queue
|
|
339
|
+
* await consumer.deleteQueues('orders');
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* // Delete multiple queues
|
|
343
|
+
* await consumer.deleteQueues(['orders', 'payments', 'notifications']);
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* // Delete queue without retry/DLQ queues
|
|
347
|
+
* await consumer.deleteQueues('orders', {
|
|
348
|
+
* includeRetry: false,
|
|
349
|
+
* includeDLQ: false
|
|
350
|
+
* });
|
|
351
|
+
*/
|
|
352
|
+
async deleteQueues(queueNames, options = {}) {
|
|
353
|
+
// Auto-connect on first use (lazy loading)
|
|
354
|
+
await this.ensureConnection();
|
|
355
|
+
// Normalize to array
|
|
356
|
+
const queues = Array.isArray(queueNames) ? queueNames : [queueNames];
|
|
357
|
+
const { includeRetry = true, includeDLQ = true } = options;
|
|
358
|
+
try {
|
|
359
|
+
let deletedCount = 0;
|
|
360
|
+
// Delete all queues
|
|
361
|
+
await this.channel.addSetup(async (ch) => {
|
|
362
|
+
for (const queueName of queues) {
|
|
363
|
+
// Delete main queue
|
|
364
|
+
try {
|
|
365
|
+
await ch.deleteQueue(queueName, { ifEmpty: false });
|
|
366
|
+
console.log(`✓ Deleted queue: ${queueName}`);
|
|
367
|
+
deletedCount++;
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
// Queue doesn't exist or already deleted
|
|
371
|
+
if (error.code !== 404) {
|
|
372
|
+
console.warn(`Warning: Could not delete queue ${queueName}:`, error.message);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Delete retry queue if requested
|
|
376
|
+
if (includeRetry) {
|
|
377
|
+
try {
|
|
378
|
+
await ch.deleteQueue(`${queueName}${types_1.RETRY_QUEUE_SUFFIX}`, {
|
|
379
|
+
ifEmpty: false,
|
|
380
|
+
});
|
|
381
|
+
console.log(`✓ Deleted retry queue: ${queueName}${types_1.RETRY_QUEUE_SUFFIX}`);
|
|
382
|
+
deletedCount++;
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
// Retry queue doesn't exist, skip
|
|
386
|
+
if (error.code !== 404) {
|
|
387
|
+
console.warn(`Warning: Could not delete retry queue ${queueName}${types_1.RETRY_QUEUE_SUFFIX}:`, error.message);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Delete DLQ if requested
|
|
392
|
+
if (includeDLQ) {
|
|
393
|
+
try {
|
|
394
|
+
await ch.deleteQueue(`${queueName}${types_1.DLQ_SUFFIX}`, {
|
|
395
|
+
ifEmpty: false,
|
|
396
|
+
});
|
|
397
|
+
console.log(`✓ Deleted DLQ: ${queueName}${types_1.DLQ_SUFFIX}`);
|
|
398
|
+
deletedCount++;
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
// DLQ doesn't exist, skip
|
|
402
|
+
if (error.code !== 404) {
|
|
403
|
+
console.warn(`Warning: Could not delete DLQ ${queueName}${types_1.DLQ_SUFFIX}:`, error.message);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
console.log(`✓ Successfully deleted ${deletedCount} queue(s)`);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
console.error('Failed to delete queues:', error);
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Setup Dead Letter Queue (DLQ) infrastructure for a queue with retry mechanism
|
|
279
418
|
*/
|
|
280
|
-
async
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
419
|
+
async setupDLQInfrastructure(queueName, exchangeName, routingKey, retryConfig) {
|
|
420
|
+
await this.channel.addSetup(async (ch) => {
|
|
421
|
+
const dlxName = `${exchangeName}${types_1.DLX_SUFFIX}`;
|
|
422
|
+
const dlqName = `${queueName}${types_1.DLQ_SUFFIX}`;
|
|
423
|
+
const retryQueueName = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
|
|
424
|
+
// 1. Create Dead Letter Exchange (DLX) - only if exchangeName is not empty
|
|
425
|
+
if (exchangeName) {
|
|
426
|
+
await ch.assertExchange(dlxName, 'direct', { durable: true });
|
|
427
|
+
}
|
|
428
|
+
// 2. Create Dead Letter Queue (DLQ) - Final resting place for failed messages
|
|
429
|
+
// assertQueue is idempotent - won't recreate if exists with same params
|
|
430
|
+
await ch.assertQueue(dlqName, {
|
|
286
431
|
durable: true,
|
|
287
432
|
arguments: {
|
|
288
|
-
'x-
|
|
289
|
-
'x-dead-letter-routing-key': queueName,
|
|
433
|
+
'x-queue-mode': 'lazy', // Better for long-term storage, saves memory
|
|
290
434
|
},
|
|
291
435
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
436
|
+
// Bind DLQ to DLX if exchange is used
|
|
437
|
+
if (exchangeName) {
|
|
438
|
+
await ch.bindQueue(dlqName, dlxName, `${routingKey}${types_1.DLQ_SUFFIX}`);
|
|
439
|
+
}
|
|
440
|
+
console.log(`✓ DLQ asserted: ${dlqName}`);
|
|
441
|
+
// 3. Create Retry Queue (if retry is enabled)
|
|
442
|
+
if (retryConfig && retryConfig.maxRetries > 0) {
|
|
443
|
+
// assertQueue is idempotent - won't recreate if exists with same params
|
|
444
|
+
await ch.assertQueue(retryQueueName, {
|
|
445
|
+
durable: true,
|
|
446
|
+
arguments: {
|
|
447
|
+
// NO fixed TTL - we use per-message expiration for exponential backoff
|
|
448
|
+
'x-dead-letter-exchange': exchangeName || '', // Send back to main exchange after TTL
|
|
449
|
+
'x-dead-letter-routing-key': routingKey, // Use original routing key
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
console.log(`✓ Retry queue asserted: ${retryQueueName}`);
|
|
453
|
+
console.log(`DLQ infrastructure setup complete for queue: ${queueName} (max retries: ${retryConfig.maxRetries}, exponential backoff enabled)`);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
console.log(`DLQ infrastructure setup complete for queue: ${queueName} (no retries)`);
|
|
457
|
+
}
|
|
300
458
|
});
|
|
301
|
-
console.log(`✓ DLQ asserted: ${dlqName}`);
|
|
302
459
|
}
|
|
303
460
|
/**
|
|
304
461
|
* Bind a queue to an exchange with a routing key (Best Practice for Consumers)
|
|
@@ -327,6 +484,16 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
327
484
|
// Auto-connect on first use (lazy loading)
|
|
328
485
|
await this.ensureConnection();
|
|
329
486
|
const { exchangeType, durable = true } = options;
|
|
487
|
+
// Validate inputs
|
|
488
|
+
if (!queueName || queueName.trim() === '') {
|
|
489
|
+
throw new Error('Queue name cannot be empty');
|
|
490
|
+
}
|
|
491
|
+
if (!exchangeName || exchangeName.trim() === '') {
|
|
492
|
+
throw new Error('Exchange name cannot be empty');
|
|
493
|
+
}
|
|
494
|
+
if (!routingKey || routingKey.trim() === '') {
|
|
495
|
+
throw new Error('Routing key cannot be empty');
|
|
496
|
+
}
|
|
330
497
|
try {
|
|
331
498
|
await this.channel.addSetup(async (ch) => {
|
|
332
499
|
// If exchangeType is provided, assert the exchange first (idempotent)
|
|
@@ -343,6 +510,98 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
343
510
|
throw error;
|
|
344
511
|
}
|
|
345
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Setup a complete queue with exchange and bindings in ONE atomic operation
|
|
515
|
+
* This is the RECOMMENDED method to prevent race conditions
|
|
516
|
+
*
|
|
517
|
+
* All operations (exchange, queue, bindings) happen in a single setup callback,
|
|
518
|
+
* guaranteeing correct order of execution.
|
|
519
|
+
*
|
|
520
|
+
* @param config - Queue setup configuration
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* await consumer.setupQueue({
|
|
524
|
+
* queueName: 'orders-processor',
|
|
525
|
+
* exchangeName: 'orders',
|
|
526
|
+
* exchangeType: 'topic',
|
|
527
|
+
* routingKeys: ['order.created', 'order.updated'],
|
|
528
|
+
* retryConfig: { maxRetries: 3, retryDelayMs: 5000 }
|
|
529
|
+
* });
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* // Setup multiple queues
|
|
533
|
+
* await consumer.setupQueue({
|
|
534
|
+
* queueName: 'fiat.account.create',
|
|
535
|
+
* exchangeName: 'user-accounts',
|
|
536
|
+
* exchangeType: 'topic',
|
|
537
|
+
* routingKeys: ['account.fiat.create', 'account.create']
|
|
538
|
+
* });
|
|
539
|
+
*/
|
|
540
|
+
async setupQueue(config) {
|
|
541
|
+
// Auto-connect on first use (lazy loading)
|
|
542
|
+
await this.ensureConnection();
|
|
543
|
+
const { queueName, exchangeName, exchangeType, routingKeys, durable = true, exclusive = false, autoDelete = false, retryConfig, } = config;
|
|
544
|
+
// Normalize routing keys to array
|
|
545
|
+
const keys = Array.isArray(routingKeys) ? routingKeys : [routingKeys];
|
|
546
|
+
// Validate retry configuration if provided
|
|
547
|
+
if (retryConfig) {
|
|
548
|
+
(0, validation_1.validateRetryConfig)(retryConfig);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
// SINGLE addSetup call - all operations happen in sequence, guaranteed order
|
|
552
|
+
await this.channel.addSetup(async (ch) => {
|
|
553
|
+
// 1. Assert exchange
|
|
554
|
+
await ch.assertExchange(exchangeName, exchangeType, { durable });
|
|
555
|
+
console.log(`✓ Exchange asserted: ${exchangeName} (${exchangeType})`);
|
|
556
|
+
// 2. Build queue arguments
|
|
557
|
+
const queueArgs = {};
|
|
558
|
+
if (retryConfig && retryConfig.maxRetries > 0) {
|
|
559
|
+
queueArgs['x-dead-letter-exchange'] = '';
|
|
560
|
+
queueArgs['x-dead-letter-routing-key'] = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
|
|
561
|
+
}
|
|
562
|
+
// 3. Assert queue
|
|
563
|
+
await ch.assertQueue(queueName, {
|
|
564
|
+
durable,
|
|
565
|
+
exclusive,
|
|
566
|
+
autoDelete,
|
|
567
|
+
arguments: Object.keys(queueArgs).length > 0 ? queueArgs : undefined,
|
|
568
|
+
});
|
|
569
|
+
console.log(`✓ Queue asserted: ${queueName}`);
|
|
570
|
+
// 4. Setup retry and DLQ queues if retry config is provided
|
|
571
|
+
if (retryConfig) {
|
|
572
|
+
const retryQueueName = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
|
|
573
|
+
const dlqName = `${queueName}${types_1.DLQ_SUFFIX}`;
|
|
574
|
+
if (retryConfig.maxRetries > 0) {
|
|
575
|
+
await ch.assertQueue(retryQueueName, {
|
|
576
|
+
durable: true,
|
|
577
|
+
arguments: {
|
|
578
|
+
'x-dead-letter-exchange': '',
|
|
579
|
+
'x-dead-letter-routing-key': queueName,
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
console.log(`✓ Retry queue asserted: ${retryQueueName}`);
|
|
583
|
+
}
|
|
584
|
+
await ch.assertQueue(dlqName, {
|
|
585
|
+
durable: true,
|
|
586
|
+
arguments: {
|
|
587
|
+
'x-queue-mode': 'lazy',
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
console.log(`✓ DLQ asserted: ${dlqName}`);
|
|
591
|
+
}
|
|
592
|
+
// 5. Bind queue to exchange with all routing keys
|
|
593
|
+
for (const routingKey of keys) {
|
|
594
|
+
await ch.bindQueue(queueName, exchangeName, routingKey);
|
|
595
|
+
console.log(`✓ Queue bound: ${queueName} -> ${exchangeName} (${routingKey})`);
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
console.log(`✓ Queue setup complete: ${queueName}`);
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
console.error(`Failed to setup queue ${queueName}:`, error);
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
346
605
|
/**
|
|
347
606
|
* Extract error code from an error object
|
|
348
607
|
* Handles various error formats (Node.js errors, custom errors, HTTP errors)
|
|
@@ -412,16 +671,50 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
412
671
|
}
|
|
413
672
|
/**
|
|
414
673
|
* Consume messages from a queue with automatic retry and DLQ handling
|
|
674
|
+
*
|
|
675
|
+
* Note: Queue should be set up separately before consuming:
|
|
676
|
+
* - Use assertQueues() to create queue with exchange binding
|
|
677
|
+
* - Use setupQueue() for complete setup with multiple routing keys
|
|
678
|
+
* - Use bindQueue() to add additional routing key bindings
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* // Step 1: Setup queue with exchange and routing keys
|
|
682
|
+
* await consumer.setupQueue({
|
|
683
|
+
* queueName: 'orders',
|
|
684
|
+
* exchangeName: 'orders',
|
|
685
|
+
* exchangeType: 'topic',
|
|
686
|
+
* routingKeys: ['order.created', 'order.updated'],
|
|
687
|
+
* retryConfig: { maxRetries: 5 }
|
|
688
|
+
* });
|
|
689
|
+
*
|
|
690
|
+
* // Step 2: Consume (queue already set up)
|
|
691
|
+
* await consumer.consumeQueue({
|
|
692
|
+
* queueName: 'orders',
|
|
693
|
+
* onMessage: async (message) => {
|
|
694
|
+
* await processOrder(message);
|
|
695
|
+
* }
|
|
696
|
+
* });
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* // Direct queue consumption (no exchange)
|
|
700
|
+
* await consumer.assertQueues('simple-queue');
|
|
701
|
+
* await consumer.consumeQueue({
|
|
702
|
+
* queueName: 'simple-queue',
|
|
703
|
+
* onMessage: async (message) => { ... }
|
|
704
|
+
* });
|
|
415
705
|
*/
|
|
416
706
|
async consumeQueue(config) {
|
|
417
707
|
// Auto-connect on first use (lazy loading)
|
|
418
708
|
await this.ensureConnection();
|
|
419
709
|
try {
|
|
420
|
-
|
|
421
|
-
const {
|
|
422
|
-
|
|
710
|
+
const { queueName, onMessage, options = {} } = config;
|
|
711
|
+
const { durable = true, prefetch = types_1.DEFAULT_PREFETCH_COUNT, noAck = false, exclusive = false, autoDelete = false, } = options;
|
|
712
|
+
// Validate inputs
|
|
713
|
+
if (!queueName || queueName.trim() === '') {
|
|
714
|
+
throw new Error('Queue name cannot be empty');
|
|
715
|
+
}
|
|
423
716
|
await this.channel.addSetup(async (ch) => {
|
|
424
|
-
// Set prefetch count
|
|
717
|
+
// Set prefetch count (critical for flow control)
|
|
425
718
|
await ch.prefetch(prefetch);
|
|
426
719
|
// Check if queue exists (passive check - doesn't modify queue properties)
|
|
427
720
|
try {
|
|
@@ -429,16 +722,9 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
429
722
|
}
|
|
430
723
|
catch (error) {
|
|
431
724
|
// Queue doesn't exist, create it without DLQ config (for simple cases)
|
|
432
|
-
//
|
|
725
|
+
// For production, use assertQueues() or setupQueue() first
|
|
433
726
|
await ch.assertQueue(queueName, { durable, exclusive, autoDelete });
|
|
434
|
-
|
|
435
|
-
// If exchange is specified, assert exchange and bind queue
|
|
436
|
-
if (exchangeName && routingKey) {
|
|
437
|
-
await ch.assertExchange(exchangeName, exchangeType, { durable });
|
|
438
|
-
// Bind queue to exchange (idempotent operation)
|
|
439
|
-
await ch.bindQueue(queueName, exchangeName, routingKey);
|
|
440
|
-
// Also bind the retry routing key
|
|
441
|
-
await ch.bindQueue(queueName, exchangeName, `${routingKey}.retry`);
|
|
727
|
+
console.warn(`Queue ${queueName} was auto-created. Consider using assertQueues() or setupQueue() for proper setup.`);
|
|
442
728
|
}
|
|
443
729
|
// Start consuming
|
|
444
730
|
await ch.consume(queueName, async (msg) => {
|
|
@@ -455,10 +741,12 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
455
741
|
fields: msg.fields,
|
|
456
742
|
properties: msg.properties,
|
|
457
743
|
content: msg.content,
|
|
458
|
-
originalMessage: msg
|
|
744
|
+
originalMessage: msg,
|
|
459
745
|
};
|
|
460
746
|
let timestamp = new Date().toISOString();
|
|
461
|
-
const retryInfo = maxRetries > 0
|
|
747
|
+
const retryInfo = maxRetries > 0
|
|
748
|
+
? ` (attempt ${currentRetryCount + 1}/${maxRetries + 1})`
|
|
749
|
+
: '';
|
|
462
750
|
console.log(`[${timestamp}] Processing from ${queueName}${retryInfo}:`, content);
|
|
463
751
|
// Try to process the message
|
|
464
752
|
await onMessage(content, messageInfo);
|
|
@@ -491,7 +779,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
491
779
|
const timestamp = new Date().toISOString();
|
|
492
780
|
console.log(`[${timestamp}] 🔄 Retry ${nextRetryCount}/${maxRetries} in ${actualDelay}ms`);
|
|
493
781
|
// Publish to retry queue with updated retry count
|
|
494
|
-
const retryQueueName = `${queueName}.
|
|
782
|
+
const retryQueueName = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
|
|
495
783
|
try {
|
|
496
784
|
await ch.sendToQueue(retryQueueName, msg.content, {
|
|
497
785
|
...msg.properties,
|
|
@@ -501,7 +789,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
501
789
|
'x-retry-count': nextRetryCount,
|
|
502
790
|
'x-retry-timestamp': new Date().toISOString(),
|
|
503
791
|
'x-base-retry-delay': baseRetryDelay, // Store the BASE delay (don't compound!)
|
|
504
|
-
}
|
|
792
|
+
},
|
|
505
793
|
});
|
|
506
794
|
// Acknowledge original message after successful retry queue publish
|
|
507
795
|
ch.ack(msg);
|
|
@@ -532,7 +820,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
532
820
|
const timestamp = new Date().toISOString();
|
|
533
821
|
console.log(`[${timestamp}] 💀 Max retries exceeded, moving to DLQ`);
|
|
534
822
|
// Manually send to DLQ (can't rely on nack because main queue's DLX routes to retry)
|
|
535
|
-
const dlqName = `${queueName}.
|
|
823
|
+
const dlqName = `${queueName}${types_1.DLQ_SUFFIX}`;
|
|
536
824
|
// Extract error code for categorization
|
|
537
825
|
const errorCode = this.extractErrorCode(error);
|
|
538
826
|
try {
|
|
@@ -544,8 +832,10 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
544
832
|
'x-death-timestamp': new Date().toISOString(),
|
|
545
833
|
'x-last-error': errorMessage,
|
|
546
834
|
'x-last-error-code': errorCode,
|
|
547
|
-
'x-is-retryable': this.isRetryableError(errorCode)
|
|
548
|
-
|
|
835
|
+
'x-is-retryable': this.isRetryableError(errorCode)
|
|
836
|
+
? 'true'
|
|
837
|
+
: 'false',
|
|
838
|
+
},
|
|
549
839
|
});
|
|
550
840
|
// Acknowledge original message after successful DLQ send
|
|
551
841
|
ch.ack(msg);
|
|
@@ -589,17 +879,34 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
589
879
|
}
|
|
590
880
|
/**
|
|
591
881
|
* Consume from multiple queues with the same connection
|
|
882
|
+
* Note: Queues should be set up separately before consuming
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* // Setup queues first
|
|
886
|
+
* await consumer.setupQueue({ queueName: 'orders', ... });
|
|
887
|
+
* await consumer.setupQueue({ queueName: 'payments', ... });
|
|
888
|
+
*
|
|
889
|
+
* // Then consume from all
|
|
890
|
+
* await consumer.consumeMultipleQueues({
|
|
891
|
+
* queues: [
|
|
892
|
+
* {
|
|
893
|
+
* queueName: 'orders',
|
|
894
|
+
* onMessage: async (msg) => { ... }
|
|
895
|
+
* },
|
|
896
|
+
* {
|
|
897
|
+
* queueName: 'payments',
|
|
898
|
+
* onMessage: async (msg) => { ... }
|
|
899
|
+
* }
|
|
900
|
+
* ]
|
|
901
|
+
* });
|
|
592
902
|
*/
|
|
593
903
|
async consumeMultipleQueues(config) {
|
|
594
904
|
const { queues, options = {} } = config;
|
|
595
905
|
for (const queueConfig of queues) {
|
|
596
906
|
await this.consumeQueue({
|
|
597
907
|
queueName: queueConfig.queueName,
|
|
598
|
-
exchangeName: queueConfig.exchangeName,
|
|
599
|
-
exchangeType: queueConfig.exchangeType,
|
|
600
|
-
routingKey: queueConfig.routingKey,
|
|
601
908
|
onMessage: queueConfig.onMessage,
|
|
602
|
-
options
|
|
909
|
+
options,
|
|
603
910
|
});
|
|
604
911
|
}
|
|
605
912
|
}
|
|
@@ -631,7 +938,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
631
938
|
fields: msg.fields,
|
|
632
939
|
properties: msg.properties,
|
|
633
940
|
content: msg.content,
|
|
634
|
-
originalMessage: msg
|
|
941
|
+
originalMessage: msg,
|
|
635
942
|
};
|
|
636
943
|
await onMessage(content, routingKey, messageInfo);
|
|
637
944
|
ch.ack(msg);
|
|
@@ -659,7 +966,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
659
966
|
getConsumerStats() {
|
|
660
967
|
return {
|
|
661
968
|
rabbitUrl: this.rabbitUrl,
|
|
662
|
-
activeQueues: [...this.activeQueues]
|
|
969
|
+
activeQueues: [...this.activeQueues],
|
|
663
970
|
};
|
|
664
971
|
}
|
|
665
972
|
/**
|
|
@@ -671,7 +978,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
671
978
|
if (this.channel) {
|
|
672
979
|
// Give time for in-flight messages to complete processing
|
|
673
980
|
// In production, you might want to implement a more sophisticated drain mechanism
|
|
674
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
981
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
675
982
|
await this.channel.close();
|
|
676
983
|
this.channel = undefined;
|
|
677
984
|
}
|