@onlineapps/mq-client-core 1.0.33 → 1.0.34

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.34",
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,13 @@ 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;
38
45
  }
39
46
 
40
47
  /**
@@ -95,14 +102,18 @@ class RabbitMQClient extends EventEmitter {
95
102
  // Use ConfirmChannel to enable publisher confirms for publish operations
96
103
  this._channel = await this._connection.createConfirmChannel();
97
104
  // Enable publisher confirms explicitly
98
- this._channel.on('error', (err) => {
105
+ this._channel.on('error', async (err) => {
99
106
  console.error('[RabbitMQClient] [mq-client-core] Publisher channel error:', err.message);
107
+ // Auto-recreate channel if connection is still alive
108
+ await this._ensurePublisherChannel();
100
109
  this.emit('error', err);
101
110
  });
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'));
111
+ this._channel.on('close', async () => {
112
+ console.warn('[RabbitMQClient] [mq-client-core] Publisher channel closed - will auto-recreate on next publish');
113
+ // Mark channel as null - will be recreated on next publish
114
+ this._channel = null;
115
+ // Try to recreate immediately if connection is alive
116
+ await this._ensurePublisherChannel();
106
117
  });
107
118
  // Set up publisher confirms callback
108
119
  this._channel.on('drain', () => {
@@ -112,24 +123,35 @@ class RabbitMQClient extends EventEmitter {
112
123
  // Create a separate regular channel for queue operations (assertQueue, checkQueue)
113
124
  // This avoids RPC reply queue issues with ConfirmChannel.assertQueue()
114
125
  this._queueChannel = await this._connection.createChannel();
115
- this._queueChannel.on('error', (err) => {
126
+ this._queueChannel.on('error', async (err) => {
116
127
  // Log but don't emit - queue channel errors are less critical
117
128
  console.warn('[RabbitMQClient] Queue channel error:', err.message);
129
+ // Auto-recreate channel if connection is still alive
130
+ await this._ensureQueueChannel();
118
131
  });
119
- this._queueChannel.on('close', () => {
120
- console.warn('[RabbitMQClient] Queue channel closed');
132
+ this._queueChannel.on('close', async () => {
133
+ console.warn('[RabbitMQClient] Queue channel closed - will auto-recreate on next operation');
134
+ // Mark channel as null - will be recreated on next queue operation
135
+ this._queueChannel = null;
136
+ // Try to recreate immediately if connection is alive
137
+ await this._ensureQueueChannel();
121
138
  });
122
139
 
123
140
  // Create a dedicated channel for consume operations
124
141
  // This prevents channel conflicts between publish (ConfirmChannel) and consume operations
125
142
  this._consumerChannel = await this._connection.createChannel();
126
- this._consumerChannel.on('error', (err) => {
143
+ this._consumerChannel.on('error', async (err) => {
127
144
  console.warn('[RabbitMQClient] Consumer channel error:', err.message);
145
+ // Auto-recreate channel and re-register consumers
146
+ await this._ensureConsumerChannel();
128
147
  this.emit('error', err);
129
148
  });
130
- this._consumerChannel.on('close', () => {
131
- console.warn('[RabbitMQClient] Consumer channel closed');
132
- this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
149
+ this._consumerChannel.on('close', async () => {
150
+ console.warn('[RabbitMQClient] Consumer channel closed - will auto-recreate and re-register consumers');
151
+ // Mark channel as null - will be recreated and consumers re-registered
152
+ this._consumerChannel = null;
153
+ // Try to recreate and re-register consumers immediately if connection is alive
154
+ await this._ensureConsumerChannel();
133
155
  });
134
156
  } catch (err) {
135
157
  // Cleanup partially created resources
@@ -145,11 +167,192 @@ class RabbitMQClient extends EventEmitter {
145
167
  }
146
168
  }
147
169
 
170
+ /**
171
+ * Ensure publisher channel exists and is open
172
+ * Auto-recreates if closed
173
+ * @private
174
+ * @returns {Promise<void>}
175
+ */
176
+ async _ensurePublisherChannel() {
177
+ if (this._channel && !this._channel.closed) {
178
+ return; // Channel is good
179
+ }
180
+
181
+ if (!this._connection || this._connection.closed) {
182
+ throw new Error('Cannot recreate publisher channel: connection is closed');
183
+ }
184
+
185
+ try {
186
+ // Close old channel if exists
187
+ if (this._channel) {
188
+ try {
189
+ await this._channel.close();
190
+ } catch (_) {
191
+ // Ignore errors when closing already-closed channel
192
+ }
193
+ }
194
+
195
+ // Create new ConfirmChannel
196
+ this._channel = await this._connection.createConfirmChannel();
197
+ this._channel.on('error', async (err) => {
198
+ console.error('[RabbitMQClient] [mq-client-core] Publisher channel error:', err.message);
199
+ await this._ensurePublisherChannel();
200
+ this.emit('error', err);
201
+ });
202
+ this._channel.on('close', async () => {
203
+ console.warn('[RabbitMQClient] [mq-client-core] Publisher channel closed - will auto-recreate on next publish');
204
+ this._channel = null;
205
+ await this._ensurePublisherChannel();
206
+ });
207
+ this._channel.on('drain', () => {
208
+ console.log('[RabbitMQClient] [mq-client-core] Publisher channel drained');
209
+ });
210
+
211
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Publisher channel recreated');
212
+ } catch (err) {
213
+ this._channel = null;
214
+ throw new Error(`Failed to recreate publisher channel: ${err.message}`);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Ensure queue channel exists and is open
220
+ * Auto-recreates if closed
221
+ * @private
222
+ * @returns {Promise<void>}
223
+ */
224
+ async _ensureQueueChannel() {
225
+ if (this._queueChannel && !this._queueChannel.closed) {
226
+ return; // Channel is good
227
+ }
228
+
229
+ if (!this._connection || this._connection.closed) {
230
+ throw new Error('Cannot recreate queue channel: connection is closed');
231
+ }
232
+
233
+ try {
234
+ // Close old channel if exists
235
+ if (this._queueChannel) {
236
+ try {
237
+ await this._queueChannel.close();
238
+ } catch (_) {
239
+ // Ignore errors when closing already-closed channel
240
+ }
241
+ }
242
+
243
+ // Create new regular channel
244
+ this._queueChannel = await this._connection.createChannel();
245
+ this._queueChannel.on('error', async (err) => {
246
+ console.warn('[RabbitMQClient] Queue channel error:', err.message);
247
+ await this._ensureQueueChannel();
248
+ });
249
+ this._queueChannel.on('close', async () => {
250
+ console.warn('[RabbitMQClient] Queue channel closed - will auto-recreate on next operation');
251
+ this._queueChannel = null;
252
+ await this._ensureQueueChannel();
253
+ });
254
+
255
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Queue channel recreated');
256
+ } catch (err) {
257
+ this._queueChannel = null;
258
+ throw new Error(`Failed to recreate queue channel: ${err.message}`);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Ensure consumer channel exists and is open
264
+ * Auto-recreates and re-registers all consumers if closed
265
+ * @private
266
+ * @returns {Promise<void>}
267
+ */
268
+ async _ensureConsumerChannel() {
269
+ if (this._consumerChannel && !this._consumerChannel.closed) {
270
+ return; // Channel is good
271
+ }
272
+
273
+ if (!this._connection || this._connection.closed) {
274
+ throw new Error('Cannot recreate consumer channel: connection is closed');
275
+ }
276
+
277
+ try {
278
+ // Close old channel if exists
279
+ if (this._consumerChannel) {
280
+ try {
281
+ await this._consumerChannel.close();
282
+ } catch (_) {
283
+ // Ignore errors when closing already-closed channel
284
+ }
285
+ }
286
+
287
+ // Create new regular channel
288
+ this._consumerChannel = await this._connection.createChannel();
289
+ this._consumerChannel.on('error', async (err) => {
290
+ console.warn('[RabbitMQClient] Consumer channel error:', err.message);
291
+ await this._ensureConsumerChannel();
292
+ this.emit('error', err);
293
+ });
294
+ this._consumerChannel.on('close', async () => {
295
+ console.warn('[RabbitMQClient] Consumer channel closed - will auto-recreate and re-register consumers');
296
+ this._consumerChannel = null;
297
+ await this._ensureConsumerChannel();
298
+ });
299
+
300
+ // Re-register all active consumers
301
+ if (this._activeConsumers.size > 0) {
302
+ console.log(`[RabbitMQClient] [mq-client-core] Re-registering ${this._activeConsumers.size} consumers...`);
303
+ for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
304
+ try {
305
+ // Re-assert queue with correct options before consuming (may have been deleted)
306
+ if (consumerInfo.options.queueOptions) {
307
+ try {
308
+ await this._ensureQueueChannel();
309
+ await this._queueChannel.assertQueue(queue, consumerInfo.options.queueOptions);
310
+ } catch (assertErr) {
311
+ if (assertErr.code === 404) {
312
+ console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} does not exist, skipping consumer re-registration`);
313
+ this._activeConsumers.delete(queue);
314
+ continue;
315
+ }
316
+ // 406 error means queue exists with different args - skip this consumer
317
+ if (assertErr.code === 406) {
318
+ console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} exists with different args, skipping consumer re-registration`);
319
+ this._activeConsumers.delete(queue);
320
+ continue;
321
+ }
322
+ throw assertErr;
323
+ }
324
+ }
325
+
326
+ const consumeResult = await this._consumerChannel.consume(
327
+ queue,
328
+ consumerInfo.handler,
329
+ { noAck: consumerInfo.options.noAck }
330
+ );
331
+ consumerInfo.consumerTag = consumeResult.consumerTag;
332
+ console.log(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
333
+ } catch (err) {
334
+ console.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
335
+ // Remove failed consumer from tracking
336
+ this._activeConsumers.delete(queue);
337
+ }
338
+ }
339
+ }
340
+
341
+ console.log('[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated');
342
+ } catch (err) {
343
+ this._consumerChannel = null;
344
+ throw new Error(`Failed to recreate consumer channel: ${err.message}`);
345
+ }
346
+ }
347
+
148
348
  /**
149
349
  * Disconnects: closes channel and connection.
150
350
  * @returns {Promise<void>}
151
351
  */
152
352
  async disconnect() {
353
+ // Clear active consumers tracking
354
+ this._activeConsumers.clear();
355
+
153
356
  try {
154
357
  if (this._consumerChannel) {
155
358
  await this._consumerChannel.close();
@@ -194,15 +397,11 @@ class RabbitMQClient extends EventEmitter {
194
397
  * @throws {Error} If publish fails or channel is not available.
195
398
  */
196
399
  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
- }
400
+ // Ensure publisher channel exists and is open (auto-recreates if closed)
401
+ await this._ensurePublisherChannel();
203
402
 
204
403
  // Log publish attempt for debugging
205
- console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Attempting to publish to queue "${queue}" (channel open: ${!this._channel.closed})`);
404
+ console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Attempting to publish to queue "${queue}"`);
206
405
 
207
406
  const exchange = this._config.exchange || '';
208
407
  const routingKey = options.routingKey || queue;
@@ -217,21 +416,8 @@ class RabbitMQClient extends EventEmitter {
217
416
  // If queue doesn't exist (404), we should NOT auto-create it - infrastructure queues must be created explicitly
218
417
  // This prevents creating queues with wrong arguments (no TTL) which causes 406 errors later
219
418
  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
- }
419
+ // Ensure queue channel exists and is open (auto-recreates if closed)
420
+ await this._ensureQueueChannel();
235
421
  await this._queueChannel.checkQueue(queue);
236
422
  // Queue exists - proceed to publish
237
423
  } catch (checkErr) {
@@ -266,23 +452,13 @@ class RabbitMQClient extends EventEmitter {
266
452
  }
267
453
  }
268
454
  // 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
-
455
+ // Channel is guaranteed to be open (ensured above)
274
456
  console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Sending message to queue "${queue}" (size: ${buffer.length} bytes)`);
275
457
 
276
- // Use callback-based confirmation for more reliable error handling
458
+ // Use callback-based confirmation - kanály jsou spolehlivé, takže callback vždy dorazí
277
459
  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
460
  // Send message with callback
284
- const sent = this._channel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
285
- clearTimeout(timeout);
461
+ this._channel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
286
462
  if (err) {
287
463
  console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Send callback error for queue "${queue}":`, err.message);
288
464
  reject(err);
@@ -291,42 +467,17 @@ class RabbitMQClient extends EventEmitter {
291
467
  resolve();
292
468
  }
293
469
  });
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
470
  });
