@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/mq-client-core",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -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
- this._channel.on('error', (err) => this.emit('error', err));
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
- throw new Error('Cannot publish: channel is not initialized or closed');
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('Cannot publish: channel closed during operation');
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
- this._channel.publish(exchange, routingKey, buffer, { persistent, headers });
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
- if (!this._channel) {
274
- throw new Error('Cannot consume: channel is not initialized');
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._channel.prefetch(prefetch);
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 channel.consume(${queue})`);
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
- // CRITICAL: Wrap amqplib's channel.consume() to intercept any internal assertQueue() calls
384
- // This will help us identify if amqplib is creating the queue without TTL
385
- const originalConsume = this._channel.consume.bind(this._channel);
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._channel.ack(msg);
528
+ this._consumerChannel.ack(msg);
406
529
  }
407
530
  } catch (handlerErr) {
408
531
  // Negative acknowledge and requeue by default
409
- this._channel.nack(msg, false, true);
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;