@onlineapps/mq-client-core 1.0.83 → 2.0.0
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/README.md +24 -0
- package/jest.config.js +15 -0
- package/jest.integration.config.js +16 -0
- package/package.json +3 -3
- package/src/BaseClient.js +13 -25
- package/src/config/queueConfig.js +12 -12
- package/src/transports/rabbitmqClient.js +89 -21
- package/src/utils/publishErrors.js +23 -4
- package/src/workers/RecoveryWorker.js +33 -6
- package/tests/unit/queueConfig-delivery.test.js +0 -38
- package/tests/unit/queueConfig-validation.test.js +0 -28
package/README.md
CHANGED
|
@@ -62,6 +62,30 @@ await mqClient.publish('workflow.control', { workflowId: '123', data: '...' });
|
|
|
62
62
|
- `isConnected()` - Check connection status
|
|
63
63
|
- `onError(callback)` - Register error handler
|
|
64
64
|
|
|
65
|
+
`publish()` and the workflow helpers built on it resolve to `undefined`; failure is
|
|
66
|
+
signalled by a thrown `PublishError`, never by a falsy return.
|
|
67
|
+
|
|
68
|
+
### Queue ownership — publishing never creates an owned queue
|
|
69
|
+
|
|
70
|
+
A missing queue is answered by who owns the name, not by the client's scope:
|
|
71
|
+
|
|
72
|
+
| Name | Owner | Missing at publish time |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `workflow.*`, `registry.*`, `infrastructure.*`, `validation.*`, `monitoring.*`, `telemetry.*`, `delivery.*` | the infrastructure service that declares it | `QueueNotFoundError` (`kind: 'infrastructure'`) |
|
|
75
|
+
| `{service}.workflow`, `{service}.queue`, `{service}.dlq` | the owning service, via `setupServiceQueues()` **after** registration | `QueueNotFoundError` (`kind: 'business'`) |
|
|
76
|
+
| anything else | nobody | created with default options, if `recoveryScope` allows it |
|
|
77
|
+
|
|
78
|
+
Publishing must never create an owned queue: `sendToQueue()`/`assertQueue()` would
|
|
79
|
+
declare it with default arguments — no TTL, no DLQ — and the owner's later
|
|
80
|
+
`setupServiceQueues()` then fails with 406 `PRECONDITION_FAILED` because the
|
|
81
|
+
arguments no longer match. The rule is enforced in two places, and both are
|
|
82
|
+
required: the 404 branch of `_publishOnce()` refuses the publish, and
|
|
83
|
+
`RecoveryWorker.handleQueueNotFound()` refuses to create the queue behind it.
|
|
84
|
+
|
|
85
|
+
`recoveryScope: 'business'` (the default every `ConnectorMQClient` sets) says the
|
|
86
|
+
client belongs to a business service. It has never meant "may create business
|
|
87
|
+
queues".
|
|
88
|
+
|
|
65
89
|
## Architecture
|
|
66
90
|
|
|
67
91
|
```
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default tier: unit + component only.
|
|
5
|
+
*
|
|
6
|
+
* `tests/integration` is excluded here and served by `jest.integration.config.js`
|
|
7
|
+
* instead, because that tier fails fast on a missing `RABBITMQ_URL` — folding it
|
|
8
|
+
* into the default run would make `npm test` unrunnable without a broker, and the
|
|
9
|
+
* usual "fix" for that is a skip, which is how a suite quietly leaves the
|
|
10
|
+
* regression loop (`.claude/rules/service-refactoring.md` #4).
|
|
11
|
+
*/
|
|
12
|
+
module.exports = {
|
|
13
|
+
testEnvironment: 'node',
|
|
14
|
+
testPathIgnorePatterns: ['/node_modules/', '/tests/integration/']
|
|
15
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration tier: runs against the live broker, never skipped.
|
|
5
|
+
*
|
|
6
|
+
* `globalSetup` aborts the whole run before Jest builds a describe tree when
|
|
7
|
+
* `RABBITMQ_URL` is missing or the broker is unreachable — see
|
|
8
|
+
* `tests/integration/setup.js`.
|
|
9
|
+
*/
|
|
10
|
+
module.exports = {
|
|
11
|
+
testEnvironment: 'node',
|
|
12
|
+
testMatch: ['**/tests/integration/**/*.test.js'],
|
|
13
|
+
testTimeout: 30000,
|
|
14
|
+
globalSetup: './tests/integration/setup.js',
|
|
15
|
+
verbose: true
|
|
16
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/mq-client-core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "jest",
|
|
8
8
|
"test:unit": "jest --testPathPattern=tests/unit",
|
|
9
9
|
"test:component": "jest --testPathPattern=tests/component",
|
|
10
|
-
"test:integration": "jest --
|
|
10
|
+
"test:integration": "jest --config=jest.integration.config.js"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
13
13
|
"rabbitmq",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"amqplib": "^0.10.3",
|
|
22
22
|
"ajv": "^8.12.0",
|
|
23
23
|
"lodash.merge": "^4.6.2",
|
|
24
|
-
"@onlineapps/runtime-config": "1.0.
|
|
24
|
+
"@onlineapps/runtime-config": "1.0.3"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"jest": "^29.7.0"
|
package/src/BaseClient.js
CHANGED
|
@@ -263,6 +263,19 @@ class BaseClient {
|
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
265
|
* Acknowledge a raw broker message (delegates to transport).
|
|
266
|
+
*
|
|
267
|
+
* Settling is idempotent: the transport marks the delivery via `msg._mqProcessed`
|
|
268
|
+
* and `consume()` skips its own auto-ack for a message the handler already
|
|
269
|
+
* settled (`transports/rabbitmqClient.js:1769` and `:2268`). Calling this from
|
|
270
|
+
* inside a consume handler is therefore supported, not a double-settle.
|
|
271
|
+
*
|
|
272
|
+
* Until 2026-08-28 this method was declared TWICE in this class; the second,
|
|
273
|
+
* later declaration silently overrode this one and omitted the
|
|
274
|
+
* `typeof this._transport.ack !== 'function'` capability check below, so a
|
|
275
|
+
* transport without `ack` failed with `TypeError: this._transport.ack is not a
|
|
276
|
+
* function` instead of the actionable message. Same defect shape as the
|
|
277
|
+
* duplicate pair removed from the storage connector.
|
|
278
|
+
*
|
|
266
279
|
* @param {Object} msg - Broker message object
|
|
267
280
|
* @returns {Promise<void>}
|
|
268
281
|
*/
|
|
@@ -292,31 +305,6 @@ class BaseClient {
|
|
|
292
305
|
return await this._transport.nack(msg, options);
|
|
293
306
|
}
|
|
294
307
|
|
|
295
|
-
/**
|
|
296
|
-
* Acknowledges a RabbitMQ message.
|
|
297
|
-
* @param {Object} msg - RabbitMQ message object.
|
|
298
|
-
* @returns {Promise<void>}
|
|
299
|
-
*/
|
|
300
|
-
async ack(msg) {
|
|
301
|
-
if (!this._connected || !this._transport) {
|
|
302
|
-
throw new ConnectionError('Cannot ack: client is not connected');
|
|
303
|
-
}
|
|
304
|
-
return this._transport.ack(msg);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Negative-acknowledges a RabbitMQ message.
|
|
309
|
-
* @param {Object} msg - RabbitMQ message object.
|
|
310
|
-
* @param {Object} [options] - Options such as { requeue: boolean }.
|
|
311
|
-
* @returns {Promise<void>}
|
|
312
|
-
*/
|
|
313
|
-
async nack(msg, options = {}) {
|
|
314
|
-
if (!this._connected || !this._transport) {
|
|
315
|
-
throw new ConnectionError('Cannot nack: client is not connected');
|
|
316
|
-
}
|
|
317
|
-
return this._transport.nack(msg, options);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
308
|
/**
|
|
321
309
|
* Registers a global error handler. Internal or transport-level errors will be forwarded here.
|
|
322
310
|
* @param {function(Error): void} callback
|
|
@@ -174,15 +174,12 @@ module.exports = {
|
|
|
174
174
|
},
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
|
-
* delivery.result
|
|
177
|
+
* `delivery.result` used to be defined here as an "audit" queue. It never had a
|
|
178
|
+
* consumer in any commit, so everything published to it expired unread inside its
|
|
179
|
+
* 24h TTL. Delivery outcome is persisted durably (and redacted) through
|
|
180
|
+
* monitoring.workflow -> workflow_traces instead — see docs/architecture/delivery.md.
|
|
181
|
+
* Do not re-add it; enrich the monitoring.workflow event instead.
|
|
178
182
|
*/
|
|
179
|
-
result: {
|
|
180
|
-
durable: true,
|
|
181
|
-
arguments: {
|
|
182
|
-
'x-message-ttl': 86400000, // 24 hours TTL for audit traceability
|
|
183
|
-
'x-max-length': 50000
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
183
|
|
|
187
184
|
/**
|
|
188
185
|
* delivery.websocket.notify - Notifications for Gateway websocket hub
|
|
@@ -370,10 +367,13 @@ module.exports = {
|
|
|
370
367
|
*
|
|
371
368
|
* CRITICAL: This is the ONLY queue used for service-to-Registry communication.
|
|
372
369
|
* Registry listener processes different message types based on msg.type:
|
|
373
|
-
* - type: 'register'
|
|
374
|
-
* - type: 'heartbeat'
|
|
375
|
-
* - type: '
|
|
376
|
-
*
|
|
370
|
+
* - type: 'register' - Service registration requests (full spec + operations)
|
|
371
|
+
* - type: 'heartbeat' - Periodic health check messages (sent every 10s)
|
|
372
|
+
* - type: 'deregister' - Voluntary shutdown notice
|
|
373
|
+
* (The legacy 'apiDescription' / 'apiDescriptionRequest' types are no
|
|
374
|
+
* longer supported — see operations-registry-contract.md §2.)
|
|
375
|
+
*
|
|
376
|
+
*
|
|
377
377
|
* Architecture rationale:
|
|
378
378
|
* - Single consumer on registry.register handles all message types
|
|
379
379
|
* - Simplifies queue management (one queue instead of multiple)
|
|
@@ -19,6 +19,41 @@ const RecoveryWorker = require('../workers/RecoveryWorker');
|
|
|
19
19
|
const PublishMonitor = require('../monitoring/PublishMonitor');
|
|
20
20
|
const runtimeCfg = require('../config');
|
|
21
21
|
|
|
22
|
+
/** AMQP basic.publish fields forwarded from publish() options (amqplib Options.Publish). */
|
|
23
|
+
const AMQP_MESSAGE_PROPERTY_KEYS = [
|
|
24
|
+
'correlationId',
|
|
25
|
+
'replyTo',
|
|
26
|
+
'messageId',
|
|
27
|
+
'timestamp',
|
|
28
|
+
'type',
|
|
29
|
+
'priority',
|
|
30
|
+
'expiration',
|
|
31
|
+
'contentType',
|
|
32
|
+
'contentEncoding',
|
|
33
|
+
'userId',
|
|
34
|
+
'appId',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Merge persistent + headers with whitelisted AMQP message properties for sendToQueue/publish.
|
|
39
|
+
* @param {Object} options
|
|
40
|
+
* @param {boolean} persistent
|
|
41
|
+
* @param {Object} headers
|
|
42
|
+
* @returns {Object}
|
|
43
|
+
*/
|
|
44
|
+
function buildAmqpMessageProperties(options, persistent, headers) {
|
|
45
|
+
const props = { persistent };
|
|
46
|
+
if (headers && typeof headers === 'object') {
|
|
47
|
+
props.headers = headers;
|
|
48
|
+
}
|
|
49
|
+
for (const key of AMQP_MESSAGE_PROPERTY_KEYS) {
|
|
50
|
+
if (options[key] !== undefined && options[key] !== null && options[key] !== '') {
|
|
51
|
+
props[key] = options[key];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return props;
|
|
55
|
+
}
|
|
56
|
+
|
|
22
57
|
/**
|
|
23
58
|
* Local structured logger (module-owned) to avoid dependency cycles.
|
|
24
59
|
*
|
|
@@ -1283,10 +1318,16 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1283
1318
|
try {
|
|
1284
1319
|
// Ensure queue exists if publishing directly to queue and using default exchange
|
|
1285
1320
|
if (!exchange) {
|
|
1321
|
+
// RPC / client reply targets: server-named exclusive queues (amq.gen-*) or direct reply-to
|
|
1322
|
+
// exist only on the client's connection — another connection cannot checkQueue them.
|
|
1323
|
+
const skipQueueExistencePrecheck =
|
|
1324
|
+
queue === 'amq.rabbitmq.reply-to' || queue.startsWith('amq.gen-');
|
|
1325
|
+
|
|
1286
1326
|
// CRITICAL: For infrastructure queues, they should already exist with specific arguments from queueConfig
|
|
1287
1327
|
// Use queueChannel (regular channel) for checkQueue/assertQueue to avoid RPC reply queue issues
|
|
1288
1328
|
// If queue doesn't exist (404), we should NOT auto-create it - infrastructure queues must be created explicitly
|
|
1289
1329
|
// This prevents creating queues with wrong arguments (no TTL) which causes 406 errors later
|
|
1330
|
+
if (!skipQueueExistencePrecheck) {
|
|
1290
1331
|
try {
|
|
1291
1332
|
// Ensure queue channel exists and is open (auto-recreates if closed)
|
|
1292
1333
|
await this._ensureQueueChannel();
|
|
@@ -1302,27 +1343,44 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1302
1343
|
// Queue exists - proceed to publish
|
|
1303
1344
|
} catch (checkErr) {
|
|
1304
1345
|
// If queue doesn't exist (404), this je ERROR – ale rozlišujeme infra vs. non-infra:
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1346
|
+
// 404 na chybějící frontu se rozpadá na tři případy podle vlastnictví:
|
|
1347
|
+
// - Infrastrukturní fronty (`queueConfig.isInfrastructureQueue`) zakládají
|
|
1348
|
+
// infra služby předem → QueueNotFoundError(kind='infrastructure').
|
|
1349
|
+
// - Business fronty (`queueConfig.isBusinessQueue`, tj. `{service}.{workflow|queue|dlq}`)
|
|
1350
|
+
// zakládá vlastnící služba přes setupServiceQueues() po registraci
|
|
1351
|
+
// → QueueNotFoundError(kind='business'). Publisher je nikdy nevytváří.
|
|
1352
|
+
// - Ostatní jména (ani infra, ani business vzor) se smějí založit
|
|
1353
|
+
// s default parametry, pokud to scope RecoveryWorkeru dovoluje.
|
|
1308
1354
|
if (checkErr.code === 404) {
|
|
1309
|
-
//
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
queue.startsWith('validation.');
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
if (isInfraQueue) {
|
|
1355
|
+
// `../config/queueConfig` is a sibling module of this file; the require
|
|
1356
|
+
// cannot fail. It used to sit in a try/catch whose `catch` re-implemented
|
|
1357
|
+
// `isInfrastructureQueue` as an inline prefix list — a fallback
|
|
1358
|
+
// (`architecture-principles.md` §3) that had also gone stale: it was
|
|
1359
|
+
// missing `telemetry.` and `delivery.`, both of which the real
|
|
1360
|
+
// queueConfig classifies as infrastructure. A fallback that answers
|
|
1361
|
+
// differently from the thing it stands in for is a second, wrong source
|
|
1362
|
+
// of truth, so it is gone.
|
|
1363
|
+
const queueConfig = require('../config/queueConfig');
|
|
1364
|
+
|
|
1365
|
+
if (queueConfig.isInfrastructureQueue(queue)) {
|
|
1324
1366
|
// Jasný, hlasitý signál pro infra služby – fronta chybí, je to programátorská chyba
|
|
1325
|
-
throw new QueueNotFoundError(queue, true, checkErr);
|
|
1367
|
+
throw new QueueNotFoundError(queue, true, checkErr, 'infrastructure');
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Business queues (`{service}.{workflow|queue|dlq}`) MUST exist before
|
|
1371
|
+
// publish. `sendToQueue()`/`assertQueue()` with default options would
|
|
1372
|
+
// create them with no TTL and no DLQ, and the owning service's later
|
|
1373
|
+
// `setupServiceQueues()` then hits 406 PRECONDITION_FAILED because the
|
|
1374
|
+
// arguments no longer match. The queue is created by the owner AFTER
|
|
1375
|
+
// registration; a publisher that creates it is a programming error.
|
|
1376
|
+
//
|
|
1377
|
+
// The rule and this exact wording came from
|
|
1378
|
+
// `conn-infra-mq/src/transports/rabbitmqClient.js:264-277` — a transport
|
|
1379
|
+
// that nothing in `src/` instantiated, so the guard protected nobody
|
|
1380
|
+
// while this file, the one every client actually runs, auto-created the
|
|
1381
|
+
// queue and only printed a warning.
|
|
1382
|
+
if (queueConfig.isBusinessQueue(queue)) {
|
|
1383
|
+
throw new QueueNotFoundError(queue, false, checkErr, 'business');
|
|
1326
1384
|
}
|
|
1327
1385
|
// For non-infrastructure queues, check if queue creation is allowed before auto-creating
|
|
1328
1386
|
// Check recovery scope and filter
|
|
@@ -1358,6 +1416,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1358
1416
|
throw checkErr;
|
|
1359
1417
|
}
|
|
1360
1418
|
}
|
|
1419
|
+
}
|
|
1361
1420
|
// Publish to queue using ConfirmChannel (for publisher confirms)
|
|
1362
1421
|
// Channel is guaranteed to be open (ensured above)
|
|
1363
1422
|
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Sending message to queue "${queue}" (size: ${buffer.length} bytes)`);
|
|
@@ -1383,7 +1442,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1383
1442
|
|
|
1384
1443
|
// Send message with callback
|
|
1385
1444
|
try {
|
|
1386
|
-
originalChannel.sendToQueue(
|
|
1445
|
+
originalChannel.sendToQueue(
|
|
1446
|
+
queue,
|
|
1447
|
+
buffer,
|
|
1448
|
+
buildAmqpMessageProperties(options, persistent, headers),
|
|
1449
|
+
(err, ok) => {
|
|
1387
1450
|
callbackInvoked = true;
|
|
1388
1451
|
this._clearTimeout(timeout);
|
|
1389
1452
|
|
|
@@ -1483,7 +1546,12 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1483
1546
|
reject(new Error(`Exchange publish confirmation timeout for exchange "${exchange}" after ${this._publishConfirmationTimeout}ms`));
|
|
1484
1547
|
}, this._publishConfirmationTimeout);
|
|
1485
1548
|
|
|
1486
|
-
this._channel.publish(
|
|
1549
|
+
this._channel.publish(
|
|
1550
|
+
exchange,
|
|
1551
|
+
routingKey,
|
|
1552
|
+
buffer,
|
|
1553
|
+
buildAmqpMessageProperties(options, persistent, headers),
|
|
1554
|
+
(err, ok) => {
|
|
1487
1555
|
this._clearTimeout(timeout);
|
|
1488
1556
|
if (err) {
|
|
1489
1557
|
console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
|
|
@@ -36,15 +36,34 @@ class QueueNotFoundError extends PermanentPublishError {
|
|
|
36
36
|
* @param {string} queueName
|
|
37
37
|
* @param {boolean} isInfrastructure
|
|
38
38
|
* @param {Error} [cause]
|
|
39
|
+
* @param {('infrastructure'|'business'|'unknown')} [kind] - Which ownership rule
|
|
40
|
+
* was violated. Defaults to `'infrastructure'`/`'unknown'` from
|
|
41
|
+
* `isInfrastructure`, so existing two- and three-argument callers are unchanged.
|
|
39
42
|
*/
|
|
40
|
-
constructor(queueName, isInfrastructure, cause) {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
constructor(queueName, isInfrastructure, cause, kind) {
|
|
44
|
+
const resolvedKind = kind || (isInfrastructure ? 'infrastructure' : 'unknown');
|
|
45
|
+
let baseMessage;
|
|
46
|
+
if (resolvedKind === 'infrastructure') {
|
|
47
|
+
baseMessage =
|
|
48
|
+
`Cannot publish to infrastructure queue ${queueName}: queue does not exist. ` +
|
|
49
|
+
'Infrastructure queues must be created explicitly with correct arguments ' +
|
|
50
|
+
'(TTL, max-length, etc.) before publishing.';
|
|
51
|
+
} else if (resolvedKind === 'business') {
|
|
52
|
+
baseMessage =
|
|
53
|
+
`Cannot publish to business queue ${queueName}: queue does not exist. ` +
|
|
54
|
+
'Expected: business queues are created by their owning service via ' +
|
|
55
|
+
'setupServiceQueues() AFTER successful registration, with the arguments ' +
|
|
56
|
+
'queueConfig prescribes (TTL, DLQ). Fix: call setupServiceQueues() before ' +
|
|
57
|
+
'publishing — publishing must never create the queue, because sendToQueue() ' +
|
|
58
|
+
'would create it with default arguments (no TTL, no DLQ).';
|
|
59
|
+
} else {
|
|
60
|
+
baseMessage = `Queue ${queueName} does not exist`;
|
|
61
|
+
}
|
|
44
62
|
super(baseMessage, cause);
|
|
45
63
|
this.name = 'QueueNotFoundError';
|
|
46
64
|
this.queueName = queueName;
|
|
47
65
|
this.isInfrastructure = !!isInfrastructure;
|
|
66
|
+
this.kind = resolvedKind;
|
|
48
67
|
}
|
|
49
68
|
}
|
|
50
69
|
|
|
@@ -7,11 +7,18 @@ const { QueueNotFoundError } = require('../utils/publishErrors');
|
|
|
7
7
|
*
|
|
8
8
|
* Scope:
|
|
9
9
|
* - infrastructure: connection recovery, channel recreation, consumer re-registration (NENÍ queue creation)
|
|
10
|
-
* - business:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
10
|
+
* - business: totéž + zakládání front, ale POUZE těch, které nemají vlastníka
|
|
11
|
+
* (viz Queue Creation níže). Scope říká, ČÍ ten klient je — ne co smí založit.
|
|
12
|
+
*
|
|
13
|
+
* Queue Creation — vlastnictví fronty rozhoduje, ne scope:
|
|
14
|
+
* - infrastrukturní fronty (`queueConfig.isInfrastructureQueue`) zakládá vlastnící
|
|
15
|
+
* infra služba předem → tento worker je nikdy nezaloží, chybu propustí dál;
|
|
16
|
+
* - business fronty (`queueConfig.isBusinessQueue`, tj. `{service}.{workflow|queue|dlq}`)
|
|
17
|
+
* zakládá vlastnící služba přes `setupServiceQueues()` po registraci, s argumenty
|
|
18
|
+
* z queueConfig (TTL, DLQ) → tento worker je nikdy nezaloží, chybu propustí dál;
|
|
19
|
+
* - ostatní jména (ani infra, ani business vzor) smí založit, pokud to scope
|
|
20
|
+
* a `queueCreationFilter` dovolují — buď přes `queueCreationCallback`
|
|
21
|
+
* (deleguje na QueueManager), nebo přímo.
|
|
15
22
|
*/
|
|
16
23
|
class RecoveryWorker {
|
|
17
24
|
/**
|
|
@@ -66,7 +73,27 @@ class RecoveryWorker {
|
|
|
66
73
|
throw error;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
|
-
// Business queues
|
|
76
|
+
// Business queues ({service}.{workflow|queue|dlq}) must exist too — they are
|
|
77
|
+
// created by their owning service via setupServiceQueues() AFTER registration,
|
|
78
|
+
// with the arguments queueConfig prescribes.
|
|
79
|
+
//
|
|
80
|
+
// Measured 2026-08-28: without this branch the ownership guard in
|
|
81
|
+
// `_publishOnce` was defeated by its own error path. The publish was refused
|
|
82
|
+
// correctly, the caller saw PublishError — and then this worker, listening for
|
|
83
|
+
// QueueNotFoundError on the client's error event, created the very queue the
|
|
84
|
+
// guard had just refused ("[RecoveryWorker] ✓ Created queue '<q>' (direct)"),
|
|
85
|
+
// with default arguments and no TTL. Refusing at one door while opening the
|
|
86
|
+
// other is not a guard.
|
|
87
|
+
//
|
|
88
|
+
// `_scope: 'business'` says the CLIENT belongs to a business service. It has
|
|
89
|
+
// never meant "may create business queues", and ConnectorMQClient.js:74 sets
|
|
90
|
+
// it for every biz service, so that reading made the rule vacuous.
|
|
91
|
+
if (error.kind === 'business') {
|
|
92
|
+
this._logger?.error?.(`[RecoveryWorker] Cannot create business queue '${error.queueName}' - it is created by its owning service via setupServiceQueues() after registration`);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Everything else - create if scope allows
|
|
70
97
|
if (this._queueCreationEnabled) {
|
|
71
98
|
// Check filter if provided
|
|
72
99
|
if (this._queueCreationFilter && !this._queueCreationFilter(error.queueName)) {
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const queueConfig = require('../../src/config/queueConfig');
|
|
4
|
-
|
|
5
|
-
describe('queueConfig delivery infrastructure', () => {
|
|
6
|
-
test('delivery.retry configuration', () => {
|
|
7
|
-
const config = queueConfig.getInfrastructureQueueConfig('delivery.retry');
|
|
8
|
-
expect(config).toEqual({
|
|
9
|
-
durable: true,
|
|
10
|
-
arguments: {
|
|
11
|
-
'x-max-length': 10000
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('delivery.result configuration', () => {
|
|
17
|
-
const config = queueConfig.getInfrastructureQueueConfig('delivery.result');
|
|
18
|
-
expect(config).toEqual({
|
|
19
|
-
durable: true,
|
|
20
|
-
arguments: {
|
|
21
|
-
'x-message-ttl': 86400000,
|
|
22
|
-
'x-max-length': 50000
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('delivery.websocket.notify configuration', () => {
|
|
28
|
-
const config = queueConfig.getInfrastructureQueueConfig('delivery.websocket.notify');
|
|
29
|
-
expect(config).toEqual({
|
|
30
|
-
durable: true,
|
|
31
|
-
arguments: {
|
|
32
|
-
'x-message-ttl': 60000,
|
|
33
|
-
'x-max-length': 10000
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const queueConfig = require('../../src/config/queueConfig');
|
|
4
|
-
|
|
5
|
-
describe('queueConfig validation infrastructure', () => {
|
|
6
|
-
test('validation.requests configuration', () => {
|
|
7
|
-
const config = queueConfig.getInfrastructureQueueConfig('validation.requests');
|
|
8
|
-
expect(config).toEqual({
|
|
9
|
-
durable: true,
|
|
10
|
-
arguments: {
|
|
11
|
-
'x-message-ttl': 300000,
|
|
12
|
-
'x-max-length': 10000
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('validation.responses configuration', () => {
|
|
18
|
-
const config = queueConfig.getInfrastructureQueueConfig('validation.responses');
|
|
19
|
-
expect(config).toEqual({
|
|
20
|
-
durable: true,
|
|
21
|
-
arguments: {
|
|
22
|
-
'x-message-ttl': 300000,
|
|
23
|
-
'x-max-length': 10000
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|