303
471
 
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
- }
472
+ await confirmPromise;
315
473
  } else {
316
474
  // 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
- }
475
+ // Channel is guaranteed to be open (ensured above)
320
476
  await this._channel.assertExchange(exchange, 'direct', { durable: this._config.durable });
321
477
 
322
- // Use callback-based confirmation for exchange publish
478
+ // Use callback-based confirmation - kanály jsou spolehlivé
323
479
  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);
480
+ this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
330
481
  if (err) {
331
482
  console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
332
483
  reject(err);
@@ -335,28 +486,22 @@ class RabbitMQClient extends EventEmitter {
335
486
  resolve();
336
487
  }
337
488
  });
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
489
  });
345
490
 
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
- }
491
+ await confirmPromise;
354
492
  }
355
493
  } catch (err) {
356
- // If channel was closed, mark it for recreation
494
+ // If channel was closed, try to recreate and retry once
357
495
  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;
496
+ console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish, recreating and retrying...');
497
+ try {
498
+ await this._ensurePublisherChannel();
499
+ // Retry publish once
500
+ return await this.publish(queue, buffer, options);
501
+ } catch (retryErr) {
502
+ console.error('[RabbitMQClient] [mq-client-core] [PUBLISH] Retry failed:', retryErr.message);
503
+ throw retryErr;
504
+ }
360
505
  }
