@onlineapps/mq-client-core 1.0.33 → 1.0.35

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.33",
3
+ "version": "1.0.35",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -35,6 +35,28 @@ class RabbitMQClient extends EventEmitter {
35
35
  this._channel = null; // ConfirmChannel for publish operations
36
36
  this._queueChannel = null; // Regular channel for queue operations (assertQueue, checkQueue)
37
37
  this._consumerChannel = null; // Dedicated channel for consume operations
38
+
39
+ // Track active consumers for re-registration after channel recreation
40
+ this._activeConsumers = new Map(); // queue -> { handler, options, consumerTag }
41
+
42
+ // Auto-reconnect flags
43
+ this._reconnecting = false;
44
+ this._reconnectAttempts = 0;
45
+
46
+ // Channel close hooks - called when channel closes with detailed information
47
+ this._channelCloseHooks = []; // Array of { type: 'publisher'|'queue'|'consumer', callback: (details) => void }
48
+
49
+ // Health monitoring
50
+ this._healthCheckInterval = null;
51
+ this._healthCheckIntervalMs = this._config.healthCheckInterval || 30000; // Default 30s
52
+ this._healthCheckEnabled = this._config.healthCheckEnabled !== false;
53
+
54
+ // Health reporting callbacks
55
+ this._healthReportCallback = this._config.healthReportCallback || null; // (health) => Promise<void>
56
+ this._healthCriticalCallback = this._config.healthCriticalCallback || null; // (health) => Promise<void>
57
+ this._criticalHealthShutdown = this._config.criticalHealthShutdown !== false; // Default: true - shutdown on critical health
58
+ this._criticalHealthShutdownDelay = this._config.criticalHealthShutdownDelay || 60000; // Default: 60s delay before shutdown
59
+ this._criticalHealthStartTime = null; // Track when critical health started
38
60
  }
39
61
 
40
62
  /**
@@ -59,6 +81,50 @@ class RabbitMQClient extends EventEmitter {
59
81
  get queueChannel() {
60
82
  return this._queueChannel || this._channel; // Fallback to main channel if queueChannel not available
61
83
  }
84
+
85
+ /**
86
+ * Register a hook to be called when a channel closes
87
+ * @param {string} type - Channel type: 'publisher', 'queue', 'consumer', or 'all'
88
+ * @param {Function} callback - Callback function receiving close details: { type, channel, error, reason, timestamp, stack }
89
+ */
90
+ onChannelClose(type, callback) {
91
+ this._channelCloseHooks.push({ type, callback });
92
+ }
93
+
94
+ /**
95
+ * Call all registered channel close hooks
96
+ * @private
97
+ * @param {string} channelType - 'publisher', 'queue', or 'consumer'
98
+ * @param {Object} channel - The closed channel
99
+ * @param {Error} error - Error that caused closure (if any)
100
+ * @param {string} reason - Human-readable reason
101
+ */
102
+ _callChannelCloseHooks(channelType, channel, error, reason) {
103
+ const details = {
104
+ type: channelType,
105
+ channel: channel,
106
+ error: error,
107
+ reason: reason,
108
+ timestamp: new Date().toISOString(),
109
+ stack: error?.stack || new Error().stack,
110
+ connectionState: this._connection ? (this._connection.closed ? 'closed' : 'open') : 'null',
111
+ channelCreatedAt: channel?._createdAt || 'unknown',
112
+ lastOperation: channel?._lastOperation || 'unknown'
113
+ };
114
+
115
+ for (const hook of this._channelCloseHooks) {
116
+ if (hook.type === 'all' || hook.type === channelType) {
117
+ try {
118
+ hook.callback(details);
119
+ } catch (hookErr) {
120
+ console.error(`[RabbitMQClient] [mq-client-core] Channel close hook error:`, hookErr.message);
121
+ }
122
+ }
123
+ }
124
+
125
+ // Emit event for external listeners
126
+ this.emit('channel:close', details);
127
+ }
62
128
 
