@onlineapps/mq-client-core 1.0.83 → 2.0.1-rc.1

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 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": "1.0.83",
3
+ "version": "2.0.1-rc.1",
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 --testPathPattern=tests/integration"
10
+ "test:integration": "jest --config=jest.integration.config.js"
11
11
  },
12
12
  "keywords": [
13
13
  "rabbitmq",
@@ -18,10 +18,11 @@
18
18
  "author": "OnlineApps",
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "amqplib": "^0.10.3",
21
+ "@onlineapps/infra-logger": "2.0.0",
22
+ "@onlineapps/runtime-config": "1.0.3",
22
23
  "ajv": "^8.12.0",
23
- "lodash.merge": "^4.6.2",
24
- "@onlineapps/runtime-config": "1.0.2"
24
+ "amqplib": "^0.10.3",
25
+ "lodash.merge": "^4.6.2"
25
26
  },
26
27
  "devDependencies": {
27
28
  "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 - Final delivery status events for audits/monitoring
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' - Service registration requests
374
- * - type: 'heartbeat' - Periodic health check messages (sent every 10s)
375
- * - type: 'apiDescription' - API specification responses
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)
@@ -13,37 +13,47 @@ const {
13
13
  PermanentPublishError,
14
14
  QueueNotFoundError,
15
15
  classifyPublishError,
16
+ missingInfrastructureQueueMessage,
16
17
  } = require('../utils/publishErrors');
18
+ const { createLogger } = require('@onlineapps/infra-logger');
17
19
  const PublishLayer = require('../layers/PublishLayer');
18
20
  const RecoveryWorker = require('../workers/RecoveryWorker');
19
21
  const PublishMonitor = require('../monitoring/PublishMonitor');
20
22
  const runtimeCfg = require('../config');
21
23
 
24
+ /** AMQP basic.publish fields forwarded from publish() options (amqplib Options.Publish). */
25
+ const AMQP_MESSAGE_PROPERTY_KEYS = [
26
+ 'correlationId',
27
+ 'replyTo',
28
+ 'messageId',
29
+ 'timestamp',
30
+ 'type',
31
+ 'priority',
32
+ 'expiration',
33
+ 'contentType',
34
+ 'contentEncoding',
35
+ 'userId',
36
+ 'appId',
37
+ ];
38
+
22
39
  /**
23
- * Local structured logger (module-owned) to avoid dependency cycles.
24
- *
25
- * IMPORTANT:
26
- * - mq-client-core is a low-level library used by infrastructure-tools.
27
- * - Therefore mq-client-core MUST NOT depend on infrastructure-tools.
40
+ * Merge persistent + headers with whitelisted AMQP message properties for sendToQueue/publish.
41
+ * @param {Object} options
42
+ * @param {boolean} persistent
43
+ * @param {Object} headers
44
+ * @returns {Object}
28
45
  */
