@onlineapps/mq-client-core 1.0.30 → 1.0.32

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.30",
3
+ "version": "1.0.32",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -113,6 +113,72 @@ module.exports = {
113
113
  }
114
114
  },
115
115
 
116
+ /**
117
+ * Delivery infrastructure queue configurations
118
+ * Managed by API Delivery Dispatcher
119
+ */
120
+ delivery: {
121
+ /**
122
+ * delivery.retry - Retry queue for failed delivery attempts
123
+ * Used for delayed retries driven by Delivery Dispatcher
124
+ */
125
+ retry: {
126
+ durable: true,
127
+ arguments: {
128
+ 'x-max-length': 10000 // Allow up to 10k pending retry messages
129
+ }
130
+ },
131
+
132
+ /**
133
+ * delivery.result - Final delivery status events for audits/monitoring
134
+ */
135
+ result: {
136
+ durable: true,
137
+ arguments: {
138
+ 'x-message-ttl': 86400000, // 24 hours TTL for audit traceability
139
+ 'x-max-length': 50000
140
+ }
141
+ },
142
+
143
+ /**
144
+ * delivery.websocket.notify - Notifications for Gateway websocket hub
145
+ */
146
+ 'websocket.notify': {
147
+ durable: true,
148
+ arguments: {
149
+ 'x-message-ttl': 60000, // Notifications older than 60s are irrelevant
150
+ 'x-max-length': 10000
151
+ }
152
+ }
153
+ },
154
+
155
+ /**
156
+ * Validation infrastructure queue configurations
157
+ */
158
+ validation: {
159
+ /**
160
+ * validation.requests - Requests from Registry to Validator
161
+ */
162
+ requests: {
163
+ durable: true,
164
+ arguments: {
165
+ 'x-message-ttl': 300000, // 5 minutes TTL to avoid stale requests
166
+ 'x-max-length': 10000
167
+ }
168
+ },
169
+
170
+ /**
171
+ * validation.responses - Responses from Validator back to Registry
172
+ */
173
+ responses: {
174
+ durable: true,
175
+ arguments: {
176
+ 'x-message-ttl': 300000,
177
+ 'x-max-length': 10000
178
+ }
179
+ }
180
+ },
181
+
116
182
  /**
117
183
  * Infrastructure health tracking queue configurations
118
184
  */
@@ -269,7 +335,34 @@ module.exports = {
269
335
  queueName.startsWith('registry.') ||
270
336
  queueName.startsWith('infrastructure.') ||
271
337
  queueName.startsWith('validation.') ||
272
- queueName.startsWith('monitoring.');
338
+ queueName.startsWith('monitoring.') ||
339
+ queueName.startsWith('delivery.');
340
+ },
341
+
342
+ /**
343
+ * Get delivery queue configuration
344
+ * @param {string} queueName - Queue name (e.g., 'delivery.retry')
345
+ * @returns {Object} Queue configuration
346
+ */
347
+ getDeliveryQueueConfig(queueName) {
348
+ if (!queueName.startsWith('delivery.')) {
349
+ throw new Error(`Queue ${queueName} is not a delivery queue`);
350
+ }
351
+ const name = queueName.replace('delivery.', '');
352
+ return this.getQueueConfig('delivery', name);
353
+ },
354
+
355
+ /**
356
+ * Get validation queue configuration
357
+ * @param {string} queueName - Queue name (e.g., 'validation.requests')
358
+ * @returns {Object} Queue configuration
359
+ */
360
+ getValidationQueueConfig(queueName) {
361
+ if (!queueName.startsWith('validation.')) {
362
+ throw new Error(`Queue ${queueName} is not a validation queue`);
363
+ }
364
+ const name = queueName.replace('validation.', '');
365
+ return this.getQueueConfig('validation', name);
273
366
  },
274
367
 