63
129
  /**
64
130
  * Connects to RabbitMQ server and creates a confirm channel.
@@ -94,15 +160,39 @@ class RabbitMQClient extends EventEmitter {
94
160
 
95
161
  // Use ConfirmChannel to enable publisher confirms for publish operations
96
162
  this._channel = await this._connection.createConfirmChannel();
163
+
164
+ // Track channel metadata for debugging
165
+ this._channel._createdAt = new Date().toISOString();
166
+ this._channel._closeReason = null;
167
+ this._channel._lastOperation = 'Initial creation';
168
+ this._channel._type = 'publisher';
169
+
97
170
  // Enable publisher confirms explicitly
98
- this._channel.on('error', (err) => {
99
- console.error('[RabbitMQClient] [mq-client-core] Publisher channel error:', err.message);
171
+ this._channel.on('error', async (err) => {
172
+ const reason = `Publisher channel error: ${err.message} (code: ${err.code || 'unknown'})`;
173
+ this._channel._closeReason = reason;
174
+ console.error(`[RabbitMQClient] [mq-client-core] ${reason}`);
175
+
176
+ // Call close hooks with detailed information
177
+ this._callChannelCloseHooks('publisher', this._channel, err, reason);
178
+
179
+ // Auto-recreate channel if connection is still alive
180
+ await this._ensurePublisherChannel();
100
181
  this.emit('error', err);
101
182
  });
102
- this._channel.on('close', () => {
103
- console.warn('[RabbitMQClient] [mq-client-core] Publisher channel closed');
104
- // Emit a channel close error
105
- this.emit('error', new Error('RabbitMQ channel closed unexpectedly'));
183
+ this._channel.on('close', async () => {
184
+ const reason = this._channel._closeReason || 'Publisher channel closed unexpectedly';
185
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate on next publish`);
186
+
187
+ // Call close hooks with detailed information
188
+ this._callChannelCloseHooks('publisher', this._channel, null, reason);
189
+
190
+ // Mark channel as null - will be recreated on next publish
191
+ const closedChannel = this._channel;
192
+ this._channel = null;
193
+
194
+ // Try to recreate immediately if connection is alive
195
+ await this._ensurePublisherChannel();
106
196
  });
107
197
  // Set up publisher confirms callback
108
198
  this._channel.on('drain', () => {
@@ -112,25 +202,80 @@ class RabbitMQClient extends EventEmitter {
112
202
  // Create a separate regular channel for queue operations (assertQueue, checkQueue)
113
203
  // This avoids RPC reply queue issues with ConfirmChannel.assertQueue()
114
204
  this._queueChannel = await this._connection.createChannel();
115
- this._queueChannel.on('error', (err) => {
116
- // Log but don't emit - queue channel errors are less critical
117
- console.warn('[RabbitMQClient] Queue channel error:', err.message);
205
+
206
+ // Track channel metadata for debugging
207
+ this._queueChannel._createdAt = new Date().toISOString();
208
+ this._queueChannel._closeReason = null;
209
+ this._queueChannel._lastOperation = 'Initial creation';
210
+ this._queueChannel._type = 'queue';
211
+
212
+ this._queueChannel.on('error', async (err) => {
213
+ const reason = `Queue channel error: ${err.message} (code: ${err.code || 'unknown'})`;
214
+ this._queueChannel._closeReason = reason;
215
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason}`);
216
+
217
+ // Call close hooks with detailed information
218
+ this._callChannelCloseHooks('queue', this._queueChannel, err, reason);
219
+
220
+ // Auto-recreate channel if connection is still alive
221
+ await this._ensureQueueChannel();
118
222
  });
119
- this._queueChannel.on('close', () => {
120
- console.warn('[RabbitMQClient] Queue channel closed');
223
+ this._queueChannel.on('close', async () => {
224
+ const reason = this._queueChannel._closeReason || 'Queue channel closed unexpectedly';
225
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate on next operation`);
226
+
227
+ // Call close hooks with detailed information
228
+ this._callChannelCloseHooks('queue', this._queueChannel, null, reason);
229
+
230
+ // Mark channel as null - will be recreated on next queue operation
231
+ const closedChannel = this._queueChannel;
232
+ this._queueChannel = null;
233
+
234
+ // Try to recreate immediately if connection is alive
235
+ await this._ensureQueueChannel();
121
236
  });
122
237
 
123
238
  // Create a dedicated channel for consume operations
124
239
  // This prevents channel conflicts between publish (ConfirmChannel) and consume operations
125
240
  this._consumerChannel = await this._connection.createChannel();
126
- this._consumerChannel.on('error', (err) => {
127
- console.warn('[RabbitMQClient] Consumer channel error:', err.message);
241
+
242
+ // Track channel metadata for debugging
243
+ this._consumerChannel._createdAt = new Date().toISOString();
244
+ this._consumerChannel._closeReason = null;
245
+ this._consumerChannel._lastOperation = 'Initial creation';
246
+ this._consumerChannel._type = 'consumer';
247
+
248
+ this._consumerChannel.on('error', async (err) => {
249
+ const reason = `Consumer channel error: ${err.message} (code: ${err.code || 'unknown'})`;
250
+ this._consumerChannel._closeReason = reason;
251
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason}`);
252
+
253
+ // Call close hooks with detailed information
254
+ this._callChannelCloseHooks('consumer', this._consumerChannel, err, reason);
255
+
256
+ // Auto-recreate channel and re-register consumers
257
+ await this._ensureConsumerChannel();
128
258
  this.emit('error', err);
129
259
  });
130
- this._consumerChannel.on('close', () => {
131
- console.warn('[RabbitMQClient] Consumer channel closed');
132
- this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
260
+ this._consumerChannel.on('close', async () => {
261
+ const reason = this._consumerChannel._closeReason || 'Consumer channel closed unexpectedly';
262
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate and re-register consumers`);
263
+
264
+ // Call close hooks with detailed information
265
+ this._callChannelCloseHooks('consumer', this._consumerChannel, null, reason);
266
+
267
+ // Mark channel as null - will be recreated and consumers re-registered
268
+ const closedChannel = this._consumerChannel;
269
+ this._consumerChannel = null;
270
+
271
+ // Try to recreate and re-register consumers immediately if connection is alive
272
+ await this._ensureConsumerChannel();
133
273
  });
274
+
275
+ // Start health monitoring if enabled
276
+ if (this._healthCheckEnabled) {
277
+ this._startHealthMonitoring();
278
+ }
134
279
  } catch (err) {
135
280
  // Cleanup partially created resources
136
281
  if (this._connection) {
@@ -145,11 +290,446 @@ class RabbitMQClient extends EventEmitter {
145
290
  }
146
291
  }
147
292
 
293
+ /**
294
+ * Ensure publisher channel exists and is open
295
+ * Auto-recreates if closed
296
+ * @private
297
+ * @returns {Promise<void>}
298
+ */
299
+ async _ensurePublisherChannel() {
300
+ if (this._channel && !this._channel.closed) {
301
+ return; // Channel is good
302
+ }
303
+
304
+ if (!this._connection || this._connection.closed) {
305
+ throw new Error('Cannot recreate publisher channel: connection is closed');
306
+ }
307
+
308
+ try {
309
+ // Close old channel if exists
310
+ if (this._channel) {
311
+ try {
312
+ await this._channel.close();
313
+ } catch (_) {
314
+ // Ignore errors when closing already-closed channel
315
+ }
316
+ }
317
+
318
+ // Create new ConfirmChannel
319
+ this._channel = await this._connection.createConfirmChannel();
320
+
321
+ // Track channel metadata for debugging
322
+ this._channel._createdAt = new Date().toISOString();
323
+ this._channel._closeReason = null;
324
+ this._channel._lastOperation = 'Recreated via _ensurePublisherChannel()';
325
+ this._channel._type = 'publisher';
326
+
327
+ this._channel.on('error', async (err) => {
328
+ const reason = `Publisher channel error: ${err.message} (code: ${err.code || 'unknown'})`;
329
+ this._channel._closeReason = reason;
330
+ console.error(`[RabbitMQClient] [mq-client-core] ${reason}`);
331
+
332
+ // Call close hooks
333
+ this._callChannelCloseHooks('publisher', this._channel, err, reason);
334
+
335
+ await this._ensurePublisherChannel();
336
+ this.emit('error', err);
337
+ });
338
+ this._channel.on('close', async () => {
339
+ const reason = this._channel._closeReason || 'Publisher channel closed unexpectedly';
340
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate on next publish`);
341
+
342
+ // Call close hooks
343
+ this._callChannelCloseHooks('publisher', this._channel, null, reason);
344
+
345
+ const closedChannel = this._channel;
346
+ this._channel = null;
347
+ await this._ensurePublisherChannel();
348
+ });
349
+ this._channel.on('drain', () => {
350
+ console.log('[RabbitMQClient] [mq-client-core] Publisher channel drained');
351
+ });
352
+
353
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Publisher channel recreated');
354
+ } catch (err) {
355
+ this._channel = null;
356
+ throw new Error(`Failed to recreate publisher channel: ${err.message}`);
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Ensure queue channel exists and is open
362
+ * Auto-recreates if closed
363
+ * @private
364
+ * @returns {Promise<void>}
365
+ */
366
+ async _ensureQueueChannel() {
367
+ if (this._queueChannel && !this._queueChannel.closed) {
368
+ return; // Channel is good
369
+ }
370
+
371
+ if (!this._connection || this._connection.closed) {
372
+ throw new Error('Cannot recreate queue channel: connection is closed');
373
+ }
374
+
375
+ try {
376
+ // Close old channel if exists
377
+ if (this._queueChannel) {
378
+ try {
379
+ await this._queueChannel.close();
380
+ } catch (_) {
381
+ // Ignore errors when closing already-closed channel
382
+ }
383
+ }
384
+
385
+ // Create new regular channel
386
+ this._queueChannel = await this._connection.createChannel();
387
+
388
+ // Track channel metadata for debugging
389
+ this._queueChannel._createdAt = new Date().toISOString();
390
+ this._queueChannel._closeReason = null;
391
+ this._queueChannel._lastOperation = 'Recreated via _ensureQueueChannel()';
392
+ this._queueChannel._type = 'queue';
393
+
394
+ this._queueChannel.on('error', async (err) => {
395
+ const reason = `Queue channel error: ${err.message} (code: ${err.code || 'unknown'})`;
396
+ this._queueChannel._closeReason = reason;
397
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason}`);
398
+
399
+ // Call close hooks
400
+ this._callChannelCloseHooks('queue', this._queueChannel, err, reason);
401
+
402
+ await this._ensureQueueChannel();
403
+ });
404
+ this._queueChannel.on('close', async () => {
405
+ const reason = this._queueChannel._closeReason || 'Queue channel closed unexpectedly';
406
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate on next operation`);
407
+
408
+ // Call close hooks
409
+ this._callChannelCloseHooks('queue', this._queueChannel, null, reason);
410
+
411
+ const closedChannel = this._queueChannel;
412
+ this._queueChannel = null;
413
+ await this._ensureQueueChannel();
414
+ });
415
+
416
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Queue channel recreated');
417
+ } catch (err) {
418
+ this._queueChannel = null;
419
+ throw new Error(`Failed to recreate queue channel: ${err.message}`);
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Ensure consumer channel exists and is open
425
+ * Auto-recreates and re-registers all consumers if closed
426
+ * @private
427
+ * @returns {Promise<void>}
428
+ */
429
+ async _ensureConsumerChannel() {
430
+ if (this._consumerChannel && !this._consumerChannel.closed) {
431
+ return; // Channel is good
432
+ }
433
+
434
+ if (!this._connection || this._connection.closed) {
435
+ throw new Error('Cannot recreate consumer channel: connection is closed');
436
+ }
437
+
438
+ try {
439
+ // Close old channel if exists
440
+ if (this._consumerChannel) {
441
+ try {
442
+ await this._consumerChannel.close();
443
+ } catch (_) {
444
+ // Ignore errors when closing already-closed channel
445
+ }
446
+ }
447
+
448
+ // Create new regular channel
449
+ this._consumerChannel = await this._connection.createChannel();
450
+
451
+ // Track channel metadata for debugging
452
+ this._consumerChannel._createdAt = new Date().toISOString();
453
+ this._consumerChannel._closeReason = null;
454
+ this._consumerChannel._lastOperation = 'Recreated via _ensureConsumerChannel()';
455
+ this._consumerChannel._type = 'consumer';
456
+
457
+ this._consumerChannel.on('error', async (err) => {
458
+ const reason = `Consumer channel error: ${err.message} (code: ${err.code || 'unknown'})`;
459
+ this._consumerChannel._closeReason = reason;
460
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason}`);
461
+
462
+ // Call close hooks
463
+ this._callChannelCloseHooks('consumer', this._consumerChannel, err, reason);
464
+
465
+ await this._ensureConsumerChannel();
466
+ this.emit('error', err);
467
+ });
468
+ this._consumerChannel.on('close', async () => {
469
+ const reason = this._consumerChannel._closeReason || 'Consumer channel closed unexpectedly';
470
+ console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate and re-register consumers`);
471
+
472
+ // Call close hooks
473
+ this._callChannelCloseHooks('consumer', this._consumerChannel, null, reason);
474
+
475
+ const closedChannel = this._consumerChannel;
476
+ this._consumerChannel = null;
477
+ await this._ensureConsumerChannel();
478
+ });
479
+
480
+ // Re-register all active consumers
481
+ if (this._activeConsumers.size > 0) {
482
+ console.log(`[RabbitMQClient] [mq-client-core] Re-registering ${this._activeConsumers.size} consumers...`);
483
+ for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
484
+ try {
485
+ // Re-assert queue with correct options before consuming (may have been deleted)
486
+ if (consumerInfo.options.queueOptions) {
487
+ try {
488
+ await this._ensureQueueChannel();
489
+ this._trackChannelOperation(this._queueChannel, `assertQueue ${queue} (re-register)`);
490
+ await this._queueChannel.assertQueue(queue, consumerInfo.options.queueOptions);
491
+ } catch (assertErr) {
492
+ if (assertErr.code === 404) {
493
+ console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} does not exist, skipping consumer re-registration`);
494
+ this._activeConsumers.delete(queue);
495
+ continue;
496
+ }
497
+ // 406 error means queue exists with different args - skip this consumer
498
+ if (assertErr.code === 406) {
499
+ console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} exists with different args, skipping consumer re-registration`);
500
+ this._activeConsumers.delete(queue);
501
+ continue;
502
+ }
503
+ throw assertErr;
504
+ }
505
+ }
506
+
507
+ const consumeResult = await this._consumerChannel.consume(
508
+ queue,
509
+ consumerInfo.handler,
510
+ { noAck: consumerInfo.options.noAck }
511
+ );
512
+ consumerInfo.consumerTag = consumeResult.consumerTag;
513
+ console.log(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
514
+ } catch (err) {
515
+ console.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
516
+ // Remove failed consumer from tracking
517
+ this._activeConsumers.delete(queue);
518
+ }
519
+ }
520
+ }
521
+
522
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated');
523
+ } catch (err) {
524
+ this._consumerChannel = null;
525
+ throw new Error(`Failed to recreate consumer channel: ${err.message}`);
526
+ }
527
+ }
528
+
148
529
  /**
149
530
  * Disconnects: closes channel and connection.
150
531
  * @returns {Promise<void>}
151
532
  */
533
+ /**
534
+ * Start health monitoring - periodically checks channels, queues, and consumers
535
+ * @private
536
+ */
537
+ _startHealthMonitoring() {
538
+ if (this._healthCheckInterval) {
539
+ return; // Already started
540
+ }
541
+
542
+ this._healthCheckInterval = setInterval(async () => {
543
+ try {
544
+ await this._performHealthCheck();
545
+ } catch (err) {
546
+ console.error(`[RabbitMQClient] [mq-client-core] Health check error:`, err.message);
547
+ // Emit event for external monitoring
548
+ this.emit('health:check:error', err);
549
+ }
550
+ }, this._healthCheckIntervalMs);
551
+
552
+ console.log(`[RabbitMQClient] [mq-client-core] Health monitoring started (interval: ${this._healthCheckIntervalMs}ms)`);
553
+ }
554
+
555
+ /**
556
+ * Stop health monitoring
557
+ * @private
558
+ */
559
+ _stopHealthMonitoring() {
560
+ if (this._healthCheckInterval) {
561
+ clearInterval(this._healthCheckInterval);
562
+ this._healthCheckInterval = null;
563
+ console.log('[RabbitMQClient] [mq-client-core] Health monitoring stopped');
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Perform health check - verify channels, queues, and consumers
569
+ * @private
570
+ * @returns {Promise<Object>} Health check results
571
+ */
572
+ async _performHealthCheck() {
573
+ const health = {
574
+ timestamp: new Date().toISOString(),
575
+ connection: {
576
+ exists: !!this._connection,
577
+ closed: this._connection ? this._connection.closed : true
578
+ },
579
+ channels: {
580
+ publisher: {
581
+ exists: !!this._channel,
582
+ closed: this._channel ? this._channel.closed : true
583
+ },
584
+ queue: {
585
+ exists: !!this._queueChannel,
586
+ closed: this._queueChannel ? this._queueChannel.closed : true
587
+ },
588
+ consumer: {
589
+ exists: !!this._consumerChannel,
590
+ closed: this._consumerChannel ? this._consumerChannel.closed : true
591
+ }
592
+ },
593
+ consumers: {
594
+ tracked: this._activeConsumers.size,
595
+ active: 0,
596
+ inactive: []
597
+ },
598
+ queues: {
599
+ checked: 0,
600
+ missing: [],
601
+ noConsumer: []
602
+ },
603
+ healthy: true,
604
+ issues: []
605
+ };
606
+
607
+ // Check connection
608
+ if (!health.connection.exists || health.connection.closed) {
609
+ health.healthy = false;
610
+ health.issues.push('Connection is closed or missing');
611
+ }
612
+
613
+ // Check channels
614
+ if (health.channels.publisher.closed || !health.channels.publisher.exists) {
615
+ health.healthy = false;
616
+ health.issues.push('Publisher channel is closed or missing');
617
+ }
618
+ if (health.channels.queue.closed || !health.channels.queue.exists) {
619
+ health.healthy = false;
620
+ health.issues.push('Queue channel is closed or missing');
621
+ }
622
+ if (health.channels.consumer.closed || !health.channels.consumer.exists) {
623
+ health.healthy = false;
624
+ health.issues.push('Consumer channel is closed or missing');
625
+ }
626
+
627
+ // Check tracked consumers - verify queues exist and have active consumers
628
+ if (this._connection && !this._connection.closed && this._queueChannel && !this._queueChannel.closed) {
629
+ for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
630
+ health.queues.checked++;
631
+ try {
632
+ // Check if queue exists
633
+ this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
634
+ const queueInfo = await this._queueChannel.checkQueue(queue);
635
+ if (queueInfo) {
636
+ // Queue exists - check if consumer is active
637
+ // Note: RabbitMQ doesn't provide direct API to check if consumer is active
638
+ // We can only verify that consumerTag is set
639
+ if (consumerInfo.consumerTag) {
640
+ health.consumers.active++;
641
+ } else {
642
+ health.consumers.inactive.push(queue);
643
+ health.queues.noConsumer.push(queue);
644
+ health.healthy = false;
645
+ health.issues.push(`Queue ${queue} has no active consumer (consumerTag missing)`);
646
+ }
647
+ } else {
648
+ health.queues.missing.push(queue);
649
+ health.healthy = false;
650
+ health.issues.push(`Queue ${queue} does not exist`);
651
+ }
652
+ } catch (err) {
653
+ if (err.code === 404) {
654
+ health.queues.missing.push(queue);
655
+ health.healthy = false;
656
+ health.issues.push(`Queue ${queue} does not exist (404)`);
657
+ } else {
658
+ health.healthy = false;
659
+ health.issues.push(`Error checking queue ${queue}: ${err.message}`);
660
+ }
661
+ }
662
+ }
663
+ }
664
+
665
+ // Emit health check result
666
+ this.emit('health:check', health);
667
+
668
+ // If unhealthy, log and potentially trigger shutdown
669
+ if (!health.healthy) {
670
+ console.error(`[RabbitMQClient] [mq-client-core] Health check FAILED:`, health.issues);
671
+ console.error(`[RabbitMQClient] [mq-client-core] Health details:`, JSON.stringify(health, null, 2));
672
+
673
+ // Track critical health start time
674
+ if (!this._criticalHealthStartTime) {
675
+ this._criticalHealthStartTime = Date.now();
676
+ console.error(`[RabbitMQClient] [mq-client-core] Critical health detected at ${new Date(this._criticalHealthStartTime).toISOString()}`);
677
+ }
678
+
679
+ // Report to monitoring if callback provided
680
+ if (this._healthReportCallback) {
681
+ try {
682
+ await this._healthReportCallback(health);
683
+ } catch (reportErr) {
684
+ console.error(`[RabbitMQClient] [mq-client-core] Health report callback failed:`, reportErr.message);
685
+ }
686
+ }
687
+
688
+ // Call critical health callback if provided
689
+ if (this._healthCriticalCallback) {
690
+ try {
691
+ await this._healthCriticalCallback(health);
692
+ } catch (criticalErr) {
693
+ console.error(`[RabbitMQClient] [mq-client-core] Critical health callback failed:`, criticalErr.message);
694
+ }
695
+ }
696
+
697
+ // Emit critical health event
698
+ this.emit('health:critical', health);
699
+
700
+ // Check if we should shutdown after delay
701
+ if (this._criticalHealthShutdown) {
702
+ const timeSinceCritical = Date.now() - this._criticalHealthStartTime;
703
+ if (timeSinceCritical >= this._criticalHealthShutdownDelay) {
704
+ console.error(`[RabbitMQClient] [mq-client-core] Critical health persisted for ${timeSinceCritical}ms (threshold: ${this._criticalHealthShutdownDelay}ms) - triggering shutdown`);
705
+ this.emit('health:shutdown', health);
706
+ // Note: Actual shutdown should be handled by the application using this event
707
+ } else {
708
+ const remaining = this._criticalHealthShutdownDelay - timeSinceCritical;
709
+ console.warn(`[RabbitMQClient] [mq-client-core] Critical health will trigger shutdown in ${remaining}ms if not resolved`);
710
+ }
711
+ }
712
+ } else {
713
+ // Health is OK - reset critical health tracking
714
+ if (this._criticalHealthStartTime) {
715
+ const duration = Date.now() - this._criticalHealthStartTime;
716
+ console.log(`[RabbitMQClient] [mq-client-core] Health recovered after ${duration}ms of critical state`);
717
+ this._criticalHealthStartTime = null;
718
+ }
719
+
720
+ console.log(`[RabbitMQClient] [mq-client-core] Health check OK: ${health.consumers.active}/${health.consumers.tracked} consumers active, ${health.queues.checked} queues checked`);
721
+ }
722
+
723
+ return health;
724
+ }
725
+
152
726
  async disconnect() {
727
+ // Stop health monitoring
728
+ this._stopHealthMonitoring();
729
+
730
+ // Clear active consumers tracking
731
+ this._activeConsumers.clear();
732
+
153
733
  try {
154
734
  if (this._consumerChannel) {
155
735
  await this._consumerChannel.close();
@@ -194,15 +774,14 @@ class RabbitMQClient extends EventEmitter {
194
774
  * @throws {Error} If publish fails or channel is not available.
195
775
  */
196
776
  async publish(queue, buffer, options = {}) {
197
- // Check channel state before publish
198
- if (!this._channel || this._channel.closed) {
199
- const errorMsg = `Cannot publish to queue "${queue}": channel is not initialized or closed (channel: ${!!this._channel}, closed: ${this._channel?.closed})`;
200
- console.error('[RabbitMQClient] [mq-client-core] [PUBLISH]', errorMsg);
201
- throw new Error(errorMsg);
202
- }
777
+ // Ensure publisher channel exists and is open (auto-recreates if closed)
778
+ await this._ensurePublisherChannel();
779
+
780
+ // Track operation for debugging
781
+ this._trackChannelOperation(this._channel, `publish to ${queue}`);
203
782
 
204
783
  // Log publish attempt for debugging
205
- console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Attempting to publish to queue "${queue}" (channel open: ${!this._channel.closed})`);
784
+ console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Attempting to publish to queue "${queue}"`);
206
785
 
207
786
  const exchange = this._config.exchange || '';
208
787
  const routingKey = options.routingKey || queue;
@@ -217,21 +796,9 @@ class RabbitMQClient extends EventEmitter {
217
796
  // If queue doesn't exist (404), we should NOT auto-create it - infrastructure queues must be created explicitly
218
797
  // This prevents creating queues with wrong arguments (no TTL) which causes 406 errors later
219
798
  try {
220
- // Check queueChannel state
221
- if (!this._queueChannel || this._queueChannel.closed) {
222
- // Recreate queueChannel if closed
223
- if (this._connection && !this._connection.closed) {
224
- this._queueChannel = await this._connection.createChannel();
225
- this._queueChannel.on('error', (err) => {
226
- console.warn('[RabbitMQClient] Queue channel error:', err.message);
227
- });
228
- this._queueChannel.on('close', () => {
229
- console.warn('[RabbitMQClient] Queue channel closed');
230
- });
231
- } else {
232
- throw new Error('Cannot publish: connection is closed');
233
- }
234
- }
799
+ // Ensure queue channel exists and is open (auto-recreates if closed)
800
+ await this._ensureQueueChannel();
801
+ this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
235
802
  await this._queueChannel.checkQueue(queue);
236
803
  // Queue exists - proceed to publish
237
804
  } catch (checkErr) {
@@ -259,6 +826,7 @@ class RabbitMQClient extends EventEmitter {
259
826
  // For non-infrastructure queues, allow auto-creation with default options
260
827
  const queueOptions = options.queueOptions || { durable: this._config.durable };
261
828
  console.warn(`[RabbitMQClient] [mq-client-core] [PUBLISH] Auto-creating non-infrastructure queue ${queue} with default options (no TTL). This should be avoided for production.`);
829
+ this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
262
830
  await this._queueChannel.assertQueue(queue, queueOptions);
263
831
  } else {
264
832
  // Other error - rethrow
@@ -266,23 +834,13 @@ class RabbitMQClient extends EventEmitter {
266
834
  }
267
835
  }
268
836
  // Publish to queue using ConfirmChannel (for publisher confirms)
269
- // Check channel state again before sendToQueue
270
- if (this._channel.closed) {
271
- throw new Error(`Cannot publish to queue "${queue}": channel closed during operation (after queue check)`);
272
- }
273
-
837
+ // Channel is guaranteed to be open (ensured above)
274
838
  console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Sending message to queue "${queue}" (size: ${buffer.length} bytes)`);
