@onlineapps/mq-client-core 1.0.39 → 1.0.40

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.39",
3
+ "version": "1.0.40",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -56,6 +56,9 @@ class RabbitMQClient extends EventEmitter {
56
56
  this._reconnectMaxDelay = this._config.reconnectMaxDelay || 30000; // Max 30 seconds
57
57
  this._reconnectEnabled = this._config.reconnectEnabled !== false; // Default: enabled
58
58
  this._reconnectTimer = null;
59
+ // Reconnect wait timeout: at least maxReconnectAttempts × reconnectMaxDelay, minimum 60s
60
+ this._reconnectWaitTimeout = this._config.reconnectWaitTimeout ||
61
+ Math.max(60000, (this._maxReconnectAttempts * this._reconnectMaxDelay));
59
62
 
60
63
  // Publisher retry configuration
61
64
  this._publishRetryEnabled = this._config.publishRetryEnabled !== false; // Default: enabled
@@ -182,6 +185,53 @@ class RabbitMQClient extends EventEmitter {
182
185
  return this._publishMonitor.getPrometheusMetrics();
183
186
  }
184
187
 
188
+ /**
189
+ * Get current channel state
190
+ * @returns {Object} Channel state information
191
+ */
192
+ getChannelState() {
193
+ return {
194
+ publisher: {
195
+ exists: !!this._channel,
196
+ closed: this._channel ? this._channel.closed : true,
197
+ ready: this._channel && !this._channel.closed
198
+ },
199
+ queue: {
200
+ exists: !!this._queueChannel,
201
+ closed: this._queueChannel ? this._queueChannel.closed : true,
202
+ ready: this._queueChannel && !this._queueChannel.closed
203
+ },
204
+ consumer: {
205
+ exists: !!this._consumerChannel,
206
+ closed: this._consumerChannel ? this._consumerChannel.closed : true,
207
+ ready: this._consumerChannel && !this._consumerChannel.closed
208
+ }
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Get current consumer state
214
+ * @returns {Object} Consumer state information
215
+ */
216
+ getConsumerState() {
217
+ return {
218
+ tracked: this._activeConsumers.size,
219
+ active: Array.from(this._activeConsumers.keys())
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Get current buffer state
225
+ * @returns {Object} Buffer state information
226
+ */
227
+ getBufferState() {
228
+ return {
229
+ size: this._publishLayer._buffer.size(),
230
+ inMemory: this._publishLayer._buffer._inMemory?.size() || 0,
231
+ persistent: this._publishLayer._buffer._persistent ? (this._publishLayer._buffer._persistent.size?.() || 0) : 0
232
+ };
233
+ }
234
+
185
235
  /**
186
236
  * Getter for channel - provides compatibility with QueueManager
187
237
  * Returns ConfirmChannel for publish operations
@@ -457,8 +507,8 @@ class RabbitMQClient extends EventEmitter {
457
507
  const timeout = setTimeout(() => {
458
508
  this.removeListener('reconnected', onReconnected);
459
509
  this.removeListener('error', onError);
460
- reject(new Error('Reconnection timeout after 30 seconds'));
461
- }, 30000); // 30 seconds timeout
510
+ reject(new Error(`Reconnection timeout after ${this._reconnectWaitTimeout}ms`));
511
+ }, this._reconnectWaitTimeout);
462
512
 
463
513
  const onReconnected = () => {
464
514
  clearTimeout(timeout);
@@ -619,11 +669,12 @@ class RabbitMQClient extends EventEmitter {
619
669
  * Ensure consumer channel exists and is open
620
670
  * Auto-recreates and re-registers all consumers if closed
621
671
  * @private
622
- * @returns {Promise<void>}
672
+ * @returns {Promise<{reRegistered: number, failed: number}>} - Number of consumers re-registered and failed
623
673
  */
624
674
  async _ensureConsumerChannel() {
625
675
  if (this._consumerChannel && !this._consumerChannel.closed) {
626
- return; // Channel is good
676
+ // Channel is good - return current consumer state
677
+ return { reRegistered: this._activeConsumers.size, failed: 0 };
627
678
  }
628
679
 
629
680
  if (!this._connection || this._connection.closed) {
@@ -631,7 +682,7 @@ class RabbitMQClient extends EventEmitter {
631
682
  // Reconnection logic will handle this - don't throw, just return
632
683
  // The channel will be recreated after reconnection completes
633
684
  console.warn('[RabbitMQClient] [mq-client-core] Cannot recreate consumer channel: connection is closed (reconnection in progress)');
634
- return;
685
+ return { reRegistered: 0, failed: this._activeConsumers.size };
635
686
  }
636
687
 
637
688
  try {
@@ -657,6 +708,9 @@ class RabbitMQClient extends EventEmitter {
657
708
  this._attachConsumerChannelHandlers(this._consumerChannel);
658
709
 
659
710
  // Re-register all active consumers
711
+ let reRegistered = 0;
712
+ let failed = 0;
713
+
660
714
  if (this._activeConsumers.size > 0) {
661
715
  console.log(`[RabbitMQClient] [mq-client-core] Re-registering ${this._activeConsumers.size} consumers...`);
662
716
  for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
@@ -665,18 +719,25 @@ class RabbitMQClient extends EventEmitter {
665
719
  if (consumerInfo.options.queueOptions) {
666
720
  try {
667
721
  await this._ensureQueueChannel();
722
+ if (!this._queueChannel || this._queueChannel.closed) {
723
+ throw new Error('Queue channel is not available (connection may be closed)');
724
+ }
668
725
  this._trackChannelOperation(this._queueChannel, `assertQueue ${queue} (re-register)`);
669
726
  await this._queueChannel.assertQueue(queue, consumerInfo.options.queueOptions);
670
727
  } catch (assertErr) {
671
728
  if (assertErr.code === 404) {
672
729
  console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} does not exist, skipping consumer re-registration`);
673
730
  this._activeConsumers.delete(queue);
731
+ failed++;
732
+ this.emit('consumer:re-registration:failed', { queue, error: 'Queue does not exist', code: 404 });
674
733
  continue;
675
734
  }
676
735
  // 406 error means queue exists with different args - skip this consumer
677
736
  if (assertErr.code === 406) {
678
737
  console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} exists with different args, skipping consumer re-registration`);
679
738
  this._activeConsumers.delete(queue);
739
+ failed++;
740
+ this.emit('consumer:re-registration:failed', { queue, error: 'Queue exists with different arguments', code: 406 });
680
741
  continue;
681
742
  }
682
743
  throw assertErr;
@@ -689,16 +750,21 @@ class RabbitMQClient extends EventEmitter {
689
750
  { noAck: consumerInfo.options.noAck }
690
751
  );
691
752
  consumerInfo.consumerTag = consumeResult.consumerTag;
753
+ reRegistered++;
692
754
  console.log(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
693
755
  } catch (err) {
756
+ failed++;
694
757
  console.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
695
758
  // Remove failed consumer from tracking
696
759
  this._activeConsumers.delete(queue);
760
+ // Emit event for re-registration failure
761
+ this.emit('consumer:re-registration:failed', { queue, error: err.message, code: err.code });
697
762
  }
698
763
  }
699
764
  }
700
765
 
701
- console.log('[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated');
766
+ console.log(`[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated (re-registered: ${reRegistered}, failed: ${failed})`);
767
+ return { reRegistered, failed };
702
768
  } catch (err) {
703
769
  this._consumerChannel = null;
704
770
  throw new Error(`Failed to recreate consumer channel: ${err.message}`);
@@ -1010,6 +1076,13 @@ class RabbitMQClient extends EventEmitter {
1010
1076
  try {
1011
1077
  // Ensure queue channel exists and is open (auto-recreates if closed)
1012
1078
  await this._ensureQueueChannel();
1079
+ if (!this._queueChannel || this._queueChannel.closed) {
1080
+ // Connection is closed and not reconnecting - cannot check queue
1081
+ throw new TransientPublishError(
1082
+ `Cannot check queue ${queue}: queue channel is not available (connection may be closed)`,
1083
+ new Error('Queue channel unavailable')
1084
+ );
1085
+ }
1013
1086
  this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
1014
1087
  await this._queueChannel.checkQueue(queue);
1015
1088
  // Queue exists - proceed to publish
@@ -1041,8 +1114,13 @@ class RabbitMQClient extends EventEmitter {
1041
1114
  const queueOptions = options.queueOptions || { durable: this._config.durable };
1042
1115
  console.warn(`[RabbitMQClient] [mq-client-core] [PUBLISH] Auto-creating non-infrastructure queue ${queue} with default options (no TTL). This should be avoided for production.`);
1043
1116
  // Channel could have been nulled out by close handlers; ensure it's available
1044
- if (!this._queueChannel) {
1045
- await this._ensureQueueChannel();
1117
+ await this._ensureQueueChannel();
1118
+ if (!this._queueChannel || this._queueChannel.closed) {
1119
+ // Connection is closed and not reconnecting - cannot create queue
1120
+ throw new TransientPublishError(
1121
+ `Cannot create queue ${queue}: queue channel is not available (connection may be closed)`,
1122
+ checkErr
1123
+ );
1046
1124
  }
1047
1125
  this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
1048
1126
  await this._queueChannel.assertQueue(queue, queueOptions);
@@ -1184,23 +1262,11 @@ class RabbitMQClient extends EventEmitter {
1184
1262
  await confirmPromise;
1185
1263
  }
1186
1264
  } catch (err) {
1187
- // Pokud je kanál zavřený, stále se pokusíme o jedno interní obnovení a retry,
1188
- // aby byl publish co nejodolnější i předtím, než vstoupí do vyšší retry/recovery vrstvy.
1189
- if (err && err.message && (err.message.includes('Channel closed') || err.message.includes('channel is closed') || this._channel?.closed)) {
1190
- console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish, recreating and retrying once...');
1191
- try {
1192
- await this._ensurePublisherChannel();
1193
- // Retry publish once (může znovu vyhodit – už dojde do retry/recovery vrstvy)
1194
- return await this.publish(queue, buffer, options);
1195
- } catch (retryErr) {
1196
- console.error('[RabbitMQClient] [mq-client-core] [PUBLISH] Retry after channel recreation failed:', retryErr.message);
1197
- const classifiedRetryErr = classifyPublishError(retryErr);
1198
- this.emit('error', classifiedRetryErr);
1199
- throw classifiedRetryErr;
1200
- }
1201
- }
1202
-
1203
- // Všechny ostatní chyby projdou jednotnou klasifikací do Transient/Permanent/QueueNotFound.
1265
+ // CRITICAL: _publishOnce() je "single attempt" - žádný vnitřní retry
1266
+ // Retry logika je pouze v PublishLayer, aby byl počet pokusů předvídatelný
1267
+ // Pokud channel closed, vyhodíme TransientPublishError a necháme PublishLayer řešit retry
1268
+
1269
+ // Všechny chyby projdou jednotnou klasifikací do Transient/Permanent/QueueNotFound
1204
1270
  const classifiedErr = classifyPublishError(err);
1205
1271
  this.emit('error', classifiedErr);
1206
1272
  throw classifiedErr;
@@ -1505,60 +1571,103 @@ class RabbitMQClient extends EventEmitter {
1505
1571
  }
1506
1572
  });
1507
1573
 
1508
- // Recreate all channels
1574
+ // Recreate all channels with best effort - partial success is better than total failure
1509
1575
  console.log('[RabbitMQClient] Recreating channels...');
1510
1576
 
1577
+ const channelState = { publisher: false, queue: false, consumer: false };
1578
+ let consumerReRegResult = { reRegistered: 0, failed: 0 };
1579
+
1511
1580
  // Recreate publisher channel
1512
- this._channel = await this._connection.createConfirmChannel();
1513
- this._channel._createdAt = new Date().toISOString();
1514
- this._channel._closeReason = null;
1515
- this._channel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1516
- this._channel._type = 'publisher';
1517
- this._attachPublisherChannelHandlers(this._channel);
1581
+ try {
1582
+ this._channel = await this._connection.createConfirmChannel();
1583
+ this._channel._createdAt = new Date().toISOString();
1584
+ this._channel._closeReason = null;
1585
+ this._channel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1586
+ this._channel._type = 'publisher';
1587
+ this._attachPublisherChannelHandlers(this._channel);
1588
+ channelState.publisher = true;
1589
+ console.log('[RabbitMQClient] ✓ Publisher channel recreated');
1590
+ } catch (err) {
1591
+ console.error('[RabbitMQClient] Failed to recreate publisher channel:', err.message);
1592
+ this._channel = null;
1593
+ // Continue - partial success is better than total failure
1594
+ }
1518
1595
 
1519
1596
  // Recreate queue channel
1520
- this._queueChannel = await this._connection.createChannel();
1521
- this._queueChannel._createdAt = new Date().toISOString();
1522
- this._queueChannel._closeReason = null;
1523
- this._queueChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1524
- this._queueChannel._type = 'queue';
1525
- this._attachQueueChannelHandlers(this._queueChannel);
1597
+ try {
1598
+ this._queueChannel = await this._connection.createChannel();
1599
+ this._queueChannel._createdAt = new Date().toISOString();
1600
+ this._queueChannel._closeReason = null;
1601
+ this._queueChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1602
+ this._queueChannel._type = 'queue';
1603
+ this._attachQueueChannelHandlers(this._queueChannel);
1604
+ channelState.queue = true;
1605
+ console.log('[RabbitMQClient] ✓ Queue channel recreated');
1606
+ } catch (err) {
1607
+ console.error('[RabbitMQClient] Failed to recreate queue channel:', err.message);
1608
+ this._queueChannel = null;
1609
+ // Continue - partial success is better than total failure
1610
+ }
1526
1611
 
1527
1612
  // Recreate consumer channel and re-register all consumers
1528
- this._consumerChannel = await this._connection.createChannel();
1529
- this._consumerChannel._createdAt = new Date().toISOString();
1530
- this._consumerChannel._closeReason = null;
1531
- // IMPORTANT: Don't re-register consumers here - _ensureConsumerChannel() will do it
1532
- // This prevents duplicate re-registration (once in _reconnectWithBackoff, once in _ensureConsumerChannel)
1533
- // Just ensure consumer channel is created - it will automatically re-register all active consumers
1534
- await this._ensureConsumerChannel();
1613
+ try {
1614
+ this._consumerChannel = await this._connection.createChannel();
1615
+ this._consumerChannel._createdAt = new Date().toISOString();
1616
+ this._consumerChannel._closeReason = null;
1617
+ this._consumerChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1618
+ this._consumerChannel._type = 'consumer';
1619
+ this._attachConsumerChannelHandlers(this._consumerChannel);
1620
+
1621
+ // Re-register all consumers
1622
+ consumerReRegResult = await this._ensureConsumerChannel();
1623
+ channelState.consumer = true;
1624
+ console.log('[RabbitMQClient] ✓ Consumer channel recreated');
1625
+ } catch (err) {
1626
+ console.error('[RabbitMQClient] Failed to recreate consumer channel:', err.message);
1627
+ this._consumerChannel = null;
1628
+ // Continue - partial success is better than total failure
1629
+ }
1535
1630
 
1536
1631
  // Reset reconnect state
1537
1632
  this._reconnectAttempts = 0;
1538
1633
  this._reconnecting = false;
1539
1634
 
1540
- // CRITICAL: Verify all channels are ready before emitting 'reconnected'
1541
- // This ensures publish/consume operations can proceed immediately after reconnection
1542
- if (!this._channel || this._channel.closed) {
1543
- throw new Error('Publisher channel not ready after reconnection');
1544
- }
1545
- if (!this._queueChannel || this._queueChannel.closed) {
1546
- throw new Error('Queue channel not ready after reconnection');
1547
- }
1548
- if (!this._consumerChannel || this._consumerChannel.closed) {
1549
- throw new Error('Consumer channel not ready after reconnection');
1550
- }
1635
+ // Emit reconnected event with state information
1636
+ // Partial success is acceptable - at least connection is re-established
1637
+ const reconnectState = {
1638
+ channels: channelState,
1639
+ consumers: consumerReRegResult,
1640
+ timestamp: new Date().toISOString()
1641
+ };
1551
1642
 
1552
- console.log('[RabbitMQClient] ✓ Connection-level recovery completed successfully - all channels ready');
1553
- this.emit('reconnected');
1643
+ console.log('[RabbitMQClient] ✓ Connection-level recovery completed', JSON.stringify(reconnectState, null, 2));
1644
+ this.emit('reconnected', reconnectState);
1554
1645
 
1555
1646
  // Flush buffered messages po reconnectu
1647
+ let bufferFlushResult = { inMemory: 0, persistent: 0, flushed: false };
1556
1648
  try {
1557
1649
  const flushResult = await this._publishLayer.flushBuffered();
1558
- this._publishMonitor.trackFlushed(flushResult.inMemory + flushResult.persistent);
1559
- console.log(`[RabbitMQClient] ✓ Flushed ${flushResult.inMemory + flushResult.persistent} buffered messages after reconnection`);
1650
+ bufferFlushResult = {
1651
+ inMemory: flushResult.inMemory || 0,
1652
+ persistent: flushResult.persistent || 0,
1653
+ flushed: true
1654
+ };
1655
+ this._publishMonitor.trackFlushed(bufferFlushResult.inMemory + bufferFlushResult.persistent);
1656
+ console.log(`[RabbitMQClient] ✓ Flushed ${bufferFlushResult.inMemory + bufferFlushResult.persistent} buffered messages after reconnection`);
1560
1657
  } catch (flushErr) {
1561
1658
  console.warn(`[RabbitMQClient] Failed to flush buffered messages after reconnection: ${flushErr.message}`);
1659
+ this.emit('buffer:flush:failed', { error: flushErr.message, bufferSize: this._publishLayer._buffer.size() });
1660
+ }
1661
+
1662
+ // If critical channels failed, log warning but don't throw - connection is re-established
1663
+ if (!channelState.publisher) {
1664
+ console.warn('[RabbitMQClient] ⚠ Publisher channel not ready after reconnection - publish operations may fail');
1665
+ }
1666
+ if (!channelState.queue) {
1667
+ console.warn('[RabbitMQClient] ⚠ Queue channel not ready after reconnection - queue operations may fail');
1668
+ }
1669
+ if (!channelState.consumer) {
1670
+ console.warn('[RabbitMQClient] ⚠ Consumer channel not ready after reconnection - consume operations may fail');
1562
1671
  }
1563
1672
 
1564
1673
  return; // Success - exit reconnection loop
@@ -90,6 +90,11 @@ class RecoveryWorker {
90
90
  // Use queue channel to create queue
91
91
  await this._client._ensureQueueChannel();
92
92
 
93
+ // Guard: ensure queue channel is available after ensure
94
+ if (!this._client._queueChannel || this._client._queueChannel.closed) {
95
+ throw new Error(`Cannot create queue ${queueName}: queue channel is not available (connection may be closed)`);
96
+ }
97
+
93
98
  const queueOptions = options.queueOptions || { durable: this._client._config.durable };
94
99
  await this._client._queueChannel.assertQueue(queueName, queueOptions);
95
100
  }