275
368
  /**
@@ -368,6 +461,10 @@ module.exports = {
368
461
  return this.getInfrastructureHealthQueueConfig(queueName);
369
462
  } else if (queueName.startsWith('monitoring.')) {
370
463
  return this.getMonitoringQueueConfig(queueName);
464
+ } else if (queueName.startsWith('validation.')) {
465
+ return this.getValidationQueueConfig(queueName);
466
+ } else if (queueName.startsWith('delivery.')) {
467
+ return this.getDeliveryQueueConfig(queueName);
371
468
  } else {
372
469
  throw new Error(`Queue ${queueName} is not an infrastructure queue. Infrastructure queues must start with 'workflow.', 'registry.', 'infrastructure.', or 'monitoring.'`);
373
470
  }
@@ -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
  /**
@@ -109,6 +110,18 @@ class RabbitMQClient extends EventEmitter {
109
110
  this._queueChannel.on('close', () => {
110
111
  console.warn('[RabbitMQClient] Queue channel closed');
111
112
  });
113
+
114
+ // Create a dedicated channel for consume operations
115
+ // This prevents channel conflicts between publish (ConfirmChannel) and consume operations
116
+ this._consumerChannel = await this._connection.createChannel();
117
+ this._consumerChannel.on('error', (err) => {
118
+ console.warn('[RabbitMQClient] Consumer channel error:', err.message);
119
+ this.emit('error', err);
120
+ });
121
+ this._consumerChannel.on('close', () => {
122
+ console.warn('[RabbitMQClient] Consumer channel closed');
123
+ this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
124
+ });
112
125
  } catch (err) {
113
126
  // Cleanup partially created resources
114
127
  if (this._connection) {
@@ -128,6 +141,14 @@ class RabbitMQClient extends EventEmitter {
128
141
  * @returns {Promise<void>}
129
142
  */
