rabbitmq-with-retry-and-dlq 1.0.24 → 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 +235 -139
- package/dist/consumerMq.d.ts +89 -3
- package/dist/consumerMq.d.ts.map +1 -1
- package/dist/consumerMq.js +274 -59
- 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.d.ts
CHANGED
|
@@ -76,8 +76,11 @@ declare class RabbitMQConsumer extends EventEmitter {
|
|
|
76
76
|
* await consumer.assertQueues(['orders-worker', 'payments-worker']);
|
|
77
77
|
*
|
|
78
78
|
* @example
|
|
79
|
-
* // Assert queue with
|
|
79
|
+
* // Assert queue with exchange binding and retry config
|
|
80
80
|
* await consumer.assertQueues('orders-worker', {
|
|
81
|
+
* exchangeName: 'orders',
|
|
82
|
+
* exchangeType: 'direct',
|
|
83
|
+
* routingKey: 'order.created',
|
|
81
84
|
* retryConfig: { maxRetries: 3, retryDelayMs: 5000 }
|
|
82
85
|
* });
|
|
83
86
|
*/
|
|
@@ -85,12 +88,44 @@ declare class RabbitMQConsumer extends EventEmitter {
|
|
|
85
88
|
durable?: boolean;
|
|
86
89
|
exclusive?: boolean;
|
|
87
90
|
autoDelete?: boolean;
|
|
91
|
+
exchangeName?: string;
|
|
92
|
+
exchangeType?: 'direct' | 'topic' | 'fanout' | 'headers';
|
|
93
|
+
routingKey?: string;
|
|
88
94
|
retryConfig?: RetryConfig;
|
|
89
95
|
}): Promise<void>;
|
|
90
96
|
/**
|
|
91
|
-
*
|
|
97
|
+
* Delete queues (Best Practice for cleanup/testing)
|
|
98
|
+
* Can be called with a single queue name or multiple queue names
|
|
99
|
+
* Also deletes associated retry and DLQ queues if they exist
|
|
100
|
+
*
|
|
101
|
+
* @param queueNames - Single queue name (string) or array of queue names
|
|
102
|
+
* @param options - Delete options (includeRetry, includeDLQ)
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // Delete a single queue
|
|
106
|
+
* await consumer.deleteQueues('orders');
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // Delete multiple queues
|
|
110
|
+
* await consumer.deleteQueues(['orders', 'payments', 'notifications']);
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // Delete queue without retry/DLQ queues
|
|
114
|
+
* await consumer.deleteQueues('orders', {
|
|
115
|
+
* includeRetry: false,
|
|
116
|
+
* includeDLQ: false
|
|
117
|
+
* });
|
|
118
|
+
*/
|
|
119
|
+
deleteQueues(queueNames: string | string[], options?: {
|
|
120
|
+
/** Whether to delete associated retry queue (default: true) */
|
|
121
|
+
includeRetry?: boolean;
|
|
122
|
+
/** Whether to delete associated DLQ (default: true) */
|
|
123
|
+
includeDLQ?: boolean;
|
|
124
|
+
}): Promise<void>;
|
|
125
|
+
/**
|
|
126
|
+
* Setup Dead Letter Queue (DLQ) infrastructure for a queue with retry mechanism
|
|
92
127
|
*/
|
|
93
|
-
private
|
|
128
|
+
private setupDLQInfrastructure;
|
|
94
129
|
/**
|
|
95
130
|
* Bind a queue to an exchange with a routing key (Best Practice for Consumers)
|
|
96
131
|
* Consumers decide which exchanges/routing keys they want to subscribe to
|
|
@@ -176,10 +211,61 @@ declare class RabbitMQConsumer extends EventEmitter {
|
|
|
176
211
|
private calculateRetryDelay;
|
|
177
212
|
/**
|
|
178
213
|
* Consume messages from a queue with automatic retry and DLQ handling
|
|
214
|
+
*
|
|
215
|
+
* Note: Queue should be set up separately before consuming:
|
|
216
|
+
* - Use assertQueues() to create queue with exchange binding
|
|
217
|
+
* - Use setupQueue() for complete setup with multiple routing keys
|
|
218
|
+
* - Use bindQueue() to add additional routing key bindings
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* // Step 1: Setup queue with exchange and routing keys
|
|
222
|
+
* await consumer.setupQueue({
|
|
223
|
+
* queueName: 'orders',
|
|
224
|
+
* exchangeName: 'orders',
|
|
225
|
+
* exchangeType: 'topic',
|
|
226
|
+
* routingKeys: ['order.created', 'order.updated'],
|
|
227
|
+
* retryConfig: { maxRetries: 5 }
|
|
228
|
+
* });
|
|
229
|
+
*
|
|
230
|
+
* // Step 2: Consume (queue already set up)
|
|
231
|
+
* await consumer.consumeQueue({
|
|
232
|
+
* queueName: 'orders',
|
|
233
|
+
* onMessage: async (message) => {
|
|
234
|
+
* await processOrder(message);
|
|
235
|
+
* }
|
|
236
|
+
* });
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* // Direct queue consumption (no exchange)
|
|
240
|
+
* await consumer.assertQueues('simple-queue');
|
|
241
|
+
* await consumer.consumeQueue({
|
|
242
|
+
* queueName: 'simple-queue',
|
|
243
|
+
* onMessage: async (message) => { ... }
|
|
244
|
+
* });
|
|
179
245
|
*/
|
|
180
246
|
consumeQueue<T = any>(config: ConsumeConfig<T>): Promise<void>;
|
|
181
247
|
/**
|
|
182
248
|
* Consume from multiple queues with the same connection
|
|
249
|
+
* Note: Queues should be set up separately before consuming
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* // Setup queues first
|
|
253
|
+
* await consumer.setupQueue({ queueName: 'orders', ... });
|
|
254
|
+
* await consumer.setupQueue({ queueName: 'payments', ... });
|
|
255
|
+
*
|
|
256
|
+
* // Then consume from all
|
|
257
|
+
* await consumer.consumeMultipleQueues({
|
|
258
|
+
* queues: [
|
|
259
|
+
* {
|
|
260
|
+
* queueName: 'orders',
|
|
261
|
+
* onMessage: async (msg) => { ... }
|
|
262
|
+
* },
|
|
263
|
+
* {
|
|
264
|
+
* queueName: 'payments',
|
|
265
|
+
* onMessage: async (msg) => { ... }
|
|
266
|
+
* }
|
|
267
|
+
* ]
|
|
268
|
+
* });
|
|
183
269
|
*/
|
|
184
270
|
consumeMultipleQueues<T = any>(config: MultiQueueConsumeConfig<T>): Promise<void>;
|
|
185
271
|
/**
|
package/dist/consumerMq.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consumerMq.d.ts","sourceRoot":"","sources":["../consumerMq.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,
|
|
1
|
+
{"version":3,"file":"consumerMq.d.ts","sourceRoot":"","sources":["../consumerMq.ts"],"names":[],"mappings":"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"}
|
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}`;
|
|
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}`;
|
|
256
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
|
+
* });
|
|
279
351
|
*/
|
|
280
|
-
async
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
418
|
+
*/
|
|
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)
|
|
@@ -390,7 +557,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
390
557
|
const queueArgs = {};
|
|
391
558
|
if (retryConfig && retryConfig.maxRetries > 0) {
|
|
392
559
|
queueArgs['x-dead-letter-exchange'] = '';
|
|
393
|
-
queueArgs['x-dead-letter-routing-key'] = `${queueName}.
|
|
560
|
+
queueArgs['x-dead-letter-routing-key'] = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
|
|
394
561
|
}
|
|
395
562
|
// 3. Assert queue
|
|
396
563
|
await ch.assertQueue(queueName, {
|
|
@@ -402,8 +569,8 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
402
569
|
console.log(`✓ Queue asserted: ${queueName}`);
|
|
403
570
|
// 4. Setup retry and DLQ queues if retry config is provided
|
|
404
571
|
if (retryConfig) {
|
|
405
|
-
const retryQueueName = `${queueName}.
|
|
406
|
-
const dlqName = `${queueName}.
|
|
572
|
+
const retryQueueName = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
|
|
573
|
+
const dlqName = `${queueName}${types_1.DLQ_SUFFIX}`;
|
|
407
574
|
if (retryConfig.maxRetries > 0) {
|
|
408
575
|
await ch.assertQueue(retryQueueName, {
|
|
409
576
|
durable: true,
|
|
@@ -504,16 +671,50 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
504
671
|
}
|
|
505
672
|
/**
|
|
506
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
|
+
* });
|
|
507
705
|
*/
|
|
508
706
|
async consumeQueue(config) {
|
|
509
707
|
// Auto-connect on first use (lazy loading)
|
|
510
708
|
await this.ensureConnection();
|
|
511
709
|
try {
|
|
512
|
-
|
|
513
|
-
const {
|
|
514
|
-
|
|
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
|
+
}
|
|
515
716
|
await this.channel.addSetup(async (ch) => {
|
|
516
|
-
// Set prefetch count
|
|
717
|
+
// Set prefetch count (critical for flow control)
|
|
517
718
|
await ch.prefetch(prefetch);
|
|
518
719
|
// Check if queue exists (passive check - doesn't modify queue properties)
|
|
519
720
|
try {
|
|
@@ -521,16 +722,9 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
521
722
|
}
|
|
522
723
|
catch (error) {
|
|
523
724
|
// Queue doesn't exist, create it without DLQ config (for simple cases)
|
|
524
|
-
//
|
|
725
|
+
// For production, use assertQueues() or setupQueue() first
|
|
525
726
|
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`);
|
|
727
|
+
console.warn(`Queue ${queueName} was auto-created. Consider using assertQueues() or setupQueue() for proper setup.`);
|
|
534
728
|
}
|
|
535
729
|
// Start consuming
|
|
536
730
|
await ch.consume(queueName, async (msg) => {
|
|
@@ -547,10 +741,12 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
547
741
|
fields: msg.fields,
|
|
548
742
|
properties: msg.properties,
|
|
549
743
|
content: msg.content,
|
|
550
|
-
originalMessage: msg
|
|
744
|
+
originalMessage: msg,
|
|
551
745
|
};
|
|
552
746
|
let timestamp = new Date().toISOString();
|
|
553
|
-
const retryInfo = maxRetries > 0
|
|
747
|
+
const retryInfo = maxRetries > 0
|
|
748
|
+
? ` (attempt ${currentRetryCount + 1}/${maxRetries + 1})`
|
|
749
|
+
: '';
|
|
554
750
|
console.log(`[${timestamp}] Processing from ${queueName}${retryInfo}:`, content);
|
|
555
751
|
// Try to process the message
|
|
556
752
|
await onMessage(content, messageInfo);
|
|
@@ -583,7 +779,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
583
779
|
const timestamp = new Date().toISOString();
|
|
584
780
|
console.log(`[${timestamp}] 🔄 Retry ${nextRetryCount}/${maxRetries} in ${actualDelay}ms`);
|
|
585
781
|
// Publish to retry queue with updated retry count
|
|
586
|
-
const retryQueueName = `${queueName}.
|
|
782
|
+
const retryQueueName = `${queueName}${types_1.RETRY_QUEUE_SUFFIX}`;
|
|
587
783
|
try {
|
|
588
784
|
await ch.sendToQueue(retryQueueName, msg.content, {
|
|
589
785
|
...msg.properties,
|
|
@@ -593,7 +789,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
593
789
|
'x-retry-count': nextRetryCount,
|
|
594
790
|
'x-retry-timestamp': new Date().toISOString(),
|
|
595
791
|
'x-base-retry-delay': baseRetryDelay, // Store the BASE delay (don't compound!)
|
|
596
|
-
}
|
|
792
|
+
},
|
|
597
793
|
});
|
|
598
794
|
// Acknowledge original message after successful retry queue publish
|
|
599
795
|
ch.ack(msg);
|
|
@@ -624,7 +820,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
624
820
|
const timestamp = new Date().toISOString();
|
|
625
821
|
console.log(`[${timestamp}] 💀 Max retries exceeded, moving to DLQ`);
|
|
626
822
|
// Manually send to DLQ (can't rely on nack because main queue's DLX routes to retry)
|
|
627
|
-
const dlqName = `${queueName}.
|
|
823
|
+
const dlqName = `${queueName}${types_1.DLQ_SUFFIX}`;
|
|
628
824
|
// Extract error code for categorization
|
|
629
825
|
const errorCode = this.extractErrorCode(error);
|
|
630
826
|
try {
|
|
@@ -636,8 +832,10 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
636
832
|
'x-death-timestamp': new Date().toISOString(),
|
|
637
833
|
'x-last-error': errorMessage,
|
|
638
834
|
'x-last-error-code': errorCode,
|
|
639
|
-
'x-is-retryable': this.isRetryableError(errorCode)
|
|
640
|
-
|
|
835
|
+
'x-is-retryable': this.isRetryableError(errorCode)
|
|
836
|
+
? 'true'
|
|
837
|
+
: 'false',
|
|
838
|
+
},
|
|
641
839
|
});
|
|
642
840
|
// Acknowledge original message after successful DLQ send
|
|
643
841
|
ch.ack(msg);
|
|
@@ -681,17 +879,34 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
681
879
|
}
|
|
682
880
|
/**
|
|
683
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
|
+
* });
|
|
684
902
|
*/
|
|
685
903
|
async consumeMultipleQueues(config) {
|
|
686
904
|
const { queues, options = {} } = config;
|
|
687
905
|
for (const queueConfig of queues) {
|
|
688
906
|
await this.consumeQueue({
|
|
689
907
|
queueName: queueConfig.queueName,
|
|
690
|
-
exchangeName: queueConfig.exchangeName,
|
|
691
|
-
exchangeType: queueConfig.exchangeType,
|
|
692
|
-
routingKey: queueConfig.routingKey,
|
|
693
908
|
onMessage: queueConfig.onMessage,
|
|
694
|
-
options
|
|
909
|
+
options,
|
|
695
910
|
});
|
|
696
911
|
}
|
|
697
912
|
}
|
|
@@ -723,7 +938,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
723
938
|
fields: msg.fields,
|
|
724
939
|
properties: msg.properties,
|
|
725
940
|
content: msg.content,
|
|
726
|
-
originalMessage: msg
|
|
941
|
+
originalMessage: msg,
|
|
727
942
|
};
|
|
728
943
|
await onMessage(content, routingKey, messageInfo);
|
|
729
944
|
ch.ack(msg);
|
|
@@ -751,7 +966,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
751
966
|
getConsumerStats() {
|
|
752
967
|
return {
|
|
753
968
|
rabbitUrl: this.rabbitUrl,
|
|
754
|
-
activeQueues: [...this.activeQueues]
|
|
969
|
+
activeQueues: [...this.activeQueues],
|
|
755
970
|
};
|
|
756
971
|
}
|
|
757
972
|
/**
|
|
@@ -763,7 +978,7 @@ class RabbitMQConsumer extends events_1.EventEmitter {
|
|
|
763
978
|
if (this.channel) {
|
|
764
979
|
// Give time for in-flight messages to complete processing
|
|
765
980
|
// In production, you might want to implement a more sophisticated drain mechanism
|
|
766
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
981
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
767
982
|
await this.channel.close();
|
|
768
983
|
this.channel = undefined;
|
|
769
984
|
}
|