@onlineapps/mq-client-core 1.0.31 → 1.0.33
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/package.json +1 -1
- package/src/transports/rabbitmqClient.js +160 -34
package/package.json
CHANGED
|
@@ -32,8 +32,9 @@ class RabbitMQClient extends EventEmitter {
|
|
|
32
32
|
);
|
|
33
33
|
|
|
34
34
|
this._connection = null;
|
|
35
|
-
this._channel = null;
|
|
36
|
-
this._queueChannel = null;
|
|
35
|
+
this._channel = null; // ConfirmChannel for publish operations
|
|
36
|
+
this._queueChannel = null; // Regular channel for queue operations (assertQueue, checkQueue)
|
|
37
|
+
this._consumerChannel = null; // Dedicated channel for consume operations
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -93,11 +94,20 @@ class RabbitMQClient extends EventEmitter {
|
|
|
93
94
|
|
|
94
95
|
// Use ConfirmChannel to enable publisher confirms for publish operations
|
|
95
96
|
this._channel = await this._connection.createConfirmChannel();
|
|
96
|
-
|
|
97
|
+
// Enable publisher confirms explicitly
|
|
98
|
+
this._channel.on('error', (err) => {
|
|
99
|
+
console.error('[RabbitMQClient] [mq-client-core] Publisher channel error:', err.message);
|
|
100
|
+
this.emit('error', err);
|
|
101
|
+
});
|
|
97
102
|
this._channel.on('close', () => {
|
|
103
|
+
console.warn('[RabbitMQClient] [mq-client-core] Publisher channel closed');
|
|
98
104
|
// Emit a channel close error
|
|
99
105
|
this.emit('error', new Error('RabbitMQ channel closed unexpectedly'));
|
|
100
106
|
});
|
|
107
|
+
// Set up publisher confirms callback
|
|
108
|
+
this._channel.on('drain', () => {
|
|
109
|
+
console.log('[RabbitMQClient] [mq-client-core] Publisher channel drained');
|
|
110
|
+
});
|
|
101
111
|
|
|
102
112
|
// Create a separate regular channel for queue operations (assertQueue, checkQueue)
|
|
103
113
|
// This avoids RPC reply queue issues with ConfirmChannel.assertQueue()
|
|
@@ -109,6 +119,18 @@ class RabbitMQClient extends EventEmitter {
|
|
|
109
119
|
this._queueChannel.on('close', () => {
|
|
110
120
|
console.warn('[RabbitMQClient] Queue channel closed');
|
|
111
121
|
});
|
|
122
|
+
|
|
123
|
+
// Create a dedicated channel for consume operations
|
|
124
|
+
// This prevents channel conflicts between publish (ConfirmChannel) and consume operations
|
|
125
|
+
this._consumerChannel = await this._connection.createChannel();
|
|
126
|
+
this._consumerChannel.on('error', (err) => {
|
|
127
|
+
console.warn('[RabbitMQClient] Consumer channel error:', err.message);
|
|
128
|
+
this.emit('error', err);
|
|
129
|
+
});
|
|
130
|
+
this._consumerChannel.on('close', () => {
|
|
131
|
+
console.warn('[RabbitMQClient] Consumer channel closed');
|
|
132
|
+
this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
|
|
133
|
+
});
|
|
112
134
|
} catch (err) {
|
|
113
135
|
// Cleanup partially created resources
|
|
114
136
|
if (this._connection) {
|
|
@@ -128,6 +150,14 @@ class RabbitMQClient extends EventEmitter {
|
|
|
128
150
|
* @returns {Promise<void>}
|
|
129
151
|
*/
|
|
130
152
|
async disconnect() {
|
|
153
|
+
try {
|
|
154
|
+
if (this._consumerChannel) {
|
|
155
|
+
await this._consumerChannel.close();
|
|
156
|
+
this._consumerChannel = null;
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.warn('[RabbitMQClient] Error closing consumer channel:', err.message);
|
|
160
|
+
}
|
|
131
161
|
try {
|
|
132
162
|
if (this._queueChannel) {
|
|
133
163
|
await this._queueChannel.close();
|
|
@@ -166,8 +196,13 @@ class RabbitMQClient extends EventEmitter {
|
|
|
166
196
|
async publish(queue, buffer, options = {}) {
|
|
167
197
|
// Check channel state before publish
|
|
168
198
|
if (!this._channel || this._channel.closed) {
|
|
169
|
-
|
|
199
|
+
const errorMsg = `Cannot publish to queue "${queue}": channel is not initialized or closed (channel: ${!!this._channel}, closed: ${this._channel?.closed})`;
|
|
200
|
+
console.error('[RabbitMQClient] [mq-client-core] [PUBLISH]', errorMsg);
|
|
201
|
+
throw new Error(errorMsg);
|
|
170
202
|
}
|
|
203
|
+
|
|
204
|
+
// Log publish attempt for debugging
|
|
205
|
+
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Attempting to publish to queue "${queue}" (channel open: ${!this._channel.closed})`);
|
|
171
206
|
|
|
172
207
|
const exchange = this._config.exchange || '';
|
|
173
208
|
const routingKey = options.routingKey || queue;
|
|
@@ -233,23 +268,90 @@ class RabbitMQClient extends EventEmitter {
|
|
|
233
268
|
// Publish to queue using ConfirmChannel (for publisher confirms)
|
|
234
269
|
// Check channel state again before sendToQueue
|
|
235
270
|
if (this._channel.closed) {
|
|
236
|
-
throw new Error(
|
|
271
|
+
throw new Error(`Cannot publish to queue "${queue}": channel closed during operation (after queue check)`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Sending message to queue "${queue}" (size: ${buffer.length} bytes)`);
|
|
275
|
+
|
|
276
|
+
// Use callback-based confirmation for more reliable error handling
|
|
277
|
+
const confirmPromise = new Promise((resolve, reject) => {
|
|
278
|
+
// Set up timeout
|
|
279
|
+
const timeout = setTimeout(() => {
|
|
280
|
+
reject(new Error(`Publisher confirm timeout after 5 seconds for queue "${queue}"`));
|
|
281
|
+
}, 5000);
|
|
282
|
+
|
|
283
|
+
// Send message with callback
|
|
284
|
+
const sent = this._channel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
|
|
285
|
+
clearTimeout(timeout);
|
|
286
|
+
if (err) {
|
|
287
|
+
console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Send callback error for queue "${queue}":`, err.message);
|
|
288
|
+
reject(err);
|
|
289
|
+
} else {
|
|
290
|
+
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] ✓ Message confirmed for queue "${queue}"`);
|
|
291
|
+
resolve();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// If sendToQueue returns false, channel is full - wait for drain
|
|
296
|
+
if (!sent) {
|
|
297
|
+
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel full, waiting for drain for queue "${queue}"`);
|
|
298
|
+
this._channel.once('drain', () => {
|
|
299
|
+
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel drained for queue "${queue}"`);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await confirmPromise;
|
|
306
|
+
} catch (confirmErr) {
|
|
307
|
+
// Check if channel was closed during confirmation
|
|
308
|
+
if (this._channel.closed) {
|
|
309
|
+
const errorMsg = `Publisher confirm failed for queue "${queue}": channel closed during confirmation. Original error: ${confirmErr.message}`;
|
|
310
|
+
console.error('[RabbitMQClient] [mq-client-core] [PUBLISH]', errorMsg);
|
|
311
|
+
throw new Error(errorMsg);
|
|
312
|
+
}
|
|
313
|
+
throw confirmErr;
|
|
237
314
|
}
|
|
238
|
-
this._channel.sendToQueue(queue, buffer, { persistent, headers });
|
|
239
315
|
} else {
|
|
240
316
|
// If exchange is specified, assert exchange and publish to it
|
|
241
317
|
if (this._channel.closed) {
|
|
242
318
|
throw new Error('Cannot publish: channel closed during operation');
|
|
243
319
|
}
|
|
244
320
|
await this._channel.assertExchange(exchange, 'direct', { durable: this._config.durable });
|
|
245
|
-
|
|
321
|
+
|
|
322
|
+
// Use callback-based confirmation for exchange publish
|
|
323
|
+
const confirmPromise = new Promise((resolve, reject) => {
|
|
324
|
+
const timeout = setTimeout(() => {
|
|
325
|
+
reject(new Error(`Publisher confirm timeout after 5 seconds for exchange "${exchange}"`));
|
|
326
|
+
}, 5000);
|
|
327
|
+
|
|
328
|
+
const sent = this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
|
|
329
|
+
clearTimeout(timeout);
|
|
330
|
+
if (err) {
|
|
331
|
+
console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
|
|
332
|
+
reject(err);
|
|
333
|
+
} else {
|
|
334
|
+
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] ✓ Exchange publish confirmed`);
|
|
335
|
+
resolve();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (!sent) {
|
|
340
|
+
this._channel.once('drain', () => {
|
|
341
|
+
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel drained for exchange "${exchange}"`);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
await confirmPromise;
|
|
348
|
+
} catch (confirmErr) {
|
|
349
|
+
if (this._channel.closed) {
|
|
350
|
+
throw new Error(`Publisher confirm failed for exchange "${exchange}": channel closed during confirmation. Original error: ${confirmErr.message}`);
|
|
351
|
+
}
|
|
352
|
+
throw confirmErr;
|
|
353
|
+
}
|
|
246
354
|
}
|
|
247
|
-
// Wait for confirmation (with timeout to prevent hanging)
|
|
248
|
-
const confirmPromise = this._channel.waitForConfirms();
|
|
249
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
250
|
-
setTimeout(() => reject(new Error('Publisher confirm timeout after 5 seconds')), 5000);
|
|
251
|
-
});
|
|
252
|
-
await Promise.race([confirmPromise, timeoutPromise]);
|
|
253
355
|
} catch (err) {
|
|
254
356
|
// If channel was closed, mark it for recreation
|
|
255
357
|
if (err.message && (err.message.includes('Channel closed') || err.message.includes('channel is closed') || this._channel?.closed)) {
|
|
@@ -270,8 +372,40 @@ class RabbitMQClient extends EventEmitter {
|
|
|
270
372
|
* @throws {Error} If consume setup fails or channel is not available.
|
|
271
373
|
*/
|
|
272
374
|
async consume(queue, onMessage, options = {}) {
|
|
273
|
-
|
|
274
|
-
|
|
375
|
+
// Use dedicated consumer channel instead of ConfirmChannel
|
|
376
|
+
// ConfirmChannel is optimized for publish operations, not consume
|
|
377
|
+
if (!this._consumerChannel) {
|
|
378
|
+
// Recreate consumer channel if closed
|
|
379
|
+
if (this._connection && !this._connection.closed) {
|
|
380
|
+
this._consumerChannel = await this._connection.createChannel();
|
|
381
|
+
this._consumerChannel.on('error', (err) => {
|
|
382
|
+
console.warn('[RabbitMQClient] Consumer channel error:', err.message);
|
|
383
|
+
this.emit('error', err);
|
|
384
|
+
});
|
|
385
|
+
this._consumerChannel.on('close', () => {
|
|
386
|
+
console.warn('[RabbitMQClient] Consumer channel closed');
|
|
387
|
+
this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
|
|
388
|
+
});
|
|
389
|
+
} else {
|
|
390
|
+
throw new Error('Cannot consume: consumer channel is not initialized and connection is closed');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (this._consumerChannel.closed) {
|
|
395
|
+
// Recreate consumer channel if closed
|
|
396
|
+
if (this._connection && !this._connection.closed) {
|
|
397
|
+
this._consumerChannel = await this._connection.createChannel();
|
|
398
|
+
this._consumerChannel.on('error', (err) => {
|
|
399
|
+
console.warn('[RabbitMQClient] Consumer channel error:', err.message);
|
|
400
|
+
this.emit('error', err);
|
|
401
|
+
});
|
|
402
|
+
this._consumerChannel.on('close', () => {
|
|
403
|
+
console.warn('[RabbitMQClient] Consumer channel closed');
|
|
404
|
+
this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
|
|
405
|
+
});
|
|
406
|
+
} else {
|
|
407
|
+
throw new Error('Cannot consume: consumer channel is closed and connection is closed');
|
|
408
|
+
}
|
|
275
409
|
}
|
|
276
410
|
|
|
277
411
|
const durable = options.durable !== undefined ? options.durable : this._config.durable;
|
|
@@ -368,32 +502,21 @@ class RabbitMQClient extends EventEmitter {
|
|
|
368
502
|
}
|
|
369
503
|
}
|
|
370
504
|
}
|
|
371
|
-
// Set prefetch if provided
|
|
505
|
+
// Set prefetch if provided (on consumer channel)
|
|
372
506
|
if (typeof prefetch === 'number') {
|
|
373
|
-
this.
|
|
507
|
+
this._consumerChannel.prefetch(prefetch);
|
|
374
508
|
}
|
|
375
509
|
|
|
376
510
|
// CRITICAL: Log before calling amqplib's consume()
|
|
377
511
|
// amqplib's consume() may internally call assertQueue() without parameters if queue doesn't exist
|
|
378
512
|
// This would create queue with default arguments (no TTL), causing 406 errors later
|
|
379
|
-
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call amqplib's
|
|
513
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call amqplib's consumerChannel.consume(${queue})`);
|
|
380
514
|
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ⚠ WARNING: amqplib's consume() may internally call assertQueue() WITHOUT parameters if queue doesn't exist`);
|
|
381
515
|
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ⚠ WARNING: We already asserted queue with correct parameters above - this should prevent auto-creation`);
|
|
382
516
|
|
|
383
|
-
//
|
|
384
|
-
// This
|
|
385
|
-
const
|
|
386
|
-
const originalAssertQueue = this._channel.assertQueue.bind(this._channel);
|
|
387
|
-
|
|
388
|
-
// Intercept assertQueue() calls to log them
|
|
389
|
-
this._channel.assertQueue = async function(queueName, options) {
|
|
390
|
-
console.log(`[RabbitMQClient] [mq-client-core] [INTERCEPT] assertQueue() called for: ${queueName}`);
|
|
391
|
-
console.log(`[RabbitMQClient] [mq-client-core] [INTERCEPT] Options:`, JSON.stringify(options || {}, null, 2));
|
|
392
|
-
console.log(`[RabbitMQClient] [mq-client-core] [INTERCEPT] Stack trace:`, new Error().stack.split('\n').slice(1, 10).join('\n'));
|
|
393
|
-
return originalAssertQueue.call(this, queueName, options);
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
await this._channel.consume(
|
|
517
|
+
// Use dedicated consumer channel for consume operations
|
|
518
|
+
// This prevents conflicts with ConfirmChannel used for publish operations
|
|
519
|
+
const consumeResult = await this._consumerChannel.consume(
|
|
397
520
|
queue,
|
|
398
521
|
async (msg) => {
|
|
399
522
|
if (msg === null) {
|
|
@@ -402,15 +525,18 @@ class RabbitMQClient extends EventEmitter {
|
|
|
402
525
|
try {
|
|
403
526
|
await onMessage(msg);
|
|
404
527
|
if (!noAck) {
|
|
405
|
-
this.
|
|
528
|
+
this._consumerChannel.ack(msg);
|
|
406
529
|
}
|
|
407
530
|
} catch (handlerErr) {
|
|
408
531
|
// Negative acknowledge and requeue by default
|
|
409
|
-
this.
|
|
532
|
+
this._consumerChannel.nack(msg, false, true);
|
|
410
533
|
}
|
|
411
534
|
},
|
|
412
535
|
{ noAck }
|
|
413
536
|
);
|
|
537
|
+
|
|
538
|
+
// Return consumer tag for cancellation
|
|
539
|
+
return consumeResult.consumerTag;
|
|
414
540
|
} catch (err) {
|
|
415
541
|
this.emit('error', err);
|
|
416
542
|
throw err;
|