275
839
 
276
- // Use callback-based confirmation for more reliable error handling
840
+ // Use callback-based confirmation - kanály jsou spolehlivé, takže callback vždy dorazí
277
841
  const confirmPromise = new Promise((resolve, reject) => {
278
- // Set up timeout
279
- const timeout = setTimeout(() => {
280
- reject(new Error(`Publisher confirm timeout after 5 seconds for queue "${queue}"`));
281
- }, 5000);
282
-
283
842
  // Send message with callback
284
- const sent = this._channel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
285
- clearTimeout(timeout);
843
+ this._channel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
286
844
  if (err) {
287
845
  console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Send callback error for queue "${queue}":`, err.message);
288
846
  reject(err);
@@ -291,42 +849,17 @@ class RabbitMQClient extends EventEmitter {
291
849
  resolve();
292
850
  }
293
851
  });
294
-
295
- // If sendToQueue returns false, channel is full - wait for drain
296
- if (!sent) {
297
- console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel full, waiting for drain for queue "${queue}"`);
298
- this._channel.once('drain', () => {
299
- console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel drained for queue "${queue}"`);
300
- });
301
- }
302
852
  });
303
853
 
304
- try {
305
- await confirmPromise;
306
- } catch (confirmErr) {
307
- // Check if channel was closed during confirmation
308
- if (this._channel.closed) {
309
- const errorMsg = `Publisher confirm failed for queue "${queue}": channel closed during confirmation. Original error: ${confirmErr.message}`;
310
- console.error('[RabbitMQClient] [mq-client-core] [PUBLISH]', errorMsg);
311
- throw new Error(errorMsg);
312
- }
313
- throw confirmErr;
314
- }
854
+ await confirmPromise;
315
855
  } else {
316
856
  // If exchange is specified, assert exchange and publish to it
317
- if (this._channel.closed) {
318
- throw new Error('Cannot publish: channel closed during operation');
319
- }
857
+ // Channel is guaranteed to be open (ensured above)
320
858
  await this._channel.assertExchange(exchange, 'direct', { durable: this._config.durable });
321
859
 
322
- // Use callback-based confirmation for exchange publish
860
+ // Use callback-based confirmation - kanály jsou spolehlivé
323
861
  const confirmPromise = new Promise((resolve, reject) => {
324
- const timeout = setTimeout(() => {
325
- reject(new Error(`Publisher confirm timeout after 5 seconds for exchange "${exchange}"`));
326
- }, 5000);
327
-
328
- const sent = this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
329
- clearTimeout(timeout);
862
+ this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
330
863
  if (err) {
331
864
  console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
332
865
  reject(err);
@@ -335,28 +868,22 @@ class RabbitMQClient extends EventEmitter {
335
868
  resolve();
336
869
  }
337
870
  });
338
-
339
- if (!sent) {
340
- this._channel.once('drain', () => {
341
- console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel drained for exchange "${exchange}"`);
342
- });
343
- }
344
871
  });