130
143
  async disconnect() {
144
+ try {
145
+ if (this._consumerChannel) {
146
+ await this._consumerChannel.close();
147
+ this._consumerChannel = null;
148
+ }
149
+ } catch (err) {
150
+ console.warn('[RabbitMQClient] Error closing consumer channel:', err.message);
151
+ }
131
152
  try {
132
153
  if (this._queueChannel) {
133
154
  await this._queueChannel.close();
@@ -270,8 +291,40 @@ class RabbitMQClient extends EventEmitter {
270
291
  * @throws {Error} If consume setup fails or channel is not available.
271
292
  */
272
293
  async consume(queue, onMessage, options = {}) {
273
- if (!this._channel) {
274
- throw new Error('Cannot consume: channel is not initialized');
294
+ // Use dedicated consumer channel instead of ConfirmChannel
295
+ // ConfirmChannel is optimized for publish operations, not consume
296
+ if (!this._consumerChannel) {
297
+ // Recreate consumer channel if closed
298
+ if (this._connection && !this._connection.closed) {
299
+ this._consumerChannel = await this._connection.createChannel();
300
+ this._consumerChannel.on('error', (err) => {
301
+ console.warn('[RabbitMQClient] Consumer channel error:', err.message);
302
+ this.emit('error', err);
303
+ });
304
+ this._consumerChannel.on('close', () => {
305
+ console.warn('[RabbitMQClient] Consumer channel closed');
306
+ this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
307
+ });
308
+ } else {
309
+ throw new Error('Cannot consume: consumer channel is not initialized and connection is closed');
310
+ }
311
+ }
312
+
313
+ if (this._consumerChannel.closed) {
314
+ // Recreate consumer channel if closed
315
+ if (this._connection && !this._connection.closed) {
316
+ this._consumerChannel = await this._connection.createChannel();
317
+ this._consumerChannel.on('error', (err) => {
318
+ console.warn('[RabbitMQClient] Consumer channel error:', err.message);
319
+ this.emit('error', err);
320
+ });
321
+ this._consumerChannel.on('close', () => {
322
+ console.warn('[RabbitMQClient] Consumer channel closed');
323
+ this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
324
+ });
325
+ } else {
326
+ throw new Error('Cannot consume: consumer channel is closed and connection is closed');
327
+ }
275
328
  }
276
329
 
277
330
  const durable = options.durable !== undefined ? options.durable : this._config.durable;
@@ -368,32 +421,21 @@ class RabbitMQClient extends EventEmitter {
368
421
  }
369
422
  }
370
423
  }
371
- // Set prefetch if provided
424
+ // Set prefetch if provided (on consumer channel)
372
425
  if (typeof prefetch === 'number') {
373
- this._channel.prefetch(prefetch);
426
+ this._consumerChannel.prefetch(prefetch);
374
427
  }
375
428
 
376
429
  // CRITICAL: Log before calling amqplib's consume()
377
430
  // amqplib's consume() may internally call assertQueue() without parameters if queue doesn't exist
378
431
  // 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})`);
432
+ console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call amqplib's consumerChannel.consume(${queue})`);
380
433
  console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ⚠ WARNING: amqplib's consume() may internally call assertQueue() WITHOUT parameters if queue doesn't exist`);
381
434
  console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ⚠ WARNING: We already asserted queue with correct parameters above - this should prevent auto-creation`);
382
435
 
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(
436
+ // Use dedicated consumer channel for consume operations
437
+ // This prevents conflicts with ConfirmChannel used for publish operations
438
+ const consumeResult = await this._consumerChannel.consume(
397
439
  queue,
398
440
  async (msg) => {
399
441
  if (msg === null) {
@@ -402,15 +444,18 @@ class RabbitMQClient extends EventEmitter {
402
444
  try {
403
445
  await onMessage(msg);
404
446
  if (!noAck) {
405
- this._channel.ack(msg);
447
+ this._consumerChannel.ack(msg);
406
448
  }
407
449
  } catch (handlerErr) {
408
450
  // Negative acknowledge and requeue by default
409
- this._channel.nack(msg, false, true);
451
+ this._consumerChannel.nack(msg, false, true);
410
452
  }
411
453
  },
412
454
  { noAck }
413
455
  );
456
+
457
+ // Return consumer tag for cancellation
458
+ return consumeResult.consumerTag;
414
459
  } catch (err) {
415
460
  this.emit('error', err);
416
461
  throw err;
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const queueConfig = require('../../src/config/queueConfig');
4
+
5
+ describe('queueConfig delivery infrastructure', () => {
6
+ test('delivery.retry configuration', () => {
7
+ const config = queueConfig.getInfrastructureQueueConfig('delivery.retry');
8
+ expect(config).toEqual({
9
+ durable: true,
10
+ arguments: {
11
+ 'x-max-length': 10000
12
+ }
13
+ });
14
+ });
15
+
16
+ test('delivery.result configuration', () => {
17
+ const config = queueConfig.getInfrastructureQueueConfig('delivery.result');
18
+ expect(config).toEqual({
19
+ durable: true,
20
+ arguments: {
21
+ 'x-message-ttl': 86400000,
22
+ 'x-max-length': 50000
23
+ }
24
+ });
25
+ });
26
+
27
+ test('delivery.websocket.notify configuration', () => {
28
+ const config = queueConfig.getInfrastructureQueueConfig('delivery.websocket.notify');
29
+ expect(config).toEqual({
30
+ durable: true,
31
+ arguments: {
32
+ 'x-message-ttl': 60000,
33
+ 'x-max-length': 10000
34
+ }
35
+ });
36
+ });
37
+ });
38
+
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const queueConfig = require('../../src/config/queueConfig');
4
+
5
+ describe('queueConfig validation infrastructure', () => {
6
+ test('validation.requests configuration', () => {
7
+ const config = queueConfig.getInfrastructureQueueConfig('validation.requests');
8
+ expect(config).toEqual({
9
+ durable: true,
10
+ arguments: {
11
+ 'x-message-ttl': 300000,
12
+ 'x-max-length': 10000
13
+ }
14
+ });
15
+ });
16
+
17
+ test('validation.responses configuration', () => {
18
+ const config = queueConfig.getInfrastructureQueueConfig('validation.responses');
19
+ expect(config).toEqual({
20
+ durable: true,
21
+ arguments: {
22
+ 'x-message-ttl': 300000,
23
+ 'x-max-length': 10000
24
+ }
25
+ });
26
+ });
27
+ });
28
+