29
- function createInfraLogger(service, layer) {
30
- const prefix = (contextId, action) => `[${contextId}:${service}:${layer}:${action}]`;
31
-
32
- const toJson = (data) => {
33
- try {
34
- return JSON.stringify(data);
35
- } catch (e) {
36
- return JSON.stringify({ error: 'failed_to_serialize', message: e.message });
46
+ function buildAmqpMessageProperties(options, persistent, headers) {
47
+ const props = { persistent };
48
+ if (headers && typeof headers === 'object') {
49
+ props.headers = headers;
50
+ }
51
+ for (const key of AMQP_MESSAGE_PROPERTY_KEYS) {
52
+ if (options[key] !== undefined && options[key] !== null && options[key] !== '') {
53
+ props[key] = options[key];
37
54
  }
38
- };
39
-
40
- return {
41
- input: (contextId, action, data) => console.log(prefix(contextId, action), toJson(data)),
42
- output: (contextId, action, data) => console.log(prefix(contextId, action), toJson(data)),
43
- process: (contextId, action, data) => console.log(prefix(contextId, action), toJson(data)),
44
- lifecycle: (contextId, action, data) => console.log(prefix(contextId, action), toJson(data)),
45
- fail: (contextId, action, data) => console.error(prefix(contextId, action), toJson(data)),
46
- };
55
+ }
56
+ return props;
47
57
  }
48
58
 
49
59
  class RabbitMQClient extends EventEmitter {
@@ -132,7 +142,14 @@ class RabbitMQClient extends EventEmitter {
132
142
  this._criticalHealthStartTime = null; // Track when critical health started
133
143
 
134
144
  // Create structured logger for infrastructure logging (module-owned)
135
- this._log = createInfraLogger('mq-client-core', 'transport');
145
+ // ONE implementation of the infrastructure logger, in
146
+ // @onlineapps/infra-logger. A private copy lived here until 2026-08-29,
147
+ // justified as avoiding a dependency cycle — a cycle that is not there:
148
+ // the comment named infrastructure-tools, but the logger is infra-logger,
149
+ // which declares no @onlineapps dependency at all. The copy meanwhile
150
+ // missed the identity fail-fast that 2.0.0 added (change-discipline.md:
151
+ // one fact, one owner).
152
+ this._log = createLogger('mq-client-core', 'transport');
136
153
 
137
154
  // Publish layer (retry + buffer)
138
155
  this._publishLayer = new PublishLayer({
@@ -1283,10 +1300,16 @@ class RabbitMQClient extends EventEmitter {
1283
1300
  try {
1284
1301
  // Ensure queue exists if publishing directly to queue and using default exchange
1285
1302
  if (!exchange) {
1303
+ // RPC / client reply targets: server-named exclusive queues (amq.gen-*) or direct reply-to
1304
+ // exist only on the client's connection — another connection cannot checkQueue them.
1305
+ const skipQueueExistencePrecheck =
1306
+ queue === 'amq.rabbitmq.reply-to' || queue.startsWith('amq.gen-');
1307
+
1286
1308
  // CRITICAL: For infrastructure queues, they should already exist with specific arguments from queueConfig
1287
1309
  // Use queueChannel (regular channel) for checkQueue/assertQueue to avoid RPC reply queue issues
1288
1310
  // If queue doesn't exist (404), we should NOT auto-create it - infrastructure queues must be created explicitly
1289
1311
  // This prevents creating queues with wrong arguments (no TTL) which causes 406 errors later
1312
+ if (!skipQueueExistencePrecheck) {
1290
1313
  try {
1291
1314
  // Ensure queue channel exists and is open (auto-recreates if closed)
1292
1315
  await this._ensureQueueChannel();
@@ -1302,27 +1325,44 @@ class RabbitMQClient extends EventEmitter {
1302
1325
  // Queue exists - proceed to publish
1303
1326
  } catch (checkErr) {
1304
1327
  // If queue doesn't exist (404), this je ERROR – ale rozlišujeme infra vs. non-infra:
1305
- // - Infrastructure queues (workflow.*, registry.*, infrastructure.*, monitoring.*, validation.*)
1306
- // musí být vytvořené infra službami předem → QueueNotFoundError(isInfrastructure=true)
1307
- // - Non-infrastructure queues můžeme (pro business scénáře) vytvořit s default parametry
1328
+ // 404 na chybějící frontu se rozpadá na tři případy podle vlastnictví:
1329
+ // - Infrastrukturní fronty (`queueConfig.isInfrastructureQueue`) zakládají
1330
+ // infra služby předem QueueNotFoundError(kind='infrastructure').
1331
+ // - Business fronty (`queueConfig.isBusinessQueue`, tj. `{service}.{workflow|queue|dlq}`)
1332
+ // zakládá vlastnící služba přes setupServiceQueues() po registraci
1333
+ // → QueueNotFoundError(kind='business'). Publisher je nikdy nevytváří.
1334
+ // - Ostatní jména (ani infra, ani business vzor) se smějí založit
1335
+ // s default parametry, pokud to scope RecoveryWorkeru dovoluje.
1308
1336
  if (checkErr.code === 404) {
1309
- // Check if this is an infrastructure queue using queueConfig
1310
- let isInfraQueue = false;
1311
- try {
1312
- const queueConfig = require('../config/queueConfig');
1313
- isInfraQueue = queueConfig.isInfrastructureQueue(queue);
1314
- } catch (requireErr) {
1315
- // Fallback to pattern matching if queueConfig not available
1316
- isInfraQueue = queue.startsWith('workflow.') ||
1317
- queue.startsWith('registry.') ||
1318
- queue.startsWith('infrastructure.') ||
1319
- queue.startsWith('monitoring.') ||
1320
- queue.startsWith('validation.');
1321
- }
1322
-
1323
- if (isInfraQueue) {
1337
+ // `../config/queueConfig` is a sibling module of this file; the require
1338
+ // cannot fail. It used to sit in a try/catch whose `catch` re-implemented
1339
+ // `isInfrastructureQueue` as an inline prefix list — a fallback
1340
+ // (`architecture-principles.md` §3) that had also gone stale: it was
1341
+ // missing `telemetry.` and `delivery.`, both of which the real
1342
+ // queueConfig classifies as infrastructure. A fallback that answers
1343
+ // differently from the thing it stands in for is a second, wrong source
1344
+ // of truth, so it is gone.
1345
+ const queueConfig = require('../config/queueConfig');
1346
+
1347
+ if (queueConfig.isInfrastructureQueue(queue)) {
1324
1348
  // Jasný, hlasitý signál pro infra služby – fronta chybí, je to programátorská chyba
1325
- throw new QueueNotFoundError(queue, true, checkErr);
1349
+ throw new QueueNotFoundError(queue, true, checkErr, 'infrastructure');
1350
+ }
1351
+
1352
+ // Business queues (`{service}.{workflow|queue|dlq}`) MUST exist before
1353
+ // publish. `sendToQueue()`/`assertQueue()` with default options would
1354
+ // create them with no TTL and no DLQ, and the owning service's later
1355
+ // `setupServiceQueues()` then hits 406 PRECONDITION_FAILED because the
1356
+ // arguments no longer match. The queue is created by the owner AFTER
1357
+ // registration; a publisher that creates it is a programming error.
1358
+ //
1359
+ // The rule and this exact wording came from
1360
+ // `conn-infra-mq/src/transports/rabbitmqClient.js:264-277` — a transport
1361
+ // that nothing in `src/` instantiated, so the guard protected nobody
1362
+ // while this file, the one every client actually runs, auto-created the
1363
+ // queue and only printed a warning.
1364
+ if (queueConfig.isBusinessQueue(queue)) {
1365
+ throw new QueueNotFoundError(queue, false, checkErr, 'business');
1326
1366
  }
1327
1367
  // For non-infrastructure queues, check if queue creation is allowed before auto-creating
1328
1368
  // Check recovery scope and filter
@@ -1358,6 +1398,7 @@ class RabbitMQClient extends EventEmitter {
1358
1398
  throw checkErr;
1359
1399
  }
1360
1400
  }
1401
+ }
1361
1402
  // Publish to queue using ConfirmChannel (for publisher confirms)
1362
1403
  // Channel is guaranteed to be open (ensured above)
1363
1404
  console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Sending message to queue "${queue}" (size: ${buffer.length} bytes)`);
@@ -1383,7 +1424,11 @@ class RabbitMQClient extends EventEmitter {
1383
1424
 
1384
1425
  // Send message with callback
1385
1426
  try {
1386
- originalChannel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
1427
+ originalChannel.sendToQueue(
1428
+ queue,
1429
+ buffer,
1430
+ buildAmqpMessageProperties(options, persistent, headers),
1431
+ (err, ok) => {
1387
1432
  callbackInvoked = true;
1388
1433
  this._clearTimeout(timeout);
1389
1434
 
@@ -1483,7 +1528,12 @@ class RabbitMQClient extends EventEmitter {
1483
1528
  reject(new Error(`Exchange publish confirmation timeout for exchange "${exchange}" after ${this._publishConfirmationTimeout}ms`));
1484
1529
  }, this._publishConfirmationTimeout);
1485
1530
 
1486
- this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
1531
+ this._channel.publish(
1532
+ exchange,
1533
+ routingKey,
1534
+ buffer,
1535
+ buildAmqpMessageProperties(options, persistent, headers),
1536
+ (err, ok) => {
1487
1537
  this._clearTimeout(timeout);
1488
1538
  if (err) {
1489
1539
  console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
@@ -1617,10 +1667,7 @@ class RabbitMQClient extends EventEmitter {
1617
1667
  console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Infrastructure queue ${queue} exists (consumer will proceed)`);
1618
1668
  } catch (checkErr) {
1619
1669
  if (checkErr.code === 404) {
1620
- throw new Error(
1621
- `Infrastructure queue ${queue} is missing. ` +
1622
- 'Ownership rule: infrastructure queues must be created by their owning infrastructure service via initInfrastructureQueues().'
1623
- );
1670
+ throw new Error(missingInfrastructureQueueMessage(queue));
1624
1671
  }
1625
1672
  throw checkErr;
1626
1673
  }
@@ -36,18 +36,63 @@ 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 baseMessage = isInfrastructure
42
- ? `Cannot publish to infrastructure queue ${queueName}: queue does not exist. Infrastructure queues must be created explicitly with correct arguments (TTL, max-length, etc.) before publishing.`
43
- : `Queue ${queueName} does not exist`;
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
 
70
+ /**
71
+ * The message a CONSUMER gets when an infrastructure queue it must attach to
72
+ * does not exist (TODO §22).
73
+ *
74
+ * It used to state the rule and leave the reader stuck: it named
75
+ * `initInfrastructureQueues()`, a function this package does not export and
76
+ * never has. `mq-client-core` sits deliberately BELOW `infrastructure-tools`
77
+ * and must not depend on it, so naming the owning package is the only thing
78
+ * that can bridge the two — and without it the fix is unfindable.
79
+ *
80
+ * @param {string} queueName
81
+ * @returns {string}
82
+ */
83
+ function missingInfrastructureQueueMessage(queueName) {
84
+ if (!queueName) {
85
+ throw new Error('[publishErrors] queueName is required - Expected the name of the missing infrastructure queue');
86
+ }
87
+
88
+ return `[RabbitMQClient] Infrastructure queue ${queueName} is missing - `
89
+ + 'Expected: it to be created by the infrastructure service that owns it, before any consumer starts. '
90
+ + 'Consumers never create infrastructure queues: sendToQueue() and consume() would declare them with '
91
+ + 'default arguments (no TTL, no max-length) and every later declaration would fail with 406. '
92
+ + 'Fix: start the owning infrastructure service, which calls initInfrastructureQueues() from '
93
+ + '@onlineapps/infrastructure-tools.';
94
+ }
95
+
51
96
  /**
52
97
  * Best-effort klasifikace chyb z publishu do specializovaných typů.
53
98
  * Vrací původní chybu, pokud neodpovídá žádnému známému patternu.
@@ -97,6 +142,7 @@ function classifyPublishError(err) {
97
142
  }
98
143
 
99
144
  module.exports = {
145
+ missingInfrastructureQueueMessage,
100
146
  TransientPublishError,
101
147
  PermanentPublishError,
102
148
  QueueNotFoundError,
@@ -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: connection recovery, channel recreation, consumer re-registration, queue creation (pokud není infra queue)
11
- *
12
- * Queue Creation:
13
- * - For business queues: delegates to queueCreationCallback if provided (e.g., QueueManager.setupServiceQueues)
14
- * - Falls back to direct queue creation if callback not provided (for backward compatibility)
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 - create if scope allows
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
-