@onlineapps/mq-client-core 1.0.22 → 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
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
|
/**
|
|
@@ -60,6 +67,11 @@ class BaseClient {
|
|
|
60
67
|
console.log(`[BaseClient] Starting connect() with config:`, JSON.stringify({ type: this._config.type, host: this._config.host }));
|
|
61
68
|
// Merge overrides into existing config
|
|
62
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}`;
|
|
63
75
|
|
|
64
76
|
try {
|
|
65
77
|
console.log(`[BaseClient] Creating transport...`);
|
|
@@ -90,6 +102,7 @@ class BaseClient {
|
|
|
90
102
|
await this._transport.disconnect();
|
|
91
103
|
this._connected = false;
|
|
92
104
|
this._transport = null;
|
|
105
|
+
BaseClient._unregisterInstance(this);
|
|
93
106
|
} catch (err) {
|
|
94
107
|
throw new Error(`Error during disconnect: ${err.message}`);
|
|
95
108
|
}
|
|
@@ -243,3 +256,44 @@ class BaseClient {
|
|
|
243
256
|
|
|
244
257
|
module.exports = BaseClient;
|
|
245
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
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
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(
|
|
85
|
+
console.log('[RabbitMQClient] Starting connection race...');
|
|
69
86
|
this._connection = await Promise.race([connectPromise, timeoutPromise]);
|
|
70
|
-
console.log(
|
|
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
|
-
|
|
57
|
-
await
|
|
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
|
-
|
|
65
|
-
|
|
103
|
+
logger.warn(
|
|
104
|
+
`[QueueInit] ⚠ Queue ${queueName} exists with different arguments - deleting and recreating...`
|
|
105
|
+
);
|
|
66
106
|
try {
|
|
67
|
-
await
|
|
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(
|
|
71
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
}
|