@onlineapps/mq-client-core 1.0.39 → 1.0.41

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.41",
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,61 @@ 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
+
235
+ /**
236
+ * Check if client is connected
237
+ * @returns {boolean} True if connection exists and is not closed
238
+ */
239
+ isConnected() {
240
+ return !!(this._connection && !this._connection.closed);
241
+ }
242
+
185
243
  /**
186
244
  * Getter for channel - provides compatibility with QueueManager
187
245
  * Returns ConfirmChannel for publish operations
@@ -457,8 +515,8 @@ class RabbitMQClient extends EventEmitter {
457
515
  const timeout = setTimeout(() => {
458
516
  this.removeListener('reconnected', onReconnected);
459
517
  this.removeListener('error', onError);
460
- reject(new Error('Reconnection timeout after 30 seconds'));
461
- }, 30000); // 30 seconds timeout
518
+ reject(new Error(`Reconnection timeout after ${this._reconnectWaitTimeout}ms`));
519
+ }, this._reconnectWaitTimeout);
462
520
 
463
521
  const onReconnected = () => {
464
522
  clearTimeout(timeout);
@@ -619,11 +677,12 @@ class RabbitMQClient extends EventEmitter {
619
677
  * Ensure consumer channel exists and is open
620
678
  * Auto-recreates and re-registers all consumers if closed
621
679
  * @private
622
- * @returns {Promise<void>}
680
+ * @returns {Promise<{reRegistered: number, failed: number}>} - Number of consumers re-registered and failed
623
681
  */