361
506
  this.emit('error', err);
362
507
  throw err;
@@ -372,41 +517,8 @@ class RabbitMQClient extends EventEmitter {
372
517
  * @throws {Error} If consume setup fails or channel is not available.
373
518
  */
374
519
  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
- }
393
-
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
- }
520
+ // Ensure consumer channel exists and is open (auto-recreates if closed and re-registers consumers)
521
+ await this._ensureConsumerChannel();
410
522
 
411
523
  const durable = options.durable !== undefined ? options.durable : this._config.durable;
412
524
  const prefetch = options.prefetch !== undefined ? options.prefetch : this._config.prefetch;
@@ -516,25 +628,36 @@ class RabbitMQClient extends EventEmitter {
516
628
 
517
629
  // Use dedicated consumer channel for consume operations
518
630
  // This prevents conflicts with ConfirmChannel used for publish operations
631
+ const messageHandler = async (msg) => {
632
+ if (msg === null) {
633
+ return;
634
+ }
635
+ try {
636
+ await onMessage(msg);
637
+ if (!noAck) {
638
+ this._consumerChannel.ack(msg);
639
+ }
640
+ } catch (handlerErr) {
641
+ // Negative acknowledge and requeue by default
642
+ this._consumerChannel.nack(msg, false, true);
643
+ }
644
+ };
645
+
519
646
  const consumeResult = await this._consumerChannel.consume(
520
647
  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
- },
648
+ messageHandler,
535
649
  { noAck }
536
650
  );
537
651
 
652
+ // Track consumer for auto re-registration if channel closes
653
+ this._activeConsumers.set(queue, {
654
+ handler: messageHandler,
655
+ options: { noAck, prefetch, durable, queueOptions },
656
+ consumerTag: consumeResult.consumerTag
657
+ });
658
+
659
+ console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Consumer registered for queue "${queue}" (consumerTag: ${consumeResult.consumerTag})`);
660
+
538
661
  // Return consumer tag for cancellation
539
662
  return consumeResult.consumerTag;
540
663
  } catch (err) {