345
872
 
346
- try {
347
- await confirmPromise;
348
- } catch (confirmErr) {
349
- if (this._channel.closed) {
350
- throw new Error(`Publisher confirm failed for exchange "${exchange}": channel closed during confirmation. Original error: ${confirmErr.message}`);
351
- }
352
- throw confirmErr;
353
- }
873
+ await confirmPromise;
354
874
  }
355
875
  } catch (err) {
356
- // If channel was closed, mark it for recreation
876
+ // If channel was closed, try to recreate and retry once
357
877
  if (err.message && (err.message.includes('Channel closed') || err.message.includes('channel is closed') || this._channel?.closed)) {
358
- console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish, will need to reconnect');
359
- this._channel = null;
878
+ console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish, recreating and retrying...');
879
+ try {
880
+ await this._ensurePublisherChannel();
881
+ // Retry publish once
882
+ return await this.publish(queue, buffer, options);
883
+ } catch (retryErr) {
884
+ console.error('[RabbitMQClient] [mq-client-core] [PUBLISH] Retry failed:', retryErr.message);
885
+ throw retryErr;
886
+ }
360
887
  }
361
888
  this.emit('error', err);
362
889
  throw err;
@@ -372,41 +899,11 @@ class RabbitMQClient extends EventEmitter {
372
899
  * @throws {Error} If consume setup fails or channel is not available.
373
900
  */
374
901
  async consume(queue, onMessage, options = {}) {
375
- // Use dedicated consumer channel instead of ConfirmChannel
376
- // ConfirmChannel is optimized for publish operations, not consume
377
- if (!this._consumerChannel) {
378
- // Recreate consumer channel if closed
379
- if (this._connection && !this._connection.closed) {
380
- this._consumerChannel = await this._connection.createChannel();
381
- this._consumerChannel.on('error', (err) => {
382
- console.warn('[RabbitMQClient] Consumer channel error:', err.message);
383
- this.emit('error', err);
384
- });
385
- this._consumerChannel.on('close', () => {
386
- console.warn('[RabbitMQClient] Consumer channel closed');
387
- this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
388
- });
389
- } else {
390
- throw new Error('Cannot consume: consumer channel is not initialized and connection is closed');
391
- }
392
- }
902
+ // Ensure consumer channel exists and is open (auto-recreates if closed and re-registers consumers)
903
+ await this._ensureConsumerChannel();
393
904
 
394
- if (this._consumerChannel.closed) {
395
- // Recreate consumer channel if closed
396
- if (this._connection && !this._connection.closed) {
397
- this._consumerChannel = await this._connection.createChannel();
398
- this._consumerChannel.on('error', (err) => {
399
- console.warn('[RabbitMQClient] Consumer channel error:', err.message);
400
- this.emit('error', err);
401
- });
402
- this._consumerChannel.on('close', () => {
403
- console.warn('[RabbitMQClient] Consumer channel closed');
404
- this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
405
- });
406
- } else {
407
- throw new Error('Cannot consume: consumer channel is closed and connection is closed');
408
- }
409
- }
905
+ // Track operation for debugging
906
+ this._trackChannelOperation(this._consumerChannel, `consume from ${queue}`);
410
907
 
411
908
  const durable = options.durable !== undefined ? options.durable : this._config.durable;
412
909
  const prefetch = options.prefetch !== undefined ? options.prefetch : this._config.prefetch;
@@ -485,6 +982,7 @@ class RabbitMQClient extends EventEmitter {
485
982
 
486
983
  try {
487
984
  console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call assertQueue(${queue}, ${JSON.stringify(queueOptions)})`);
985
+ this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
488
986
  await this._queueChannel.assertQueue(queue, queueOptions);
489
987
  console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Queue ${queue} asserted successfully`);
490
988
  } catch (assertErr) {
@@ -516,25 +1014,36 @@ class RabbitMQClient extends EventEmitter {
516
1014
 
517
1015
  // Use dedicated consumer channel for consume operations
518
1016
  // This prevents conflicts with ConfirmChannel used for publish operations
1017
+ const messageHandler = async (msg) => {
1018
+ if (msg === null) {
1019
+ return;
1020
+ }
1021
+ try {
1022
+ await onMessage(msg);
1023
+ if (!noAck) {
1024
+ this._consumerChannel.ack(msg);
1025
+ }
1026
+ } catch (handlerErr) {
1027
+ // Negative acknowledge and requeue by default
1028
+ this._consumerChannel.nack(msg, false, true);
1029
+ }
1030
+ };
1031
+
519
1032
  const consumeResult = await this._consumerChannel.consume(
520
1033
  queue,
521
- async (msg) => {
522
- if (msg === null) {
523
- return;
524
- }
525
- try {
526
- await onMessage(msg);
527
- if (!noAck) {
528
- this._consumerChannel.ack(msg);
529
- }
530
- } catch (handlerErr) {
531
- // Negative acknowledge and requeue by default
532
- this._consumerChannel.nack(msg, false, true);
533
- }
534
- },
1034
+ messageHandler,
535
1035
  { noAck }
536
1036
  );
537
1037
 
1038
+ // Track consumer for auto re-registration if channel closes
1039
+ this._activeConsumers.set(queue, {
1040
+ handler: messageHandler,
1041
+ options: { noAck, prefetch, durable, queueOptions },
1042
+ consumerTag: consumeResult.consumerTag
1043
+ });
1044
+
1045
+ console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Consumer registered for queue "${queue}" (consumerTag: ${consumeResult.consumerTag})`);
1046
+
538
1047
  // Return consumer tag for cancellation
539
1048
  return consumeResult.consumerTag;
540
1049
  } catch (err) {