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.
- package/README.md +235 -139
- package/dist/consumerMq.d.ts +90 -3
- package/dist/consumerMq.d.ts.map +1 -1
- package/dist/consumerMq.js +319 -71
- package/dist/consumerMq.js.map +1 -1
- package/dist/publisherMq.d.ts +4 -71
- package/dist/publisherMq.d.ts.map +1 -1
- package/dist/publisherMq.js +72 -251
- package/dist/publisherMq.js.map +1 -1
- package/dist/types.d.ts +12 -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/test-connection.d.ts +0 -2
- package/dist/test-connection.d.ts.map +0 -1
- package/dist/test-connection.js +0 -38
- package/dist/test-connection.js.map +0 -1
- package/dist/test-wrong-url.d.ts +0 -2
- package/dist/test-wrong-url.d.ts.map +0 -1
- package/dist/test-wrong-url.js +0 -34
- package/dist/test-wrong-url.js.map +0 -1
- package/dist/test.d.ts +0 -19
- package/dist/test.d.ts.map +0 -1
- package/dist/test.js +0 -446
- package/dist/test.js.map +0 -1
package/dist/consumerMq.js
CHANGED
|
@@ -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 =
|
|
71
|
-
|
|
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)
|
|
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
|
|
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
|
-
//
|
|
254
|
-
queueArgs['x-dead-letter-exchange'] =
|
|
255
|
-
queueArgs['x-dead-letter-routing-key'] = `${
|
|
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
|
-
//
|
|
265
|
-
if (
|
|
266
|
-
await
|
|
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
|
-
*
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
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-
|
|
289
|
-
'x-dead-letter-routing-key': queueName,
|
|
437
|
+
'x-queue-mode': 'lazy', // Better for long-term storage, saves memory
|
|
290
438
|
},
|
|
291
439
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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}.
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
|
|
513
|
-
const {
|
|
514
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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']
|
|
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
|
|
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']
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
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,
|
|
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}.
|
|
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(),
|
|
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,
|
|
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}.
|
|
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)
|
|
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
|
}
|