@onlineapps/mq-client-core 1.0.21 → 1.0.23

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.21",
3
+ "version": "1.0.23",
4
4
  "description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/BaseClient.js CHANGED
@@ -32,6 +32,11 @@ class BaseClient {
32
32
 
33
33
  // Merge user config with defaults
34
34
  this._config = merge({}, defaultConfig, config || {});
35
+ this._config.heartbeat =
36
+ this._config.heartbeat ?? Number(process.env.RABBITMQ_HEARTBEAT || 30);
37
+ this._config.connectionName =
38
+ this._config.connectionName ||
39
+ `${process.env.SERVICE_NAME || 'oa-service'}:${process.pid}`;
35
40
 
36
41
  // Validate merged config
37
42
  const valid = validate(this._config);
@@ -46,6 +51,8 @@ class BaseClient {
46
51
  this._transport = null;
47
52
  this._connected = false;
48
53
  this._errorHandlers = [];
54
+
55
+ BaseClient._registerInstance(this);
49
56
  }
50
57
 
51
58
  /**
@@ -57,17 +64,27 @@ class BaseClient {
57
64
  async connect(options = {}) {
58
65
  if (this._connected) return;
59
66
 
67
+ console.log(`[BaseClient] Starting connect() with config:`, JSON.stringify({ type: this._config.type, host: this._config.host }));
60
68
  // Merge overrides into existing config
61
69
  this._config = merge({}, this._config, options);
70
+ this._config.heartbeat =
71
+ this._config.heartbeat ?? Number(process.env.RABBITMQ_HEARTBEAT || 30);
72
+ this._config.connectionName =
73
+ this._config.connectionName ||
74
+ `${process.env.SERVICE_NAME || 'oa-service'}:${process.pid}`;
62
75
 
63
76
  try {
77
+ console.log(`[BaseClient] Creating transport...`);
64
78
  // Instantiate appropriate transport: RabbitMQClient
65
79
  this._transport = transportFactory.create(this._config);
80
+ console.log(`[BaseClient] Transport created:`, this._transport.constructor.name);
66
81
 
67
82
  // Register internal error propagation
68
83
  this._transport.on('error', (err) => this._handleError(err));
69
84
 
85
+ console.log(`[BaseClient] Calling transport.connect()...`);
70
86
  await this._transport.connect(this._config);
87
+ console.log(`[BaseClient] Transport connected successfully`);
71
88
  this._connected = true;
72
89
  } catch (err) {
73
90
  throw new ConnectionError('Failed to connect to broker', err);
@@ -85,6 +102,7 @@ class BaseClient {
85
102
  await this._transport.disconnect();
86
103
  this._connected = false;
87
104
  this._transport = null;
105
+ BaseClient._unregisterInstance(this);
88
106
  } catch (err) {
89
107
  throw new Error(`Error during disconnect: ${err.message}`);
90
108
  }
@@ -238,3 +256,44 @@ class BaseClient {
238
256
 
239
257
  module.exports = BaseClient;
240
258
 
259
+ BaseClient._instances = new Set();
260
+ BaseClient._cleanupRegistered = false;
261
+ BaseClient._cleanupRunning = false;
262
+
263
+ BaseClient._registerInstance = function registerInstance(instance) {
264
+ BaseClient._instances.add(instance);
265
+ if (!BaseClient._cleanupRegistered) {
266
+ BaseClient._registerCleanupHooks();
267
+ }
268
+ };
269
+
270
+ BaseClient._unregisterInstance = function unregisterInstance(instance) {
271
+ BaseClient._instances.delete(instance);
272
+ };
273
+
274
+ BaseClient._registerCleanupHooks = function registerCleanupHooks() {
275
+ const handler = async (signal) => {
276
+ if (BaseClient._cleanupRunning) return;
277
+ BaseClient._cleanupRunning = true;
278
+ const disconnects = Array.from(BaseClient._instances).map((instance) =>
279
+ instance
280
+ .disconnect()
281
+ .catch((err) =>
282
+ console.warn('[BaseClient] Failed to disconnect during shutdown:', err.message)
283
+ )
284
+ );
285
+ await Promise.allSettled(disconnects);
286
+ BaseClient._instances.clear();
287
+ BaseClient._cleanupRunning = false;
288
+ if (signal && signal !== 'beforeExit') {
289
+ process.exit(0); // eslint-disable-line no-process-exit
290
+ }
291
+ };
292
+
293
+ ['SIGINT', 'SIGTERM'].forEach((sig) => {
294
+ process.on(sig, () => handler(sig));
295
+ });
296
+ process.on('beforeExit', () => handler('beforeExit'));
297
+ BaseClient._cleanupRegistered = true;
298
+ };
299
+
@@ -43,6 +43,13 @@ class RabbitMQClient extends EventEmitter {
43
43
  get channel() {
44
44
  return this._channel;
45
45
  }
46
+
47
+ /**
48
+ * Getter for underlying connection
49
+ */
50
+ get connection() {
51
+ return this._connection;
52
+ }
46
53
 
47
54
  /**
48
55
  * Getter for queue channel - regular channel for queue operations
@@ -59,15 +66,25 @@ class RabbitMQClient extends EventEmitter {
59
66
  */
60
67
  async connect() {
61
68
  try {
62
- console.log(`[RabbitMQClient] Attempting to connect to: ${this._config.host}`);
63
- // Add connection timeout to prevent hanging
64
- const connectPromise = amqp.connect(this._config.host);
69
+ const rawTarget = this._config.host || this._config.url;
70
+ const heartbeat = this._config.heartbeat ?? 30;
71
+ const connectionName =
72
+ this._config.connectionName ||
73
+ this._config.clientName ||
74
+ `oa-client:${process.env.SERVICE_NAME || 'unknown'}:${process.pid}`;
75
+
76
+ console.log(`[RabbitMQClient] Attempting to connect to: ${rawTarget}`);
77
+ const connectArgs = typeof rawTarget === 'string'
78
+ ? [rawTarget, { heartbeat, clientProperties: { connection_name: connectionName } }]
79
+ : [{ ...rawTarget, heartbeat, clientProperties: { connection_name: connectionName } }];
80
+
81
+ const connectPromise = amqp.connect(...connectArgs);
65
82
  const timeoutPromise = new Promise((_, reject) => {
66
83
  setTimeout(() => reject(new Error('Connection timeout after 10 seconds')), 10000);
67
84
  });
68
- console.log(`[RabbitMQClient] Starting connection race...`);
85
+ console.log('[RabbitMQClient] Starting connection race...');
69
86
  this._connection = await Promise.race([connectPromise, timeoutPromise]);
70
- console.log(`[RabbitMQClient] Connection established`);
87
+ console.log('[RabbitMQClient] Connection established');
71
88
  this._connection.on('error', (err) => this.emit('error', err));
72
89
  this._connection.on('close', () => {
73
90
  // Emit a connection close error to notify listeners
@@ -19,7 +19,12 @@
19
19
  * @returns {Promise<void>}
20
20
  */
21
21
  async function initInfrastructureQueues(channel, options = {}) {
22
+ if (!channel) {
23
+ throw new Error('initInfrastructureQueues requires a valid channel');
24
+ }
25
+
22
26
  const logger = options.logger || console;
27
+ const connection = options.connection || channel.connection;
23
28
 
24
29
  // Load queueConfig from mq-client-core (spodní vrstva)
25
30
  // NOTE: queueConfig is part of mq-client-core because it's used by both:
@@ -38,6 +43,35 @@ async function initInfrastructureQueues(channel, options = {}) {
38
43
  'registry.heartbeats'
39
44
  ];
40
45
 
46
+ const watchChannel = (ch) => {
47
+ if (!ch) return ch;
48
+ if (!ch.__queueInitLinked) {
49
+ ch.__queueInitLinked = true;
50
+ ch.once('close', () => {
51
+ ch.__queueInitClosed = true;
52
+ logger.warn('[QueueInit] Queue channel closed');
53
+ });
54
+ ch.on('error', (err) => {
55
+ ch.__queueInitClosed = true;
56
+ logger.warn(`[QueueInit] Queue channel error: ${err.message}`);
57
+ });
58
+ }
59
+ return ch;
60
+ };
61
+
62
+ let workingChannel = watchChannel(channel);
63
+
64
+ const getChannel = async (forceNew = false) => {
65
+ if (!forceNew && workingChannel && !workingChannel.__queueInitClosed) {
66
+ return workingChannel;
67
+ }
68
+ if (!connection) {
69
+ throw new Error('Queue channel closed and no connection reference available to recreate it');
70
+ }
71
+ workingChannel = watchChannel(await connection.createChannel());
72
+ return workingChannel;
73
+ };
74
+
41
75
  logger.log(`[QueueInit] Initializing ${queuesToCreate.length} infrastructure queues...`);
42
76
 
43
77
  for (const queueName of queuesToCreate) {
@@ -53,32 +87,43 @@ async function initInfrastructureQueues(channel, options = {}) {
53
87
 
54
88
  // CRITICAL: Check if queue exists with different arguments (406 error)
55
89
  // If so, delete it and recreate with correct parameters
56
- try {
57
- await channel.assertQueue(queueName, {
90
+ const ensureQueue = async () => {
91
+ const activeChannel = await getChannel();
92
+ return activeChannel.assertQueue(queueName, {
58
93
  durable: config.durable !== false,
59
94
  arguments: { ...config.arguments }
60
95
  });
96
+ };
97
+
98
+ try {
99
+ await ensureQueue();
61
100
  logger.log(`[QueueInit] ✓ Created/verified infrastructure queue: ${queueName}`);
62
101
  } catch (assertError) {
63
102
  if (assertError.code === 406) {
64
- // Queue exists with different arguments - delete and recreate
65
- logger.warn(`[QueueInit] ⚠ Queue ${queueName} exists with different arguments - deleting and recreating...`);
103
+ logger.warn(
104
+ `[QueueInit] ⚠ Queue ${queueName} exists with different arguments - deleting and recreating...`
105
+ );
66
106
  try {
67
- await channel.deleteQueue(queueName);
107
+ const deleteChannel = await getChannel(true);
108
+ await deleteChannel.deleteQueue(queueName);
68
109
  logger.log(`[QueueInit] ✓ Deleted queue ${queueName} with incorrect parameters`);
69
110
  } catch (deleteError) {
70
- logger.error(`[QueueInit] ✗ Failed to delete queue ${queueName}:`, deleteError.message);
71
- throw new Error(`Cannot delete queue ${queueName} with incorrect parameters: ${deleteError.message}`);
111
+ logger.error(
112
+ `[QueueInit] Failed to delete queue ${queueName}:`,
113
+ deleteError.message
114
+ );
115
+ throw new Error(
116
+ `Cannot delete queue ${queueName} with incorrect parameters: ${deleteError.message}`
117
+ );
72
118
  }
73
-
74
- // Recreate with correct parameters
75
- await channel.assertQueue(queueName, {
76
- durable: config.durable !== false,
77
- arguments: { ...config.arguments }
78
- });
79
- logger.log(`[QueueInit] ✓ Recreated infrastructure queue: ${queueName} with correct parameters`);
119
+
120
+ // Recreate with correct parameters (force fresh channel to avoid closed state)
121
+ await getChannel(true);
122
+ await ensureQueue();
123
+ logger.log(
124
+ `[QueueInit] ✓ Recreated infrastructure queue: ${queueName} with correct parameters`
125
+ );
80
126
  } else {
81
- // Other error - rethrow
82
127
  logger.error(`[QueueInit] ✗ Failed to create ${queueName}:`, assertError.message);
83
128
  throw assertError;
84
129
  }