@onlineapps/mq-client-core 3.0.0 → 3.1.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/CHANGELOG.md +72 -1
- package/README.md +64 -3
- package/package.json +3 -3
- package/src/BaseClient.js +8 -0
- package/src/config/queueConfig.js +84 -0
- package/src/index.js +21 -9
- package/src/transports/rabbitmqClient.js +69 -4
- package/src/utils/redactCredentials.js +13 -45
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,77 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.1.0] — 2026-09-15
|
|
8
|
+
|
|
9
|
+
### Added — `topology` nese i tři telemetrické fronty (d.461)
|
|
10
|
+
|
|
11
|
+
`module.exports.topology` vystavuje `telemetryLogsQueue`, `telemetryMetricsQueue`,
|
|
12
|
+
`telemetryTracesQueue`, složené ze sekce `telemetry` v `queueConfig` (tatáž
|
|
13
|
+
deklarace, ne kopie). Monitoring, jediný konzument těch front, může zrušit
|
|
14
|
+
svoje privátní literály a číst jména odtud; `src/config/queueConfig` zůstává
|
|
15
|
+
interní cestou. README § topology popisuje celý export.
|
|
16
|
+
|
|
17
|
+
### Fixed — `performHealthCheck()` hlásí stav, nemění ho (d.467)
|
|
18
|
+
|
|
19
|
+
Kontrola spouštěla obnovovací cyklus bezpodmínečně: `_reconnectWithBackoff()`
|
|
20
|
+
nastaví `_reconnecting` synchronně, `isConnected()` ho čte, a tak zdravý klient
|
|
21
|
+
reportoval vlastní vedlejší účinek (`healthy: false`, `Connection is closed or
|
|
22
|
+
missing`, `isConnected()` true → false) a obnova skutečně vyměnila živý socket
|
|
23
|
+
za nový. Teď se obnova spouští jen u odstaveného klienta — otázku klade jednomu
|
|
24
|
+
zdroji živosti (`isConnected()` nad `_connectionAlive`), tam, kde ji kladou
|
|
25
|
+
`_ensurePublisherChannel()` a `_ensureConsumerChannel()`. Líný kontrakt d.339
|
|
26
|
+
zůstává: odstavený klient se health checkem dál vrací do provozu. Repro nad
|
|
27
|
+
živým brokerem s `reconnectEnabled: true` (starší sady o health checku stavěly
|
|
28
|
+
klienta s `reconnectEnabled: false`, proto vadu nikdy neviděly).
|
|
29
|
+
|
|
30
|
+
### Fixed — `redactUrl` se bere z `@onlineapps/logger-contract`, lokální kopie pryč (d.455)
|
|
31
|
+
|
|
32
|
+
`src/utils/redactCredentials.js` držel vlastní `redactUrl`
|
|
33
|
+
a `UNPARSEABLE_PLACEHOLDER`. Od `@onlineapps/logger-contract` 1.2.0 (d.453) je
|
|
34
|
+
vlastní core vrstva, takže je tenhle modul importuje a `src/index.js` je
|
|
35
|
+
**re-exportuje identitou** — veřejné API se nemění, dependants dál píší
|
|
36
|
+
`require('@onlineapps/mq-client-core').redactUrl`. Zdejší zůstává jen
|
|
37
|
+
`redactConnectionTarget`: fail-fast obal nad deklarovaným config klíčem `host`,
|
|
38
|
+
který v core vrstvě nemá co dělat (vyhazuje tam, kde `redactUrl` vrací
|
|
39
|
+
placeholder — d.292 §2/§3).
|
|
40
|
+
|
|
41
|
+
Čtyři otázky ke smazané deklaraci (`change-discipline.md` § Removing):
|
|
42
|
+
|
|
43
|
+
1. **Proč vznikla** — d.448 sjednotila sémantiku (celé userinfo pryč, ne jen
|
|
44
|
+
heslo) poté, co se na `api_service_hello` 2026-09-07 změřilo heslo brokeru
|
|
45
|
+
ve stdout a přes monitoring i v Loki. Core domov tehdy neexistoval, takže
|
|
46
|
+
funkce zůstala tady.
|
|
47
|
+
2. **Která část koncepce ji nesla** — `change-discipline.md` § One rail per
|
|
48
|
+
concern ji naopak zakazuje: tři kopie jednoho pravidla (tady,
|
|
49
|
+
`service-common`, `conn-orch-registry`). Ta část koncepce stojí dál.
|
|
50
|
+
3. **Proč ji dnes nikdo nečte** — `@onlineapps/logger-contract` 1.2.0 exportuje
|
|
51
|
+
`redactUrl` i `UNPARSEABLE_PLACEHOLDER` a všechny tři dependants míří na něj.
|
|
52
|
+
4. **Je náhrada koncepčnější** — ano: `logger-contract` je L1 bez `@onlineapps`
|
|
53
|
+
závislostí, takže na něj dosáhne i kategorie `orchestration`, které brána G7
|
|
54
|
+
odmítá pin do kategorie `runtime`.
|
|
55
|
+
|
|
56
|
+
**Změna chování na jednom okraji:** zdejší kopie vracela `parsed.href` bez
|
|
57
|
+
úpravy, logger-contract navíc utne lomítko, které WHATWG dopisuje prázdné cestě
|
|
58
|
+
u *special* schemat (`http`, `https`, `ws`, `wss`, `ftp`, `file`). Na `amqp` /
|
|
59
|
+
`amqps` — jediných, se kterými tenhle klient pracuje — jsou obě implementace
|
|
60
|
+
bajtově shodné; `http://u:p@host:8080` nově vrátí `http://host:8080` místo
|
|
61
|
+
`http://host:8080/`.
|
|
62
|
+
|
|
63
|
+
Sada `tests/unit/redact-credentials.test.js`: blok se sémantikou `redactUrl`
|
|
64
|
+
case-by-case odešel (drží ho logger-contract), místo něj drží identitu
|
|
65
|
+
re-exportu, nepřítomnost druhé koleje v `utils/redactCredentials`
|
|
66
|
+
(`Object.keys` = `['redactConnectionTarget']`), shodu výstupu
|
|
67
|
+
`redactConnectionTarget` s `loggerContract.redactUrl` a cestu selhání.
|
|
68
|
+
Zmizely i mrtvé citace na `shared/service-common/src/redactUrl.js`
|
|
69
|
+
a `shared/connector/conn-orch-registry/src/redactUrl.js` — oba soubory už
|
|
70
|
+
neexistují.
|
|
71
|
+
|
|
72
|
+
## [3.0.1] — 2026-09-14
|
|
73
|
+
|
|
74
|
+
- **Fixed: `consume()` accepts a TERMINAL queue — one this configuration dead-letters TO — instead of demanding a dead-letter route it must not have.** The d.259 gate refuses a queue for which `queueConfig` declares no `x-dead-letter-exchange`/`-routing-key`, because the policy ends a spent message with `nack(requeue=false)` and the broker drops what it cannot move. True of every working queue — and false of the queues that RECEIVE those moves: `workflow.failed` is named in `x-dead-letter-routing-key` by `workflow.init` and `workflow.control`, `<service>.dlq` by every business template, `workflow.dlq` and `delivery.dlq` by their own families, and the confirmed topology leaves exactly those without a route of their own (`api/docs/governance/confirmations/mq-consumer-contract.md` 002 point 2, 003 point 2 — "workflow.failed and workflow.dlq themselves keep no route"). A dead-letter route on a dead-letter queue is a loop or a chain with no end. So `workflow.failed` — the operator's queue, read by the delivery dispatcher — was unconsumable: `[BaseClient] Cannot consume from queue "workflow.failed": queueConfig declares no dead-letter route`. The gate now asks a second question, `queueConfig.isDeadLetterTarget(queue)`, **derived from the templates** (the set of every declared `x-dead-letter-routing-key`, `{service}` placeholder read through the same `templateShape()` the business templates use) rather than from a list of names that would have to be edited in step with them — the defect d.278 removed from `matchBusinessTemplate()`. The `.dlq` family needs no rule of its own: every `.dlq` this configuration declares is already the target of a declared routing key (measured 2026-09-14). Every other queue with no route is refused exactly as before, with the same sentence. A message that spends its budget on a terminal queue is rejected the same way and then DROPPED by the broker — the end of the chain is the end — never requeued for ever, and the loss is now logged at error level (`Message discarded on terminal queue "<queue>"`, with the queue, the attempts and the error) before the `message_dlq` event is published. Tests: new unit `consume-terminal-queue.test.js` (10 — the derivation from the declared keys, the terminal names and the non-terminal control, `consume('workflow.failed')` and `consume('<svc>.dlq')` attaching, the three refusals that must not change with the refusal verbatim, the discard with its log line and meta, and a routed queue as the control that says nothing about a discard); integration `dead-letter-route-required.integration.test.js` (+2 against the live broker: a message rejected from `<svc>.queue` reaches `<svc>.dlq` and is DELIVERED to a consumer there with `x-death[0].reason = 'rejected'`, and a handler failing on that terminal queue leaves the queue empty with no second delivery after 2 s — the drop no mock can measure). Three tests that encoded the old rule follow the new one: the `<svc>.dlq` refusal leg of `consume-dead-letter-route-required.test.js` (its subject moved to the new file), and the two integration control queues that used a dead-letter queue as "the queue with no route" (`delivery-events-lookup` → `monitoring.workflow`, `queue-declaration-options-one-rail` → `<svc>.events`).
|
|
75
|
+
|
|
76
|
+
## [3.0.0] — 2026-09-14
|
|
77
|
+
|
|
7
78
|
- **Changed (BREAKING for a reader of the log, not for a caller): the credential redaction removes the WHOLE userinfo, not just the password.** `utils/redactCredentials.js` masked the password (`amqp://oa_dev:***@queuer:5672`) and kept the account name, while the platform's two other redactors — `shared/service-common/src/redactUrl.js` (d.245/d.245b) and `shared/connector/conn-orch-registry/src/redactUrl.js` (d.446, whose header says plainly that it stayed local because this package masked the password only) — drop the userinfo entirely. One concern, two semantics, which is two rails (`.claude/rules/change-discipline.md` § One rail per concern). The platform one wins: `amqp://queuer:5672/vhost?heartbeat=30`, the account name gone with the password, because a name is the other half of the same credential and a log line needs neither. A value that is not a parseable URL is answered with `<unparseable-url>` instead of being echoed — previously a non-URL string came back verbatim (`api_services_queuer`) or with a `***@` prefix, and an unparseable value is exactly the case where nobody can say whether it holds a credential. `redactConnectionTarget()` still THROWS on a non-string: it guards a declared config key, where a wrong type is a boot-time defect. Tests: unit `redact-credentials.test.js` (rewritten to the new semantics, +6 over the package: the userinfo cases, the transport's `?heartbeat` query as a control, the placeholder cases and the export identity), `connect-credentials-not-logged.test.js` and `one-owner-a-floor-down.test.js` (both now assert the account name is absent too), integration `credential-redaction.integration.test.js` (+2: the LIVE broker accepts the real credential while no logger argument carries either half of it, and a real 403 refusal leaks nothing into the error, the stack or the log) (d.448).
|
|
8
79
|
- **Added:** `redactUrl` and `UNPARSEABLE_PLACEHOLDER` are exports of `src/index.js`. The dependants that render an AMQP URL into a log line or an error message (`conn-orch-registry`, `conn-infra-mq`) each carry a local copy of the same function; a copy can only be replaced by an import if the package declares one (d.448).
|
|
9
80
|
- **Fixed:** a reconnect wait ends only on an error that carries NO publish classification. d.435 taught `_waitForReconnection()` to skip a RETRYABLE publish failure, which left the same defect standing one class of error further out: a `PermanentPublishError` or a `QueueNotFoundError` from a concurrent publish still rejected every waiter, although "no retry will help this message" says nothing about whether the link is coming back. The condition is now the PRESENCE of the classification, not its verdict — `utils/publishErrors.js` sets `retryable` on every error it produces, so an error that never went through that rail carries none, and that absence is the whole test (plus the transport's own typed close, which never ended the wait). No second list of codes. Tests: unit `wait-for-reconnection-not-ended-by-a-foreign-publish.test.js` (+4, both permanent classifications built by the real rail, controls for an unclassified error and for the typed close), integration `…integration.test.js` (+1: the live broker refuses a publish to a name no declaration owns with a real 404, and that real `QueueNotFoundError` is put on the channel during a real outage — a permanent classification cannot arise while the link is down, where everything classifies transient) (d.435b).
|
|
@@ -30,7 +101,7 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
30
101
|
- **Removed (BREAKING: `persistentBufferEnabled` and `persistentRedisClient` are refused by name):** the persistent publish buffer, which never existed. `buffer/RedisBuffer.js` was a placeholder — `add()` stored nothing, `flush()` returned 0 — and `MessageBuffer.add()` returned after that branch without falling back (the stub did not throw, so there was nothing to fall back from). With `persistentBufferEnabled: true` a critical-priority message that failed transiently was therefore DROPPED, while the same message at normal priority was held in memory and replayed after the reconnect: the key inverted its own promise. The package's own concept node already said so in writing (`docs/architecture/mq-publish-reliability.md` § Buffering: "RedisBuffer is a stub … Leave persistentBufferEnabled at its default") — a mechanism documented as not existing (`automation-gates.md` §5). The four questions of `change-discipline.md` § Removing are answered in the commit and in `tests/unit/persistent-buffer-removed.test.js`: it arrived with the publish-reliability layer (`3588b148`) as a hook for a future extension; no part of the concept requires persistence; nothing ever set either key outside one test (measured across `api`, `api_biz`, `infra`, `fe_adminui`, 2026-09-14); and what stands in its place — one in-memory buffer holding every priority — is more conceptual than a switch that loses messages. `getBufferState()` is therefore `{ size, inMemory }` (the `persistent` count could only ever be 0), `MessageBuffer.flush()` returns the number replayed instead of `{ inMemory, persistent }`, and both key names now fail the constructor with a sentence saying the mechanism is gone rather than a name to rename them to (d.343).
|
|
31
102
|
- **Fixed (BREAKING: `publish()` no longer creates the exchange it publishes to, and `options.exchangeType` is gone):** an exchange is NAMED by a publisher, never declared by one. `_publishOnce()` asserted the exchange before EVERY publish, with arguments it made up on the spot — the type from `options.exchangeType || 'direct'` and the durability from the PUBLISHING client's own config — so the publisher decided the topology and two differently configured publishers declared one exchange two ways. Measured on the live broker: a client configured `durable: false` publishing to a fanout its owner declared durable is refused with `406 PRECONDITION-FAILED … inequivalent arg 'type' … received 'direct' but current is 'fanout'`, on a message that had nothing wrong with it. The owner's decision names this exact defect one object down — "the endpoint's own `{ durable: true }` assert is a second declaration and already yields 406 against the library's" (`docs/governance/confirmations/mq-consumer-contract.md` 003, point 4). Exchanges are declared once in `config/queueConfig.js` and asserted by their owners (measured: every exchange the platform publishes to already is), so an exchange nobody declared is now a broker-side 404 on the publish rather than an exchange invented by whoever published first — the same rule as `README.md` § Queue ownership states for queues. `options.exchangeType` existed for that one assert and left with it, including its only caller inside the library, `monitoring-publish.js` (d.342).
|
|
32
103
|
- **Added:** `BaseClient.isConnectionFatal()` and `BaseClient.performHealthCheck()` — the last two methods a service could not reach. Same hole d.296 closed for the five counters, different question: both were methods of the TRANSPORT alone, and `new RabbitMQClient` is called in exactly one place in the whole workspace (`transports/transportFactory.js`, inside this library), so a service holding a `BaseClient` saw `isConnected() === false` with no way to learn whether to wait or to restart, and the manual probe `docs/architecture/rabbitmq-channel-lifecycle.md` documents was callable on nothing it holds. The transport keeps the computation; the client delegates. `performHealthCheck()` follows the five counters and refuses before `connect()` built a transport; `isConnectionFatal()` deliberately does not — a client that never connected has a true answer (no), and a healthcheck must not be answered by an exception. `onChannelClose` stays the transport's: it is an internal hook on a channel's lifetime, not a question a service asks. The d.296 gate now covers both new counterparts (d.341).
|
|
33
|
-
- **Removed (BREAKING for a caller importing the transport):** `require('@onlineapps/mq-client-core').RabbitMQClient`. The four questions, answered before deleting: it came in with the library's extraction (`7a47b2fa`), as part of publishing the module's surface; no part of the concept ever carried it — `docs/architecture/mq-publish-reliability.md` describes the transport as what `BaseClient` selects, never as what a caller constructs; nothing reads it because `transportFactory` has been the one constructor since that same commit (measured across `api_biz/*`, `infra/*`, `shared/*`: zero uses outside this package); and what stands in its place is more conceptual, not less — the factory picks the transport from `type` (Configuration Over Convention), while constructing it by hand bypasses config composition, schema validation and the instance registry `disconnectAll()` is built on (d.341).
|
|
104
|
+
- **Removed (BREAKING for a caller importing the transport):** `require('@onlineapps/mq-client-core').RabbitMQClient`. The four questions, answered before deleting: it came in with the library's extraction (`7a47b2fa`), as part of publishing the module's surface; no part of the concept ever carried it — `docs/architecture/mq-publish-reliability.md` describes the transport as what `BaseClient` selects, never as what a caller constructs; nothing reads it because `transportFactory` has been the one constructor since that same commit (measured across `api_biz/*`, `infra/*`, `shared/*`: zero uses outside this package — that scope skipped `api/tests/**`, which carried 13 uses in 3 suites; INFRA retires them as a second rail, measured 2026-09-15); and what stands in its place is more conceptual, not less — the factory picks the transport from `type` (Configuration Over Convention), while constructing it by hand bypasses config composition, schema validation and the instance registry `disconnectAll()` is built on (d.341).
|
|
34
105
|
- **Fixed (BREAKING: a spent reconnect budget is no longer the end of the client):** after the attempt budget of one recovery cycle runs out against a broker that never answered, the client **stands down** — `connection:standby` with `{ cycle, cyclesMax, attempts, lastError, timestamp }`, no timer, no socket, `isConnected()` false and `isConnectionFatal()` FALSE — and the next `publish()`, `consume()` or `performHealthCheck()` starts the next cycle. Nothing polls in between, so a client nobody uses costs nothing while the broker is away. Until now one spent budget set the fatal flag and the client refused every operation for the life of the process, which the README taught as "dead until the process restarts"; that is the "give up forever" the owner forbade (`docs/governance/confirmations/mq-client-lifecycle-contract.md` 001 point 3), and it meant a broker restart longer than `maxReconnectAttempts × reconnectMaxDelay` needed a process restart to undo. One rail: the lazy retry re-enters the same `_reconnectWithBackoff()`, there is no second loop (d.339).
|
|
35
106
|
- **Added:** `maxReconnectCycles` (default 3, env `RABBITMQ_MAX_RECONNECT_CYCLES`) — how many recovery cycles the client runs before the connection IS permanently lost. It is what keeps the lazy retry finite; the value is the one `maxDeliveryAttempts` already answers the same question with (how many whole retries one thing gets), and two answers to one question must not differ (d.339).
|
|
36
107
|
- **Changed (BREAKING for a caller reading the fatal error):** `connection:fatal` says WHICH of the two endings it is — `reason: 'broker-refused'` or `'cycles-spent'` — and carries `cycles` beside `attempts`, with a different sentence for each. A broker that ANSWERED and refused (403 `ACCESS-REFUSED`, 530 `NOT-ALLOWED`, 406 `PRECONDITION-FAILED`) is fatal on the spot, with no further attempt spent on an answer that cannot change: measured against amqplib 0.10.9 on the live broker (2026-09-14), such a refusal arrives as a plain `Error` with `code` undefined and the AMQP reply code rendered into the message, so the reply code is what is read — the broker's protocol constant, never wording invented here. Not recognised, and said rather than implied: a wrong vhost reaches amqplib as `Expected ConnectionOpenOk; got <ConnectionClose channel:0>`, so it is treated as an outage and ends at the cycle cap (d.339).
|
package/README.md
CHANGED
|
@@ -216,6 +216,24 @@ broker cannot be asked, because `checkQueue()` answers with `queue.declare-ok`,
|
|
|
216
216
|
carries `{ queue, messageCount, consumerCount }` and no arguments (measured, 2026-09-12).
|
|
217
217
|
`queueConfig.getDeadLetterRoute(queue)` is the exported form of the same answer.
|
|
218
218
|
|
|
219
|
+
**A TERMINAL queue is consumed, and it is not an exception to that rule but its other
|
|
220
|
+
half.** A queue something else dead-letters TO — `<service>.dlq`, `workflow.failed`,
|
|
221
|
+
`workflow.dlq`, `delivery.dlq` — declares no onward route because the topology says it
|
|
222
|
+
must not have one: a dead-letter route on a dead-letter queue closes a loop or starts a
|
|
223
|
+
chain with no end (`api/docs/governance/confirmations/mq-consumer-contract.md` 002
|
|
224
|
+
point 2, 003 point 2). So the gate asks a second question, `queueConfig.isDeadLetterTarget(queue)`,
|
|
225
|
+
derived from the templates themselves — the set of every `x-dead-letter-routing-key` they
|
|
226
|
+
declare, `{service}` placeholder and all — never from a list of names. Every OTHER queue
|
|
227
|
+
with no route is refused exactly as before, with the same sentence.
|
|
228
|
+
|
|
229
|
+
What a terminal queue does NOT get is a different delivery policy: the budget is counted
|
|
230
|
+
the same way, and a message that spends it is rejected with `nack(requeue=false)` the same
|
|
231
|
+
way. The difference is what the broker then does with it — there is nowhere to move it to,
|
|
232
|
+
so it is **dropped**. That loss is deliberate (the alternative is the endless requeue this
|
|
233
|
+
policy exists to end) and it is never silent: the client logs it at error level, naming
|
|
234
|
+
the queue, the attempts and the error, and the `message_dlq` event is published as for any
|
|
235
|
+
other rejection.
|
|
236
|
+
|
|
219
237
|
```javascript
|
|
220
238
|
await client.consume(queue, async (msg, delivery) => {
|
|
221
239
|
// delivery = { attempt, maxAttempts, isFinalAttempt }
|
|
@@ -527,12 +545,18 @@ const { queueConfig } = require('@onlineapps/mq-client-core');
|
|
|
527
545
|
queueConfig.isInfrastructureQueue('workflow.init'); // true
|
|
528
546
|
queueConfig.isBusinessQueue('emailer.workflow'); // true
|
|
529
547
|
queueConfig.getInfrastructureQueueConfig('workflow.init'); // { durable, arguments }
|
|
548
|
+
queueConfig.getDeadLetterRoute('workflow.init'); // { exchange: '', routingKey: 'workflow.failed' }
|
|
549
|
+
queueConfig.isDeadLetterTarget('workflow.failed'); // true — the end of that chain
|
|
530
550
|
```
|
|
531
551
|
|
|
532
552
|
- `workflow.failed` carries no `x-message-ttl`. It is read by an operator, not by a
|
|
533
553
|
service, and it has no dead-letter exchange, so a TTL there does not move a message
|
|
534
554
|
anywhere — it deletes it. See
|
|
535
555
|
`api/docs/governance/confirmations/mq-consumer-contract.md` 002.
|
|
556
|
+
- `isDeadLetterTarget()` answers "does this configuration dead-letter TO this name",
|
|
557
|
+
derived from the declared routing keys rather than from a list. It is what lets
|
|
558
|
+
`consume()` attach to `workflow.failed` and to any `<service>.dlq` while every other
|
|
559
|
+
queue with no declared route stays refused — see § `consume()` — the delivery contract.
|
|
536
560
|
|
|
537
561
|
**That require is the contract.** Reaching into
|
|
538
562
|
`@onlineapps/mq-client-core/src/config/queueConfig` is an internal path the package
|
|
@@ -541,6 +565,42 @@ file moves. Callers holding the deep path switch to the named export; callers th
|
|
|
541
565
|
receive `queueConfig` by injection (`initInfrastructureQueues` in
|
|
542
566
|
`@onlineapps/infrastructure-tools`) pass it from here.
|
|
543
567
|
|
|
568
|
+
### topology — platform queue names, as a declared export
|
|
569
|
+
|
|
570
|
+
A name that travels ON THE WIRE between services has to have ONE owner, or it has one
|
|
571
|
+
copy per service. `topology` is that owner: a frozen object of the platform names, each
|
|
572
|
+
one composed from the entry that DECLARES it in `queueConfig`, so a rename there moves
|
|
573
|
+
the export with it and an entry that is gone fails fast instead of handing back a
|
|
574
|
+
plausible string.
|
|
575
|
+
|
|
576
|
+
```javascript
|
|
577
|
+
const { topology } = require('@onlineapps/mq-client-core');
|
|
578
|
+
|
|
579
|
+
topology.infrastructureHealthEventsExchange; // 'infrastructure.health.events'
|
|
580
|
+
topology.telemetryLogsQueue; // 'telemetry.logs.queue'
|
|
581
|
+
topology.telemetryLogsQueue = 'mine'; // TypeError — the object is frozen
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
| Key | Name on the wire | Who publishes / who drains it |
|
|
585
|
+
|---|---|---|
|
|
586
|
+
| `infrastructureHealthEventsExchange` | `infrastructure.health.events` | Fanout. The registry publishes (`api/infra/api_services_registry/src/services/infrastructureEventPublisher.js`); the two queues below are bound to it. |
|
|
587
|
+
| `monitoringInfrastructureHealthEventsQueue` | `monitoring.infrastructure.health.events` | The monitoring consumer's queue on that fanout (`api/infra/api_monitoring/src/consumer/topology.js`, `MONITORING_INFRA_EVENT_QUEUE`). |
|
|
588
|
+
| `deliveryHealthEventsQueue` | `delivery.health.events` | The delivery endpoint binds it to the fanout and consumes it for WS push (`api/infra/api_delivery_endpoint/src/index.js`, `bindQueueToFanout(config.healthEventsQueue, …)`). |
|
|
589
|
+
| `deliveryResourceEventsQueue` | `delivery.resource.events` | Bound to the `monitoring.resource` exchange by `resource.changed.#`; published by the monitoring watcher `resourceChangedPublisher.js`, drained by `ResourceChangedConsumer.js` in the delivery endpoint. |
|
|
590
|
+
| `telemetryLogsQueue` | `telemetry.logs.queue` | Bound to `telemetry.exchange`, which every service publishes to with its own name as the routing key; drained by the monitoring consumer (`api/infra/api_monitoring/src/consumer/index.js`, `topology.TELEMETRY_LOGS_QUEUE`). |
|
|
591
|
+
| `telemetryMetricsQueue` | `telemetry.metrics.queue` | The same exchange, the same consumer — metric samples. |
|
|
592
|
+
| `telemetryTracesQueue` | `telemetry.traces.queue` | The same exchange, the same consumer — spans. |
|
|
593
|
+
|
|
594
|
+
- **Frozen on purpose.** An importer must not be able to rewrite platform topology: a
|
|
595
|
+
name overridden on one side of a fanout loses the events silently, which is why batch
|
|
596
|
+
243g-C removed the per-service env override for the health-events exchange in the
|
|
597
|
+
first place.
|
|
598
|
+
- **The named export is the contract.** `@onlineapps/mq-client-core/src/config/queueConfig`
|
|
599
|
+
is an internal path this package never promised — it resolves today and breaks the day
|
|
600
|
+
the file moves. Import `topology` (or `queueConfig`) from the package root.
|
|
601
|
+
- A service that still carries one of these names as a private literal is carrying the
|
|
602
|
+
spelling of a fact declared here; importing it changes the wire by nothing at all.
|
|
603
|
+
|
|
544
604
|
### redactUrl — a connection URL that is safe to log
|
|
545
605
|
|
|
546
606
|
`RABBITMQ_URL` carries the broker account, so anything that renders it verbatim
|
|
@@ -565,9 +625,10 @@ redactUrl(undefined); // '<unparseable-url>'
|
|
|
565
625
|
goes through `redactConnectionTarget()` instead, which refuses a non-string
|
|
566
626
|
outright, because `host` is a declared config key and a wrong type there is a
|
|
567
627
|
boot-time defect.
|
|
568
|
-
- It is the SAME function this package uses internally
|
|
569
|
-
|
|
570
|
-
|
|
628
|
+
- It is the SAME function this package uses internally. Implementaci vlastní
|
|
629
|
+
`@onlineapps/logger-contract` (vrstva L1, dosáhne na ni každá kategorie);
|
|
630
|
+
tenhle balíček ji od d.455 jen **re-exportuje identitou**, aby dependant,
|
|
631
|
+
který renderuje URL brokeru, importoval funkci místo kopie.
|
|
571
632
|
|
|
572
633
|
## Architecture
|
|
573
634
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/mq-client-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Core MQ client library for RabbitMQ - shared by infrastructure services and connectors",
|
|
5
5
|
"oa": {
|
|
6
6
|
"category": "connector"
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"author": "OnlineApps",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@onlineapps/logger-contract": "1.
|
|
27
|
-
"@onlineapps/runtime-config": "1.
|
|
26
|
+
"@onlineapps/logger-contract": "1.3.0",
|
|
27
|
+
"@onlineapps/runtime-config": "1.2.0",
|
|
28
28
|
"ajv": "^8.12.0",
|
|
29
29
|
"amqplib": "^0.10.3"
|
|
30
30
|
},
|
package/src/BaseClient.js
CHANGED
|
@@ -502,6 +502,14 @@ class BaseClient {
|
|
|
502
502
|
* `message_dlq` event would report a destination nothing routed to. The fix is
|
|
503
503
|
* always to declare the route, never to consume without one.
|
|
504
504
|
*
|
|
505
|
+
* The one queue that demand cannot be made of is the END of a chain — a queue
|
|
506
|
+
* this configuration dead-letters TO (`<service>.dlq`, `workflow.failed`). The
|
|
507
|
+
* topology withholds its route deliberately, so the gate reads a second,
|
|
508
|
+
* derived answer (`queueConfig.isDeadLetterTarget()`) and lets its consumer
|
|
509
|
+
* attach (d.452). A message that spends its budget there is rejected as
|
|
510
|
+
* anywhere else and then dropped by the broker, with an error line naming the
|
|
511
|
+
* loss — never an endless requeue. README § `consume()` — the delivery contract.
|
|
512
|
+
*
|
|
505
513
|
* @param {string} queue - Name of the queue to consume from.
|
|
506
514
|
* @param {function(Object, {attempt: number, maxAttempts: number, isFinalAttempt: boolean}): Promise<void>} messageHandler -
|
|
507
515
|
* Async function to process each message. Its second argument says which
|
|
@@ -100,6 +100,10 @@ const TEMPLATE_PLACEHOLDER = /^\{[^.{}]+\}$/;
|
|
|
100
100
|
* service name, with `null` where the template accepts anything (`registry.events`
|
|
101
101
|
* -> `['registry', 'events']`; a key writing `registry.{kind}` would give
|
|
102
102
|
* `['registry', null]`).
|
|
103
|
+
*
|
|
104
|
+
* A declared dead-letter routing key is written in the same notation
|
|
105
|
+
* (`{service}.dlq`), so `isDeadLetterTarget()` reads it through this one function
|
|
106
|
+
* rather than through a second placeholder rule.
|
|
103
107
|
*/
|
|
104
108
|
const templateShape = (template) =>
|
|
105
109
|
template.split('.').map((part) => (TEMPLATE_PLACEHOLDER.test(part) ? null : part));
|
|
@@ -169,6 +173,41 @@ const declaredInfrastructureNames = (config) => {
|
|
|
169
173
|
return names;
|
|
170
174
|
};
|
|
171
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Every name this configuration dead-letters TO — the END of a chain.
|
|
178
|
+
*
|
|
179
|
+
* Derived from the declarations themselves: the value of every
|
|
180
|
+
* `x-dead-letter-routing-key` any section writes, placeholders and all
|
|
181
|
+
* (`{service}.dlq`, `workflow.failed`, `workflow.dlq`, `delivery.dlq`). A list of
|
|
182
|
+
* names typed here would have to be edited in step with the templates and could
|
|
183
|
+
* therefore disagree with them — the defect d.278 removed from
|
|
184
|
+
* `matchBusinessTemplate()`, and the reason `declaredInfrastructureNames()` above
|
|
185
|
+
* derives its set the same way.
|
|
186
|
+
*
|
|
187
|
+
* The `.dlq` family needs no rule of its own: every `.dlq` this configuration
|
|
188
|
+
* declares — `workflow.dlq`, `delivery.dlq`, `{service}.dlq` — is already the
|
|
189
|
+
* target of a declared routing key (measured 2026-09-14), so the set covers it.
|
|
190
|
+
* A `.dlq` nothing routes to would be a queue with no writer, which is a defect
|
|
191
|
+
* in the declaration, not a case to paper over here.
|
|
192
|
+
*
|
|
193
|
+
* @param {Object} config - The queueConfig object (its sections are read from `this`).
|
|
194
|
+
* @returns {Set<string>} The routing keys, e.g. `workflow.failed`, `{service}.dlq`.
|
|
195
|
+
*/
|
|
196
|
+
const declaredDeadLetterTargets = (config) => {
|
|
197
|
+
const targets = new Set();
|
|
198
|
+
|
|
199
|
+
for (const section of Object.values(config)) {
|
|
200
|
+
if (section === null || typeof section !== 'object') continue;
|
|
201
|
+
for (const entry of Object.values(section)) {
|
|
202
|
+
if (entry === null || typeof entry !== 'object') continue;
|
|
203
|
+
const routingKey = entry.arguments && entry.arguments['x-dead-letter-routing-key'];
|
|
204
|
+
if (typeof routingKey === 'string' && routingKey !== '') targets.add(routingKey);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return targets;
|
|
209
|
+
};
|
|
210
|
+
|
|
172
211
|
/**
|
|
173
212
|
* The two ways out of "this configuration declares no such queue", named in the
|
|
174
213
|
* refusal itself.
|
|
@@ -1222,6 +1261,51 @@ module.exports = {
|
|
|
1222
1261
|
return { exchange, routingKey };
|
|
1223
1262
|
},
|
|
1224
1263
|
|
|
1264
|
+
/**
|
|
1265
|
+
* Is this queue the END of a dead-letter chain — a queue something
|
|
1266
|
+
* dead-letters TO?
|
|
1267
|
+
*
|
|
1268
|
+
* The counterpart of `getDeadLetterRoute()`, and the reason a queue may have no
|
|
1269
|
+
* route without that being a defect. `workflow.failed` is named in
|
|
1270
|
+
* `x-dead-letter-routing-key` by `workflow.init` and `workflow.control`;
|
|
1271
|
+
* `{service}.dlq` by every business template; `workflow.dlq` and `delivery.dlq`
|
|
1272
|
+
* by their own families. Giving such a queue a route of its own would either
|
|
1273
|
+
* close a loop or start an endless chain, which is why the confirmed topology
|
|
1274
|
+
* leaves them without one (`docs/governance/confirmations/mq-consumer-contract.md`
|
|
1275
|
+
* 002 point 2, 003 point 2 — "workflow.failed and workflow.dlq themselves keep
|
|
1276
|
+
* no route").
|
|
1277
|
+
*
|
|
1278
|
+
* The consumer's dead-letter gate reads this second question, so a terminal
|
|
1279
|
+
* queue is consumable while every OTHER queue with no declared route stays
|
|
1280
|
+
* refused (`transports/rabbitmqClient.js`, d.452). What happens to a message
|
|
1281
|
+
* that fails there is unchanged and stated in the README: the budget is spent,
|
|
1282
|
+
* `nack(requeue=false)` is issued, the broker DROPS the message — the end of the
|
|
1283
|
+
* chain is the end — and the loss is logged as an error rather than turned into
|
|
1284
|
+
* an endless requeue.
|
|
1285
|
+
*
|
|
1286
|
+
* Derived from the templates (`declaredDeadLetterTargets()`), never from a list
|
|
1287
|
+
* of names: a `{placeholder}` segment matches any one part, exactly as a
|
|
1288
|
+
* business template key does.
|
|
1289
|
+
*
|
|
1290
|
+
* @param {string} queueName - Full queue name.
|
|
1291
|
+
* @returns {boolean} True if this configuration dead-letters to this name.
|
|
1292
|
+
*/
|
|
1293
|
+
isDeadLetterTarget(queueName) {
|
|
1294
|
+
if (typeof queueName !== 'string' || queueName === '') return false;
|
|
1295
|
+
|
|
1296
|
+
const parts = queueName.split('.');
|
|
1297
|
+
|
|
1298
|
+
for (const target of declaredDeadLetterTargets(this)) {
|
|
1299
|
+
const shape = templateShape(target);
|
|
1300
|
+
if (shape.length !== parts.length) continue;
|
|
1301
|
+
if (shape.every((part, index) => (part === null ? parts[index] !== '' : part === parts[index]))) {
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
return false;
|
|
1307
|
+
},
|
|
1308
|
+
|
|
1225
1309
|
/**
|
|
1226
1310
|
* Get infrastructure queue configuration by queue name (auto-detect type)
|
|
1227
1311
|
*
|
package/src/index.js
CHANGED
|
@@ -38,7 +38,7 @@ const {
|
|
|
38
38
|
} = require('./monitoring-publish');
|
|
39
39
|
const queueConfig = require('./config/queueConfig');
|
|
40
40
|
const deliveryPolicy = require('./config/deliveryPolicy');
|
|
41
|
-
const { redactUrl, UNPARSEABLE_PLACEHOLDER } = require('
|
|
41
|
+
const { redactUrl, UNPARSEABLE_PLACEHOLDER } = require('@onlineapps/logger-contract');
|
|
42
42
|
|
|
43
43
|
// Export BaseClient as default (constructor), with additional named exports
|
|
44
44
|
// NOTE: When destructuring, use: const { BaseClient } = require('@onlineapps/mq-client-core');
|
|
@@ -117,7 +117,18 @@ module.exports.topology = Object.freeze({
|
|
|
117
117
|
* names (`api/docs/governance/confirmations/mq-consumer-contract.md` 003 — the
|
|
118
118
|
* endpoint declaring its queues from `queueConfig` is INFRA's step).
|
|
119
119
|
*/
|
|
120
|
-
deliveryResourceEventsQueue: queueConfig.queueName('deliveryEvents', 'resource.events')
|
|
120
|
+
deliveryResourceEventsQueue: queueConfig.queueName('deliveryEvents', 'resource.events'),
|
|
121
|
+
/**
|
|
122
|
+
* The monitoring consumer's OpenTelemetry log intake, bound to `telemetry.exchange`.
|
|
123
|
+
* Declared in `queueConfig` since d.269; the consumer carries the same literal as a
|
|
124
|
+
* private constant (`api/infra/api_monitoring/src/consumer/topology.js`), which is the
|
|
125
|
+
* shape d.270 gave an owner for the health-events names.
|
|
126
|
+
*/
|
|
127
|
+
telemetryLogsQueue: queueConfig.queueName('telemetry', 'logs.queue'),
|
|
128
|
+
/** The same consumer's metric-sample intake on that exchange. */
|
|
129
|
+
telemetryMetricsQueue: queueConfig.queueName('telemetry', 'metrics.queue'),
|
|
130
|
+
/** The same consumer's span intake on that exchange. */
|
|
131
|
+
telemetryTracesQueue: queueConfig.queueName('telemetry', 'traces.queue')
|
|
121
132
|
});
|
|
122
133
|
/**
|
|
123
134
|
* The dead-letter policy of `consume()` as a contract, not as an implementation
|
|
@@ -137,16 +148,17 @@ module.exports.deliveryPolicy = deliveryPolicy;
|
|
|
137
148
|
* userinfo out, scheme/host/port/vhost in, and a fixed placeholder for a value that
|
|
138
149
|
* is not a parseable URL.
|
|
139
150
|
*
|
|
140
|
-
* It is exported because it was already written three times
|
|
141
|
-
* `
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* where a dependant can import it instead of copying it.
|
|
151
|
+
* It is exported because it was already written three times across the platform.
|
|
152
|
+
* Two semantics for one concern are two rails (`change-discipline.md` § One rail
|
|
153
|
+
* per concern); d.448 made them one, a d.455 je přesunula tam, kam patří —
|
|
154
|
+
* `@onlineapps/logger-contract` (vrstva L1, bez `@onlineapps` závislostí, dosáhne
|
|
155
|
+
* na ni každá kategorie). Zdejší export je od té chvíle **re-export identitou**:
|
|
156
|
+
* zůstává, protože dependants ho importují odsud.
|
|
147
157
|
*
|
|
148
158
|
* The placeholder travels with the function: a caller comparing against it must
|
|
149
159
|
* read the same literal, not retype it.
|
|
160
|
+
*
|
|
161
|
+
* @see node_modules/@onlineapps/logger-contract/src/redactUrl.js
|
|
150
162
|
*/
|
|
151
163
|
module.exports.redactUrl = redactUrl;
|
|
152
164
|
module.exports.UNPARSEABLE_PLACEHOLDER = UNPARSEABLE_PLACEHOLDER;
|
|
@@ -1784,7 +1784,22 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1784
1784
|
// The internal monitor calls `_performHealthCheck()` instead, deliberately:
|
|
1785
1785
|
// a timer resuming recovery would make the retry periodic, which is the
|
|
1786
1786
|
// opposite of the lazy contract.
|
|
1787
|
-
|
|
1787
|
+
//
|
|
1788
|
+
// "If this client stood down" is a QUESTION, and it is asked of the one
|
|
1789
|
+
// liveness source (`_connectionAlive`, read through `isConnected()` — d.260)
|
|
1790
|
+
// the two other uses already read it from: `_ensurePublisherChannel()` and
|
|
1791
|
+
// `_ensureConsumerChannel()` resume recovery inside their
|
|
1792
|
+
// `if (!this._connectionAlive)` branch. Until d.467 this one did not ask:
|
|
1793
|
+
// `_reconnectWithBackoff()` sets `_reconnecting` synchronously and
|
|
1794
|
+
// `isConnected()` reads it, so a check on a LIVE client reported its own
|
|
1795
|
+
// side effect — `connection: { exists: true, closed: true }`, the issue
|
|
1796
|
+
// `Connection is closed or missing`, `isConnected()` true before and false
|
|
1797
|
+
// after — and then really replaced a healthy socket with a new one. A
|
|
1798
|
+
// report that changes what it reports is not a report (INFRA 326b,
|
|
1799
|
+
// measured 2026-09-15 against the live broker, amqplib 0.10.9).
|
|
1800
|
+
if (!this.isConnected()) {
|
|
1801
|
+
this._resumeRecovery();
|
|
1802
|
+
}
|
|
1788
1803
|
return this._performHealthCheck();
|
|
1789
1804
|
}
|
|
1790
1805
|
|
|
@@ -3073,7 +3088,26 @@ class RabbitMQClient extends EventEmitter {
|
|
|
3073
3088
|
// arguments (measured, 2026-09-12 — see queueConfig.getDeadLetterRoute()).
|
|
3074
3089
|
// The caller's own `queueOptions` do not count either; a route supplied at
|
|
3075
3090
|
// the call site would be a second declaration of one queue's topology.
|
|
3076
|
-
|
|
3091
|
+
//
|
|
3092
|
+
// The ONE queue this demand cannot be made of is the one at the END of a
|
|
3093
|
+
// chain: a queue something else dead-letters TO. `workflow.failed` is named
|
|
3094
|
+
// in `x-dead-letter-routing-key` by `workflow.init` and `workflow.control`,
|
|
3095
|
+
// `<service>.dlq` by every business template — and the confirmed topology
|
|
3096
|
+
// leaves exactly those without a route of their own, because a dead-letter
|
|
3097
|
+
// route on a dead-letter queue is a loop or a chain with no end
|
|
3098
|
+
// (`mq-consumer-contract` 002 point 2, 003 point 2). Until d.452 the gate
|
|
3099
|
+
// asked only the first question, so the operator's own queue was
|
|
3100
|
+
// unconsumable and its reader — the delivery dispatcher — could not attach
|
|
3101
|
+
// at all. The second question is derived from the templates
|
|
3102
|
+
// (`queueConfig.isDeadLetterTarget()`), never from a list of names, so it
|
|
3103
|
+
// cannot fall behind them; every other queue with no route is refused
|
|
3104
|
+
// exactly as before, with the same sentence.
|
|
3105
|
+
//
|
|
3106
|
+
// A message that spends its budget on a terminal queue is REJECTED as
|
|
3107
|
+
// anywhere else and then dropped by the broker, which is the end of the
|
|
3108
|
+
// chain doing its job. It is never requeued for ever, and the loss is said
|
|
3109
|
+
// out loud in `_rejectDelivery()`.
|
|
3110
|
+
if (queueConfig.getDeadLetterRoute(queue) === null && !queueConfig.isDeadLetterTarget(queue)) {
|
|
3077
3111
|
throw new ConsumeError(
|
|
3078
3112
|
`[RabbitMQClient] consume(${queue}): the dead-letter policy needs a dead-letter route and `
|
|
3079
3113
|
+ 'queueConfig declares none for this queue - '
|
|
@@ -4136,13 +4170,21 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4136
4170
|
* `status`.
|
|
4137
4171
|
*
|
|
4138
4172
|
* The `onDeadLetter` hook — and therefore the `message_dlq` event it publishes —
|
|
4139
|
-
* is
|
|
4140
|
-
*
|
|
4173
|
+
* is reached for a message that CAN be dead-lettered: `consume()` refuses to
|
|
4174
|
+
* register a consumer on a queue for which `queueConfig` declares no
|
|
4141
4175
|
* `x-dead-letter-exchange`/`-routing-key` (d.259), so by the time a delivery
|
|
4142
4176
|
* arrives here the route exists by declaration. Without that gate the same
|
|
4143
4177
|
* `nack(requeue=false)` made the broker DROP the message while this event
|
|
4144
4178
|
* announced it had reached `.dlq` — a report of a destination nothing routed to.
|
|
4145
4179
|
*
|
|
4180
|
+
* ONE class of queue reaches this method with no route, and it is not that
|
|
4181
|
+
* defect: the END of a chain, a queue something else dead-letters TO
|
|
4182
|
+
* (`<service>.dlq`, `workflow.failed`), whose consumer the gate admits on
|
|
4183
|
+
* purpose (d.452). There the rejection does not move the message anywhere — the
|
|
4184
|
+
* broker drops it, because the end of the chain is the end — so the loss is
|
|
4185
|
+
* logged at error level below before monitoring is told. What is NOT done is
|
|
4186
|
+
* inventing a requeue for it: that is the unbounded loop d.198b ended.
|
|
4187
|
+
*
|
|
4146
4188
|
* @param {Object} args
|
|
4147
4189
|
* @private
|
|
4148
4190
|
*/
|
|
@@ -4178,6 +4220,29 @@ class RabbitMQClient extends EventEmitter {
|
|
|
4178
4220
|
return;
|
|
4179
4221
|
}
|
|
4180
4222
|
|
|
4223
|
+
// A TERMINAL queue — one this configuration dead-letters TO — declares no
|
|
4224
|
+
// onward route by design, so the rejection just issued does not MOVE the
|
|
4225
|
+
// message: the broker drops it. That is the end of the chain working as
|
|
4226
|
+
// intended, and the alternative would be the unbounded requeue d.198b ended.
|
|
4227
|
+
// It is still a message the platform has lost, so it is said out loud, at
|
|
4228
|
+
// error level, exactly once, before monitoring is told (d.452).
|
|
4229
|
+
if (queueConfig.getDeadLetterRoute(queue) === null) {
|
|
4230
|
+
this._logger.error(
|
|
4231
|
+
`[RabbitMQClient] [mq-client-core] [CONSUMER] Message discarded on terminal queue "${queue}" - `
|
|
4232
|
+
+ 'Expected: a terminal queue is the end of the dead-letter chain, so it declares no onward '
|
|
4233
|
+
+ 'route and the broker DROPS what nack(requeue=false) rejects instead of moving it. '
|
|
4234
|
+
+ 'Fix: nothing to declare — the delivery budget is spent and the message is deliberately gone '
|
|
4235
|
+
+ 'rather than circling for ever; read the error and fix what made the handler fail.',
|
|
4236
|
+
{
|
|
4237
|
+
queue,
|
|
4238
|
+
attempts,
|
|
4239
|
+
maxAttempts,
|
|
4240
|
+
classification,
|
|
4241
|
+
error: error && error.message ? error.message : null
|
|
4242
|
+
}
|
|
4243
|
+
);
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4181
4246
|
if (onDeadLetter === undefined) {
|
|
4182
4247
|
// Driving this transport directly is allowed (it is an export of this
|
|
4183
4248
|
// package), but then nothing owns the monitoring rail, and a message
|
|
@@ -12,53 +12,21 @@
|
|
|
12
12
|
* boot, so the broker account was readable in container stdout and, through the
|
|
13
13
|
* monitoring consumer, in Loki (measured 2026-09-07 on api_service_hello).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* two semantics for one concern — two rails (`change-discipline.md` § One rail
|
|
23
|
-
* per concern). `src/index.js` exports the function so the dependants import it
|
|
24
|
-
* instead of keeping a local copy.
|
|
15
|
+
* Samotné pravidlo „co smí z URL dorazit do logu“ tenhle balíček neimplementuje.
|
|
16
|
+
* Od d.455 je bere z `@onlineapps/logger-contract` (`redactUrl`,
|
|
17
|
+
* `UNPARSEABLE_PLACEHOLDER`): userinfo ven, scheme/host/port/vhost/query
|
|
18
|
+
* dovnitř, neparsovatelná hodnota nahrazená placeholderem místo echa. Do d.455
|
|
19
|
+
* tu stála bajtově stejná kopie jako v `@onlineapps/service-common` — tři
|
|
20
|
+
* vlastníci jednoho pravidla, tedy přesně to, co `change-discipline.md`
|
|
21
|
+
* § One rail per concern označuje za vadu.
|
|
25
22
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* say whether it holds a credential, and echoing it would be the leak this
|
|
29
|
-
* helper exists to prevent.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
const UNPARSEABLE_PLACEHOLDER = '<unparseable-url>';
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Strip userinfo (`user:password@`) from a connection URL.
|
|
23
|
+
* Zdejší zůstává jediná věc, kterou core vrstva nemá proč znát:
|
|
24
|
+
* `redactConnectionTarget`, fail-fast obal nad DEKLAROVANÝM config klíčem.
|
|
36
25
|
*
|
|
37
|
-
* @
|
|
38
|
-
* @returns {string} the URL without userinfo, e.g. `amqp://queuer:5672/vhost`,
|
|
39
|
-
* or `<unparseable-url>` when the input is not a URL.
|
|
26
|
+
* @see node_modules/@onlineapps/logger-contract/src/redactUrl.js
|
|
40
27
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return UNPARSEABLE_PLACEHOLDER;
|
|
44
|
-
}
|
|
45
|
-
let parsed;
|
|
46
|
-
try {
|
|
47
|
-
parsed = new URL(url);
|
|
48
|
-
} catch (_) {
|
|
49
|
-
return UNPARSEABLE_PLACEHOLDER;
|
|
50
|
-
}
|
|
51
|
-
// `new URL('api_services_queuer:5672')` SUCCEEDS — it reads `api_services_queuer:`
|
|
52
|
-
// as the scheme and `5672` as an opaque path, so the value comes back with an
|
|
53
|
-
// empty host and no userinfo to strip. Without this guard such a value would be
|
|
54
|
-
// echoed verbatim, which is the leak this helper exists to prevent.
|
|
55
|
-
if (!parsed.host) {
|
|
56
|
-
return UNPARSEABLE_PLACEHOLDER;
|
|
57
|
-
}
|
|
58
|
-
parsed.username = '';
|
|
59
|
-
parsed.password = '';
|
|
60
|
-
return parsed.href;
|
|
61
|
-
}
|
|
28
|
+
|
|
29
|
+
const { redactUrl, UNPARSEABLE_PLACEHOLDER } = require('@onlineapps/logger-contract');
|
|
62
30
|
|
|
63
31
|
/**
|
|
64
32
|
* Makes a connection target safe to log.
|
|
@@ -103,4 +71,4 @@ function redactConnectionTarget(target) {
|
|
|
103
71
|
);
|
|
104
72
|
}
|
|
105
73
|
|
|
106
|
-
module.exports = { redactConnectionTarget
|
|
74
|
+
module.exports = { redactConnectionTarget };
|