@onlineapps/mq-client-core 1.0.78 → 1.0.80

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.78",
3
+ "version": "1.0.80",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/config.js CHANGED
@@ -27,3 +27,6 @@ const runtimeCfg = createRuntimeConfig({
27
27
  module.exports = runtimeCfg;
28
28
 
29
29
 
30
+
31
+
32
+
package/src/defaults.js CHANGED
@@ -15,3 +15,6 @@ module.exports = {
15
15
  };
16
16
 
17
17
 
18
+
19
+
20
+
@@ -80,6 +80,7 @@ class RabbitMQClient extends EventEmitter {
80
80
  this._reconnecting = false;
81
81
  this._disconnecting = false;
82
82
  this._reconnectAttempts = 0;
83
+ this._activeTimers = new Set();
83
84
  this._maxReconnectAttempts = this._config.maxReconnectAttempts || 10; // Max 10 attempts
84
85
  this._reconnectBaseDelay = this._config.reconnectBaseDelay || 1000; // Start with 1 second
85
86
  this._reconnectMaxDelay = this._config.reconnectMaxDelay || 30000; // Max 30 seconds
@@ -418,6 +419,41 @@ class RabbitMQClient extends EventEmitter {
418
419
  * @returns {Promise<void>}
419
420
  * @throws {Error} If connection or channel creation fails.
420
421
  */
422
+ /**
423
+ * Helper to set a trackable timeout
424
+ * @private
425
+ */
426
+ _setTimeout(callback, ms) {
427
+ const timer = setTimeout(() => {
428
+ this._activeTimers.delete(timer);
429
+ callback();
430
+ }, ms);
431
+ this._activeTimers.add(timer);
432
+ return timer;
433
+ }
434
+
435
+ /**
436
+ * Helper to clear a trackable timeout
437
+ * @private
438
+ */
439
+ _clearTimeout(timer) {
440
+ if (timer) {
441
+ clearTimeout(timer);
442
+ this._activeTimers.delete(timer);
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Clear all active timers
448
+ * @private
449
+ */
450
+ _clearAllTimers() {
451
+ for (const timer of this._activeTimers) {
452
+ clearTimeout(timer);
453
+ }
454
+ this._activeTimers.clear();
455
+ }
456
+
421
457
  async connect() {
422
458
  let connectTimeoutTimer = null;
423
459
  try {
@@ -435,7 +471,7 @@ class RabbitMQClient extends EventEmitter {
435
471
 
436
472
  const connectPromise = amqp.connect(...connectArgs);
437
473
  const timeoutPromise = new Promise((_, reject) => {
438
- connectTimeoutTimer = setTimeout(() => reject(new Error('Connection timeout after 10 seconds')), 10000);
474
+ connectTimeoutTimer = this._setTimeout(() => reject(new Error('Connection timeout after 10 seconds')), 10000);
439
475
  if (connectTimeoutTimer && typeof connectTimeoutTimer.unref === 'function') {
440
476
  connectTimeoutTimer.unref();
441
477
  }
@@ -443,7 +479,7 @@ class RabbitMQClient extends EventEmitter {
443
479
  console.log('[RabbitMQClient] Starting connection race...');
444
480
  this._connection = await Promise.race([connectPromise, timeoutPromise]);
445
481
  if (connectTimeoutTimer) {
446
- clearTimeout(connectTimeoutTimer);
482
+ this._clearTimeout(connectTimeoutTimer);
447
483
  connectTimeoutTimer = null;
448
484
  }
449
485
  console.log('[RabbitMQClient] Connection established');
@@ -562,18 +598,18 @@ class RabbitMQClient extends EventEmitter {
562
598
 
563
599
  // Wait for 'reconnected' event
564
600
  return new Promise((resolve, reject) => {
565
- const timeout = setTimeout(() => {
601
+ const timeout = this._setTimeout(() => {
566
602
  this.removeListener('reconnected', onReconnected);
567
603
  this.removeListener('error', onError);
568
604
  reject(new Error(`Reconnection timeout after ${this._reconnectWaitTimeout}ms`));
569
605
  }, this._reconnectWaitTimeout);
570
606
 
571
607
  const onReconnected = () => {
572
- clearTimeout(timeout);
608
+ this._clearTimeout(timeout);
573
609
  this.removeListener('reconnected', onReconnected);
574
610
  this.removeListener('error', onError);
575
611
  // Wait a bit for channels to be fully recreated
576
- setTimeout(resolve, 500);
612
+ this._setTimeout(resolve, 500);
577
613
  };
578
614
 
579
615
  const onError = (error) => {
@@ -581,7 +617,7 @@ class RabbitMQClient extends EventEmitter {
581
617
  if (error.message && error.message.includes('Connection closed unexpectedly')) {
582
618
  return; // Expected during reconnection
583
619
  }
584
- clearTimeout(timeout);
620
+ this._clearTimeout(timeout);
585
621
  this.removeListener('reconnected', onReconnected);
586
622
  this.removeListener('error', onError);
587
623
  reject(error);
@@ -1052,6 +1088,10 @@ class RabbitMQClient extends EventEmitter {
1052
1088
 
1053
1089
  async disconnect() {
1054
1090
  this._disconnecting = true;
1091
+
1092
+ // Clear all active timers (reconnection, waits, etc.)
1093
+ this._clearAllTimers();
1094
+
1055
1095
  // Stop health monitoring
1056
1096
  this._stopHealthMonitoring();
1057
1097
 
@@ -1324,13 +1364,13 @@ class RabbitMQClient extends EventEmitter {
1324
1364
  // Use callback-based confirmation - kanály jsou spolehlivé, takže callback vždy dorazí
1325
1365
  const confirmPromise = new Promise((resolve, reject) => {
1326
1366
  // Set timeout for publish confirmation (configurable)
1327
- const timeout = setTimeout(() => {
1367
+ const timeout = this._setTimeout(() => {
1328
1368
  reject(new Error(`Publish confirmation timeout for queue "${queue}" after ${this._publishConfirmationTimeout}ms`));
1329
1369
  }, this._publishConfirmationTimeout);
1330
1370
 
1331
1371
  // Check if channel is still valid before sending
1332
1372
  if (!this._channel || this._channel.closed) {
1333
- clearTimeout(timeout);
1373
+ this._clearTimeout(timeout);
1334
1374
  reject(new Error(`Cannot publish: channel is closed for queue "${queue}"`));
1335
1375
  return;
1336
1376
  }
@@ -1344,7 +1384,7 @@ class RabbitMQClient extends EventEmitter {
1344
1384
  try {
1345
1385
  originalChannel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
1346
1386
  callbackInvoked = true;
1347
- clearTimeout(timeout);
1387
+ this._clearTimeout(timeout);
1348
1388
 
1349
1389
  // CRITICAL: Check if channel was closed or recreated during publish
1350
1390
  // If channel was recreated, the delivery tag is invalid - ignore this callback
@@ -1398,11 +1438,11 @@ class RabbitMQClient extends EventEmitter {
1398
1438
  });
1399
1439
 
1400
1440
  // Set a safety timeout - if callback wasn't invoked and channel closed, retry
1401
- setTimeout(() => {
1441
+ this._setTimeout(() => {
1402
1442
  if (!callbackInvoked && (!this._channel || this._channel.closed || this._channel !== originalChannel)) {
1403
1443
  console.warn(`[RabbitMQClient] [mq-client-core] [PUBLISH] Callback timeout and channel closed for queue "${queue}", will retry after reconnection`);
1404
1444
  if (this._reconnecting) {
1405
- clearTimeout(timeout);
1445
+ this._clearTimeout(timeout);
1406
1446
  this._waitForReconnection().then(() => {
1407
1447
  return this.publish(queue, buffer, options);
1408
1448
  }).then(resolve).catch(reject);
@@ -1438,12 +1478,12 @@ class RabbitMQClient extends EventEmitter {
1438
1478
  // Use callback-based confirmation - kanály jsou spolehlivé
1439
1479
  const confirmPromise = new Promise((resolve, reject) => {
1440
1480
  // Set timeout for exchange publish confirmation
1441
- const timeout = setTimeout(() => {
1481
+ const timeout = this._setTimeout(() => {
1442
1482
  reject(new Error(`Exchange publish confirmation timeout for exchange "${exchange}" after ${this._publishConfirmationTimeout}ms`));
1443
1483
  }, this._publishConfirmationTimeout);
1444
1484
 
1445
1485
  this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
1446
- clearTimeout(timeout);
1486
+ this._clearTimeout(timeout);
1447
1487
  if (err) {
1448
1488
  console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
1449
1489
  reject(err);
@@ -1512,16 +1552,17 @@ class RabbitMQClient extends EventEmitter {
1512
1552
 
1513
1553
  if (queueConfig) {
1514
1554
  if (isInfraQueue) {
1515
- // Infrastructure queue - use central config
1555
+ // Infrastructure queue - use central config for expected arguments,
1556
+ // but DO NOT create it here. Ownership rule: infra queues are created by their owning infra service.
1516
1557
  try {
1517
1558
  const infraConfig = queueConfig.getInfrastructureQueueConfig(queue);
1518
1559
  queueOptions = {
1519
1560
  durable: infraConfig.durable !== false,
1520
1561
  arguments: { ...infraConfig.arguments }
1521
1562
  };
1522
- console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] Asserting infrastructure queue ${queue} with config from queueConfig`);
1563
+ console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] Checking infrastructure queue ${queue} exists (no auto-create)`);
1523
1564
  } catch (configErr) {
1524
- console.warn(`[RabbitMQClient] [mq-client-core] [CONSUMER] Infrastructure queue config not found for ${queue}, using default:`, configErr.message);
1565
+ console.warn(`[RabbitMQClient] [mq-client-core] [CONSUMER] Infrastructure queue config not found for ${queue}, will still require it to exist:`, configErr.message);
1525
1566
  }
1526
1567
  } else if (isBusinessQueue) {
1527
1568
  // Business queue - use central config
@@ -1560,28 +1601,45 @@ class RabbitMQClient extends EventEmitter {
1560
1601
  }
1561
1602
  }
1562
1603
 
1563
- try {
1564
- console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call assertQueue(${queue}, ${JSON.stringify(queueOptions)})`);
1565
-
1566
- // Ensure queue channel is available
1567
- await this._ensureQueueChannel();
1568
- if (!this._queueChannel) {
1569
- throw new Error('Queue channel is not available (connection may be closed)');
1604
+ // Ensure queue channel is available
1605
+ await this._ensureQueueChannel();
1606
+ if (!this._queueChannel) {
1607
+ throw new Error('Queue channel is not available (connection may be closed)');
1608
+ }
1609
+
1610
+ if (isInfraQueue) {
1611
+ // IMPORTANT: Do NOT auto-create infrastructure queues in consumers.
1612
+ // If missing, fail-fast. The owning infra service must recreate on startup.
1613
+ try {
1614
+ this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
1615
+ await this._queueChannel.checkQueue(queue);
1616
+ console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Infrastructure queue ${queue} exists (consumer will proceed)`);
1617
+ } catch (checkErr) {
1618
+ if (checkErr.code === 404) {
1619
+ throw new Error(
1620
+ `Infrastructure queue ${queue} is missing. ` +
1621
+ 'Ownership rule: infrastructure queues must be created by their owning infrastructure service via initInfrastructureQueues().'
1622
+ );
1623
+ }
1624
+ throw checkErr;
1570
1625
  }
1571
-
1572
- this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
1573
- await this._queueChannel.assertQueue(queue, queueOptions);
1574
- console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] Queue ${queue} asserted successfully`);
1575
- } catch (assertErr) {
1576
- // If queue exists with different arguments (406), this is a CRITICAL ERROR
1577
- // We should NOT proceed - the root cause must be fixed
1578
- if (assertErr.code === 406) {
1579
- console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✗ CRITICAL: Queue ${queue} exists with different arguments!`);
1580
- console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] Error:`, assertErr.message);
1581
- console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] Expected options:`, JSON.stringify(queueOptions, null, 2));
1582
- console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] This means assertQueue() was called without parameters somewhere else. Root cause must be fixed!`);
1583
- throw new Error(`Cannot assertQueue ${queue}: queue exists with different arguments. Root cause: assertQueue() was called without parameters. Fix the root cause instead of proceeding.`);
1584
- } else {
1626
+ } else {
1627
+ // Business queue (or unknown) - assert with canonical parameters to prevent 406 drift.
1628
+ try {
1629
+ console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call assertQueue(${queue}, ${JSON.stringify(queueOptions)})`);
1630
+ this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
1631
+ await this._queueChannel.assertQueue(queue, queueOptions);
1632
+ console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] Queue ${queue} asserted successfully`);
1633
+ } catch (assertErr) {
1634
+ // If queue exists with different arguments (406), this is a CRITICAL ERROR
1635
+ // We should NOT proceed - the root cause must be fixed
1636
+ if (assertErr.code === 406) {
1637
+ console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] CRITICAL: Queue ${queue} exists with different arguments!`);
1638
+ console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] Error:`, assertErr.message);
1639
+ console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] Expected options:`, JSON.stringify(queueOptions, null, 2));
1640
+ console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] This means assertQueue() was called without parameters somewhere else. Root cause must be fixed!`);
1641
+ throw new Error(`Cannot assertQueue ${queue}: queue exists with different arguments. Root cause: assertQueue() was called without parameters. Fix the root cause instead of proceeding.`);
1642
+ }
1585
1643
  // Other error - rethrow
1586
1644
  throw assertErr;
1587
1645
  }
@@ -1623,6 +1681,14 @@ class RabbitMQClient extends EventEmitter {
1623
1681
  if (msg === null) {
1624
1682
  return; // Consumer cancellation
1625
1683
  }
1684
+
1685
+ // IMPORTANT:
1686
+ // `msg` MUST be acked/nacked on the SAME channel instance that delivered it.
1687
+ // During reconnects, `this._consumerChannel` may be replaced while a handler is running.
1688
+ // If we ack/nack a message using a different (new) channel, RabbitMQ will close the channel with:
1689
+ // 406 (PRECONDITION-FAILED) unknown delivery tag
1690
+ // which cascades into "Channel closed" errors across the service.
1691
+ const channelForMsg = this._consumerChannel;
1626
1692
 
1627
1693
  // Structured log: message received
1628
1694
  const msgHeaders = msg.properties?.headers || {};
@@ -1650,7 +1716,23 @@ class RabbitMQClient extends EventEmitter {
1650
1716
  await onMessage(msg);
1651
1717
  // Acknowledge message after successful processing
1652
1718
  if (!noAck) {
1653
- this._consumerChannel.ack(msg);
1719
+ // Only ack on the original channel instance; if it's gone/closed, do not attempt ack.
1720
+ if (!channelForMsg || channelForMsg.closed) {
1721
+ console.warn('[RabbitMQClient] [mq-client-core] [CONSUMER] Cannot ack - consumer channel is closed/recreated (message will be requeued by broker)');
1722
+ return;
1723
+ }
1724
+ try {
1725
+ channelForMsg.ack(msg);
1726
+ } catch (ackErr) {
1727
+ // If the channel was closed/recreated during processing, delivery tag becomes invalid.
1728
+ // Do NOT send further acks that would close the channel with 406.
1729
+ const m = ackErr && ackErr.message ? ackErr.message : '';
1730
+ if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
1731
+ console.warn('[RabbitMQClient] [mq-client-core] [CONSUMER] Ack failed due to invalid delivery tag / closed channel (message delivery will be handled by broker)', { error: m });
1732
+ return;
1733
+ }
1734
+ throw ackErr;
1735
+ }
1654
1736
  // Structured log: message acknowledged
1655
1737
  this._log.output(wfId, 'MSG_ACKED', {
1656
1738
  handler: 'consume',
@@ -1668,9 +1750,9 @@ class RabbitMQClient extends EventEmitter {
1668
1750
  } catch (handlerErr) {
1669
1751
  // Negative acknowledge and requeue by default
1670
1752
  // Check if channel is still valid before nacking
1671
- if (this._consumerChannel && !this._consumerChannel.closed) {
1753
+ if (channelForMsg && !channelForMsg.closed) {
1672
1754
  try {
1673
- this._consumerChannel.nack(msg, false, true);
1755
+ channelForMsg.nack(msg, false, true);
1674
1756
  } catch (nackErr) {
1675
1757
  // Channel may have closed during nack - ignore
1676
1758
  console.warn(`[RabbitMQClient] [mq-client-core] Failed to nack message (channel may be closed): ${nackErr.message}`);
@@ -1731,7 +1813,13 @@ class RabbitMQClient extends EventEmitter {
1731
1813
  );
1732
1814
 
1733
1815
  console.log(`[RabbitMQClient] Waiting ${delay}ms before reconnection attempt ${this._reconnectAttempts + 1}...`);
1734
- await new Promise(resolve => setTimeout(resolve, delay));
1816
+ await new Promise(resolve => this._setTimeout(resolve, delay));
1817
+
1818
+ // Check if we started disconnecting during the wait
1819
+ if (this._disconnecting) {
1820
+ console.log('[RabbitMQClient] Disconnecting during reconnection wait, aborting');
1821
+ return;
1822
+ }
1735
1823
 
1736
1824
  // Attempt to reconnect
1737
1825
  console.log(`[RabbitMQClient] Reconnection attempt ${this._reconnectAttempts + 1}...`);
@@ -1760,7 +1848,7 @@ class RabbitMQClient extends EventEmitter {
1760
1848
 
1761
1849
  const connectPromise = amqp.connect(...connectArgs);
1762
1850
  const timeoutPromise = new Promise((_, reject) => {
1763
- setTimeout(() => reject(new Error('Connection timeout after 10 seconds')), 10000);
1851
+ this._setTimeout(() => reject(new Error('Connection timeout after 10 seconds')), 10000);
1764
1852
  });
1765
1853
 
1766
1854
  this._connection = await Promise.race([connectPromise, timeoutPromise]);
@@ -2136,6 +2224,11 @@ class RabbitMQClient extends EventEmitter {
2136
2224
  try {
2137
2225
  this._consumerChannel.ack(msg);
2138
2226
  } catch (err) {
2227
+ const m = err && err.message ? err.message : '';
2228
+ if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
2229
+ console.warn('[RabbitMQClient] [mq-client-core] Cannot ack - consumer channel is closed/recreated (delivery tag invalid)', { error: m });
2230
+ return;
2231
+ }
2139
2232
  this.emit('error', err);
2140
2233
  throw err;
2141
2234
  }
@@ -2154,6 +2247,11 @@ class RabbitMQClient extends EventEmitter {
2154
2247
  try {
2155
2248
  this._consumerChannel.nack(msg, false, requeue);
2156
2249
  } catch (err) {
2250
+ const m = err && err.message ? err.message : '';
2251
+ if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
2252
+ console.warn('[RabbitMQClient] [mq-client-core] Cannot nack - consumer channel is closed/recreated (delivery tag invalid)', { error: m });
2253
+ return;
2254
+ }
2157
2255
  this.emit('error', err);
2158
2256
  throw err;
2159
2257
  }