624
682
  async _ensureConsumerChannel() {
625
683
  if (this._consumerChannel && !this._consumerChannel.closed) {
626
- return; // Channel is good
684
+ // Channel is good - return current consumer state
685
+ return { reRegistered: this._activeConsumers.size, failed: 0 };
627
686
  }
628
687
 
629
688
  if (!this._connection || this._connection.closed) {
@@ -631,7 +690,7 @@ class RabbitMQClient extends EventEmitter {
631
690
  // Reconnection logic will handle this - don't throw, just return
632
691
  // The channel will be recreated after reconnection completes
633
692
  console.warn('[RabbitMQClient] [mq-client-core] Cannot recreate consumer channel: connection is closed (reconnection in progress)');
634
- return;
693
+ return { reRegistered: 0, failed: this._activeConsumers.size };
635
694
  }
636
695
 
637
696
  try {
@@ -657,6 +716,9 @@ class RabbitMQClient extends EventEmitter {
657
716
  this._attachConsumerChannelHandlers(this._consumerChannel);
658
717
 
659
718
  // Re-register all active consumers
719
+ let reRegistered = 0;
720
+ let failed = 0;
721
+
660
722
  if (this._activeConsumers.size > 0) {
661
723
  console.log(`[RabbitMQClient] [mq-client-core] Re-registering ${this._activeConsumers.size} consumers...`);
662
724
  for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
@@ -665,18 +727,25 @@ class RabbitMQClient extends EventEmitter {
665
727
  if (consumerInfo.options.queueOptions) {
666
728
  try {
667
729
  await this._ensureQueueChannel();
730
+ if (!this._queueChannel || this._queueChannel.closed) {
731
+ throw new Error('Queue channel is not available (connection may be closed)');
732
+ }
668
733
  this._trackChannelOperation(this._queueChannel, `assertQueue ${queue} (re-register)`);
669
734
  await this._queueChannel.assertQueue(queue, consumerInfo.options.queueOptions);
670
735
  } catch (assertErr) {
671
736
  if (assertErr.code === 404) {
672
737
  console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} does not exist, skipping consumer re-registration`);
673
738
  this._activeConsumers.delete(queue);
739
+ failed++;
740
+ this.emit('consumer:re-registration:failed', { queue, error: 'Queue does not exist', code: 404 });
674
741
  continue;
675
742
  }
676
743
  // 406 error means queue exists with different args - skip this consumer
677
744
  if (assertErr.code === 406) {
678
745
  console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} exists with different args, skipping consumer re-registration`);
679
746
  this._activeConsumers.delete(queue);
747
+ failed++;
748
+ this.emit('consumer:re-registration:failed', { queue, error: 'Queue exists with different arguments', code: 406 });
680
749
  continue;
681
750
  }
682
751
  throw assertErr;
@@ -689,16 +758,21 @@ class RabbitMQClient extends EventEmitter {
689
758
  { noAck: consumerInfo.options.noAck }
690
759
  );
691
760
  consumerInfo.consumerTag = consumeResult.consumerTag;
761
+ reRegistered++;
692
762
  console.log(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
693
763
  } catch (err) {
764
+ failed++;
694
765
  console.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
695
766
  // Remove failed consumer from tracking
696
767
  this._activeConsumers.delete(queue);
768
+ // Emit event for re-registration failure
769
+ this.emit('consumer:re-registration:failed', { queue, error: err.message, code: err.code });
697
770
  }
698
771
  }
699
772
  }
700
773
 
701
- console.log('[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated');
774
+ console.log(`[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated (re-registered: ${reRegistered}, failed: ${failed})`);
775
+ return { reRegistered, failed };
702
776
  } catch (err) {
703
777
  this._consumerChannel = null;
704
778
  throw new Error(`Failed to recreate consumer channel: ${err.message}`);
@@ -1010,6 +1084,13 @@ class RabbitMQClient extends EventEmitter {
1010
1084
  try {
1011
1085
  // Ensure queue channel exists and is open (auto-recreates if closed)
1012
1086
  await this._ensureQueueChannel();
1087
+ if (!this._queueChannel || this._queueChannel.closed) {
1088
+ // Connection is closed and not reconnecting - cannot check queue
1089
+ throw new TransientPublishError(
1090
+ `Cannot check queue ${queue}: queue channel is not available (connection may be closed)`,
1091
+ new Error('Queue channel unavailable')
1092
+ );
1093
+ }
1013
1094
  this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
1014
1095
  await this._queueChannel.checkQueue(queue);
1015
1096
  // Queue exists - proceed to publish
@@ -1037,12 +1118,32 @@ class RabbitMQClient extends EventEmitter {
1037
1118
  // Jasný, hlasitý signál pro infra služby – fronta chybí, je to programátorská chyba
1038
1119
  throw new QueueNotFoundError(queue, true, checkErr);
1039
1120
  }
1121
+ // For non-infrastructure queues, check if queue creation is allowed before auto-creating
1122
+ // Check recovery scope and filter
1123
+ if (this._recoveryWorker && !this._recoveryWorker._queueCreationEnabled) {
1124
+ // Queue creation disabled for this scope (e.g., infrastructure scope)
1125
+ throw new QueueNotFoundError(queue, false, checkErr);
1126
+ }
1127
+
1128
+ // Check queue creation filter if provided
1129
+ if (this._recoveryWorker && this._recoveryWorker._queueCreationFilter) {
1130
+ if (!this._recoveryWorker._queueCreationFilter(queue)) {
1131
+ // Queue creation filtered out - throw QueueNotFoundError so RecoveryWorker can handle it
1132
+ throw new QueueNotFoundError(queue, false, checkErr);
1133
+ }
1134
+ }
1135
+
1040
1136
  // For non-infrastructure queues, allow auto-creation with default options
1041
1137
  const queueOptions = options.queueOptions || { durable: this._config.durable };
1042
1138
  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
1139
  // Channel could have been nulled out by close handlers; ensure it's available
1044
- if (!this._queueChannel) {
1045
- await this._ensureQueueChannel();
1140
+ await this._ensureQueueChannel();
1141
+ if (!this._queueChannel || this._queueChannel.closed) {
1142
+ // Connection is closed and not reconnecting - cannot create queue
1143
+ throw new TransientPublishError(
1144
+ `Cannot create queue ${queue}: queue channel is not available (connection may be closed)`,
1145
+ checkErr
1146
+ );
1046
1147
  }
1047
1148
  this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
1048
1149
  await this._queueChannel.assertQueue(queue, queueOptions);
@@ -1184,23 +1285,11 @@ class RabbitMQClient extends EventEmitter {
1184
1285
  await confirmPromise;
1185
1286
  }
1186
1287
  } 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.
1288
+ // CRITICAL: _publishOnce() je "single attempt" - žádný vnitřní retry
1289
+ // Retry logika je pouze v PublishLayer, aby byl počet pokusů předvídatelný
1290
+ // Pokud channel closed, vyhodíme TransientPublishError a necháme PublishLayer řešit retry
1291
+
1292
+ // Všechny chyby projdou jednotnou klasifikací do Transient/Permanent/QueueNotFound
1204
1293
  const classifiedErr = classifyPublishError(err);
1205
1294
  this.emit('error', classifiedErr);
1206
1295
  throw classifiedErr;
@@ -1505,60 +1594,103 @@ class RabbitMQClient extends EventEmitter {
1505
1594
  }
1506
1595
  });
1507
1596
 
1508
- // Recreate all channels
1597
+ // Recreate all channels with best effort - partial success is better than total failure
1509
1598
  console.log('[RabbitMQClient] Recreating channels...');
1510
1599
 
1600
+ const channelState = { publisher: false, queue: false, consumer: false };
1601
+ let consumerReRegResult = { reRegistered: 0, failed: 0 };
1602
+
1511
1603
  // 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);
1604
+ try {
1605
+ this._channel = await this._connection.createConfirmChannel();
1606
+ this._channel._createdAt = new Date().toISOString();
1607
+ this._channel._closeReason = null;
1608
+ this._channel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1609
+ this._channel._type = 'publisher';
1610
+ this._attachPublisherChannelHandlers(this._channel);
1611
+ channelState.publisher = true;
1612
+ console.log('[RabbitMQClient] ✓ Publisher channel recreated');
1613
+ } catch (err) {
1614
+ console.error('[RabbitMQClient] Failed to recreate publisher channel:', err.message);
1615
+ this._channel = null;
1616
+ // Continue - partial success is better than total failure
1617
+ }
1518
1618
 
1519
1619
  // 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);
1620
+ try {
1621
+ this._queueChannel = await this._connection.createChannel();
1622
+ this._queueChannel._createdAt = new Date().toISOString();
1623
+ this._queueChannel._closeReason = null;
1624
+ this._queueChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1625
+ this._queueChannel._type = 'queue';
1626
+ this._attachQueueChannelHandlers(this._queueChannel);
1627
+ channelState.queue = true;
1628
+ console.log('[RabbitMQClient] ✓ Queue channel recreated');
1629
+ } catch (err) {
1630
+ console.error('[RabbitMQClient] Failed to recreate queue channel:', err.message);
1631
+ this._queueChannel = null;
1632
+ // Continue - partial success is better than total failure
1633
+ }
1526
1634
 
1527
1635
  // 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();
1636
+ try {
1637
+ this._consumerChannel = await this._connection.createChannel();
1638
+ this._consumerChannel._createdAt = new Date().toISOString();
1639
+ this._consumerChannel._closeReason = null;
1640
+ this._consumerChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
1641
+ this._consumerChannel._type = 'consumer';
1642
+ this._attachConsumerChannelHandlers(this._consumerChannel);
1643
+
1644
+ // Re-register all consumers
1645
+ consumerReRegResult = await this._ensureConsumerChannel();
1646
+ channelState.consumer = true;
1647
+ console.log('[RabbitMQClient] ✓ Consumer channel recreated');
1648
+ } catch (err) {
1649
+ console.error('[RabbitMQClient] Failed to recreate consumer channel:', err.message);
1650
+ this._consumerChannel = null;
1651
+ // Continue - partial success is better than total failure
1652
+ }
1535
1653
 
1536
1654
  // Reset reconnect state
1537
1655
  this._reconnectAttempts = 0;
1538
1656
  this._reconnecting = false;
1539
1657
 
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
- }
1658
+ // Emit reconnected event with state information
1659
+ // Partial success is acceptable - at least connection is re-established
1660
+ const reconnectState = {
1661
+ channels: channelState,
1662
+ consumers: consumerReRegResult,
1663
+ timestamp: new Date().toISOString()
1664
+ };
1551
1665
 
1552
- console.log('[RabbitMQClient] ✓ Connection-level recovery completed successfully - all channels ready');
1553
- this.emit('reconnected');
1666
+ console.log('[RabbitMQClient] ✓ Connection-level recovery completed', JSON.stringify(reconnectState, null, 2));
1667
+ this.emit('reconnected', reconnectState);
1554
1668
 
1555
1669
  // Flush buffered messages po reconnectu
1670
+ let bufferFlushResult = { inMemory: 0, persistent: 0, flushed: false };
1556
1671
  try {
1557
1672
  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`);
1673
+ bufferFlushResult = {
1674
+ inMemory: flushResult.inMemory || 0,
1675
+ persistent: flushResult.persistent || 0,
1676
+ flushed: true
1677
+ };
1678
+ this._publishMonitor.trackFlushed(bufferFlushResult.inMemory + bufferFlushResult.persistent);
1679
+ console.log(`[RabbitMQClient] ✓ Flushed ${bufferFlushResult.inMemory + bufferFlushResult.persistent} buffered messages after reconnection`);
1560
1680
  } catch (flushErr) {
1561
1681
  console.warn(`[RabbitMQClient] Failed to flush buffered messages after reconnection: ${flushErr.message}`);
1682
+ this.emit('buffer:flush:failed', { error: flushErr.message, bufferSize: this._publishLayer._buffer.size() });
1683
+ }
1684
+
1685
+ // If critical channels failed, log warning but don't throw - connection is re-established
1686
+ if (!channelState.publisher) {
1687
+ console.warn('[RabbitMQClient] ⚠ Publisher channel not ready after reconnection - publish operations may fail');
1688
+ }
1689
+ if (!channelState.queue) {
1690
+ console.warn('[RabbitMQClient] ⚠ Queue channel not ready after reconnection - queue operations may fail');
1691
+ }
1692
+ if (!channelState.consumer) {
1693
+ console.warn('[RabbitMQClient] ⚠ Consumer channel not ready after reconnection - consume operations may fail');
1562
1694
  }
1563
1695
 
1564
1696
  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
  }