@onlineapps/mq-client-core 1.0.38 → 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.38",
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
@@ -1037,11 +1110,20 @@ class RabbitMQClient extends EventEmitter {
1037
1110
  // Jasný, hlasitý signál pro infra služby – fronta chybí, je to programátorská chyba
1038
1111
  throw new QueueNotFoundError(queue, true, checkErr);
1039
1112
  }
1040
- // For non-infrastructure queues, allow auto-creation with default options
1041
- const queueOptions = options.queueOptions || { durable: this._config.durable };
1042
- 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
- this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
1044
- await this._queueChannel.assertQueue(queue, queueOptions);
1113
+ // For non-infrastructure queues, allow auto-creation with default options
1114
+ const queueOptions = options.queueOptions || { durable: this._config.durable };
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.`);
1116
+ // Channel could have been nulled out by close handlers; ensure it's available
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
+ );
1124
+ }
1125
+ this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
1126
+ await this._queueChannel.assertQueue(queue, queueOptions);
1045
1127
  } else {
1046
1128
  // Other error - rethrow
1047
1129
  throw checkErr;
@@ -1180,23 +1262,11 @@ class RabbitMQClient extends EventEmitter {
1180
1262
  await confirmPromise;
1181
1263
  }
1182
1264
  } catch (err) {
1183
- // Pokud je kanál zavřený, stále se pokusíme o jedno interní obnovení a retry,
1184
- // aby byl publish co nejodolnější i předtím, než vstoupí do vyšší retry/recovery vrstvy.
1185
- if (err && err.message && (err.message.includes('Channel closed') || err.message.includes('channel is closed') || this._channel?.closed)) {
1186
- console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish, recreating and retrying once...');
1187
- try {
1188
- await this._ensurePublisherChannel();
1189
- // Retry publish once (může znovu vyhodit – už dojde do retry/recovery vrstvy)
1190
- return await this.publish(queue, buffer, options);
1191
- } catch (retryErr) {
1192
- console.error('[RabbitMQClient] [mq-client-core] [PUBLISH] Retry after channel recreation failed:', retryErr.message);
1193
- const classifiedRetryErr = classifyPublishError(retryErr);
1194
- this.emit('error', classifiedRetryErr);
1195
- throw classifiedRetryErr;
1196
- }
1197
- }
1198
-
1199
- // 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
1200
1270
  const classifiedErr = classifyPublishError(err);
1201
1271
  this.emit('error', classifiedErr);
1202
1272
  throw classifiedErr;
@@ -1501,60 +1571,103 @@ class RabbitMQClient extends EventEmitter {
1501
1571
  }
1502
1572
  });
1503
1573
 
1504
- // Recreate all channels
1574
+ // Recreate all channels with best effort - partial success is better than total failure
1505
1575
  console.log('[RabbitMQClient] Recreating channels...');
1506
1576
 
1577
+ const channelState = { publisher: false, queue: false, consumer: false };
1578
+ let consumerReRegResult = { reRegistered: 0, failed: 0 };
1579
+
1507
1580
  // Recreate publisher channel
1508
- this._channel = await this._connection.createConfirmChannel();
1509
- this._channel._createdAt = new Date().toISOString();
1510
- this._channel._closeReason = null;
1511
- this._channel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1512
- this._channel._type = 'publisher';
1513
- 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
+ }
1514
1595
 
1515
1596
  // Recreate queue channel
1516
- this._queueChannel = await this._connection.createChannel();
1517
- this._queueChannel._createdAt = new Date().toISOString();
1518
- this._queueChannel._closeReason = null;
1519
- this._queueChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1520
- this._queueChannel._type = 'queue';
1521
- 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
+ }
1522
1611
 
1523
1612
  // Recreate consumer channel and re-register all consumers
1524
- this._consumerChannel = await this._connection.createChannel();
1525
- this._consumerChannel._createdAt = new Date().toISOString();
1526
- this._consumerChannel._closeReason = null;
1527
- // IMPORTANT: Don't re-register consumers here - _ensureConsumerChannel() will do it
1528
- // This prevents duplicate re-registration (once in _reconnectWithBackoff, once in _ensureConsumerChannel)
1529
- // Just ensure consumer channel is created - it will automatically re-register all active consumers
1530
- 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
+ }
1531
1630
 
1532
1631
  // Reset reconnect state
1533
1632
  this._reconnectAttempts = 0;
1534
1633
  this._reconnecting = false;
1535
1634
 
1536
- // CRITICAL: Verify all channels are ready before emitting 'reconnected'
1537
- // This ensures publish/consume operations can proceed immediately after reconnection
1538
- if (!this._channel || this._channel.closed) {
1539
- throw new Error('Publisher channel not ready after reconnection');
1540
- }
1541
- if (!this._queueChannel || this._queueChannel.closed) {
1542
- throw new Error('Queue channel not ready after reconnection');
1543
- }
1544
- if (!this._consumerChannel || this._consumerChannel.closed) {
1545
- throw new Error('Consumer channel not ready after reconnection');
1546
- }
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
+ };
1547
1642
 
1548
- console.log('[RabbitMQClient] ✓ Connection-level recovery completed successfully - all channels ready');
1549
- this.emit('reconnected');
1643
+ console.log('[RabbitMQClient] ✓ Connection-level recovery completed', JSON.stringify(reconnectState, null, 2));
1644
+ this.emit('reconnected', reconnectState);
1550
1645
 
1551
1646
  // Flush buffered messages po reconnectu
1647
+ let bufferFlushResult = { inMemory: 0, persistent: 0, flushed: false };
1552
1648
  try {
1553
1649
  const flushResult = await this._publishLayer.flushBuffered();
1554
- this._publishMonitor.trackFlushed(flushResult.inMemory + flushResult.persistent);
1555
- 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`);
1556
1657
  } catch (flushErr) {
1557
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');
1558
1671
  }
1559
1672
 
1560
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
  }