@onlineapps/mq-client-core 4.2.0 → 5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,606 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [5.0.0] — 2026-09-27
8
+
9
+ ### Removed — BREAKING — fronta `workflow.control` (d.1061)
10
+
11
+ Sekce `workflow` v `src/config/queueConfig.js` už nedeklaruje položku `control` (TTL 30 s,
12
+ max 10 000, DLX → `workflow.failed`). Rozhodnutí vlastníka:
13
+ `docs/governance/confirmations/cookbook-execution-owner.md` 002.
14
+
15
+ Co to znamená pro volajícího — hodnotami, ne větou:
16
+
17
+ - `queueConfig.getInfrastructureQueueConfig('workflow.control')` a
18
+ `queueConfig.getDeadLetterRoute('workflow.control')` hází
19
+ `[queueConfig] Infrastructure queue config not found: workflow.control …`;
20
+ - `queueConfig.queueName('workflow', 'control')` hází
21
+ `[queueConfig] Cannot compose a name: queueConfig declares no "control" under "workflow" …`,
22
+ takže publikace na to jméno se odmítne dřív, než cokoli odejde;
23
+ - konzument, který frontu dosud kontroloval při startu, ji v deklaraci nenajde.
24
+
25
+ Co se **nemění**: `workflow.init` (deklarace, jméno, trasa do `workflow.failed`) a
26
+ `workflow.failed` jako konec řetězu, který `workflow.init` jmenuje. Počty deklarací klesly
27
+ o jednu frontu (36 položek / 29 front / 7 exchange); křížový součin prefix × šablona se
28
+ nemění, prefix `workflow` zůstává.
29
+
30
+ Odebrání deklarace frontu z běžícího brokeru nesmaže (fronta je durable) — to je krok
31
+ nasazení, ne knihovny.
32
+
33
+ ### Tests — `workflow.control` není deklarovaná (d.1061)
34
+
35
+ Nová sada `tests/unit/workflow-control-queue-not-declared.test.js` tvrdí nepřítomnost
36
+ hodnotami (žádný klíč `control`, tři odmítnutí se jménem fronty) a kontrolní případy
37
+ `workflow.init` a `workflow.failed`. Případy, které `workflow.control` používaly jako
38
+ deklarovanou frontu (`queueConfig-workflow-failed`, `dead-letter-topology`,
39
+ `consume-dead-letter-route-required`, `consume-terminal-queue`), ji už nejmenují; počty
40
+ deklarací v `queue-name-collision` a `d712-registry-changes-exchange-type` jsou srovnané.
41
+
42
+ ### Documentation — README: příklad publikace na `workflow.control` nahrazen `workflow.completed` (d.1061)
43
+
44
+ ### Fixed — `getChannelState()` hlásí i telemetrický kanál (d.1007)
45
+
46
+ `getChannelState()` hlásil `publisher`, `queue` a `consumer`, ale telemetrický kanál z d.724
47
+ (`_telemetryChannel`, vlastní události klienta) vynechával, přestože klíče téhle zprávy jsou
48
+ dokumentované jako seznam kanálů klienta. Teď nese čtvrtý klíč `telemetry` ve stejném tvaru
49
+ `{ exists, closed, ready }`. Kanál se otevírá prvním telemetrickým publishem; do té doby čte
50
+ `{ exists: false, closed: true, ready: false }`, stejně jako každý dosud neotevřený kanál.
51
+
52
+ Aditivní klíč. Čtenář, který tvrdí přesný tvar tří klíčů (`toEqual`), se musí srovnat: v balíčku
53
+ `connection-liveness-single-source` (unit i IT) je srovnaný v téže změně, kořenové
54
+ `api/tests/integration/channel-recovery.test.js` se srovná s re-pinem kořene na tuto verzi.
55
+
56
+ ### Added — `deliveryPolicy.validateConsumeOptions`: pravidla voleb `consume()` mají jednoho exportovaného vlastníka (d.1005)
57
+
58
+ Čtyři pravidla voleb `consume()` dosud žila inline v transportu (`rabbitmqClient.js` `consume()`):
59
+ - `requeueOnError` je boolean;
60
+ - `requeueOnError: false` znamená jeden pokus a jiné `maxAttempts` s ním je rozpor;
61
+ - rozpočet je celé číslo ≥ 1;
62
+ - `classify` je funkce.
63
+
64
+ Dvojník konzumenta (service-wrapper `MockMQConnector`) je proto mohl jen opsat, tedy druhá kolej
65
+ (nález d.998c). Teď bydlí v `src/config/deliveryPolicy.js` `validateConsumeOptions(options,
66
+ resolveDefaultMaxAttempts)`, transport ji volá a balíček ji exportuje jako
67
+ `deliveryPolicy.validateConsumeOptions`. Vrací `{ maxAttempts, classify }`, nebo hází `ValidationError`
68
+ se stejnými texty jako dosud `consume()`: `[RabbitMQClient] Invalid requeueOnError …`,
69
+ `… Contradictory delivery policy …`, `… Invalid delivery budget …`, `… Invalid error classifier …`.
70
+
71
+ Výchozí rozpočet se předává funkcí a volá se jen tehdy, když ho volby nechávají otevřený. Jeho
72
+ čtení sahá do konfigurace (env `RABBITMQ_MAX_DELIVERY_ATTEMPTS`), která může sama odmítnout, a
73
+ explicitní `maxAttempts` ani `requeueOnError: false` ho nikdy nepotřebovaly. Chování `consume()` se
74
+ nemění. Pro dvojníky konzumentů: volat `deliveryPolicy.validateConsumeOptions(options, () =>
75
+ <rozpočet dvojníka>)` místo vlastní kopie pravidel.
76
+
77
+ ### Removed — BREAKING — `RecoveryWorker` a posluchač kanálu `error`, který ho krmil (d.971)
78
+
79
+ Pryč je `src/workers/RecoveryWorker.js` (`handleQueueNotFound()`, `handleTransientError()`)
80
+ a v `src/transports/rabbitmqClient.js` jeho instance `_recoveryWorker` i vlastní posluchač
81
+ transportu `this.on('error', …)` („Recovery worker - handle QueueNotFoundError via error
82
+ event“). Čerstvě postavený transport na svém kanálu `error` neposlouchá nic.
83
+
84
+ BREAKING pro každého, kdo si worker bral hlubokou cestou
85
+ `require('@onlineapps/mq-client-core/src/workers/RecoveryWorker')` — z `src/index.js` se
86
+ nikdy neexportoval; ve workspace to dělaly jen dvě sady `conn-infra-mq`, přepsané v téže
87
+ vlně na veřejný kontrakt.
88
+
89
+ Co zmizí z logu: při publikaci do chybějící fronty už nevznikne řádek
90
+ `[RecoveryWorker] Cannot create infrastructure queue '<q>' …`,
91
+ `[RecoveryWorker] Cannot create business queue '<q>' …` ani
92
+ `[RecoveryWorker] Cannot create queue '<q>' - no section of queueConfig declares it …`.
93
+ Po d.811b (výše) už ho stejně nic nespouštělo.
94
+
95
+ Kde volající verdikt dostane (beze změny): `publish()` odmítne `PublishError`
96
+ (`error.queue` = cílová fronta) s `cause` = `QueueNotFoundError` — `queueName`,
97
+ `isInfrastructure`, `kind` (`infrastructure` / `business` / `unowned`) a pro každou třídu
98
+ věta s vlastníkem fronty a `Fix:`. Nic se nedeklaruje.
99
+
100
+ Čtyři otázky (`change-discipline.md` § Removing): (1) vznikl v `3588b148` jako zakladač
101
+ chybějící fronty pro business scope; (2) nesla ho hlavička workeru § Queue Creation, kterou
102
+ nahradila conf `mq-consumer-contract` 006 („queues come into being only from
103
+ declarations“) — od d.419 nic nezakládal, jen logoval a znovu házel do posluchače, který
104
+ chybu spolkl; (3) funkci mu vzal d.419, vstup d.811b; (4) náhrada je konceptuálnější —
105
+ jeden verdikt jednou kolejí, k volajícímu, místo druhé kopie téže věty do logu.
106
+ Sada `tests/unit/recovery-worker-removed.test.js` (kontrolní případ: `publish()` do
107
+ chybějící business fronty dál odmítá `PublishError` s `QueueNotFoundError` v `cause`).
108
+
109
+ ### Changed — BREAKING — verdikt o odmítnuté publikaci jde jen odmítnutím `publish()`, ne kanálem `error` (d.811b)
110
+
111
+ `RabbitMQClient._publishOnce()` končil každé selhání pokusu dvěma tvrzeními jednoho
112
+ verdiktu: `this.emit('error', classifiedErr)` a `throw classifiedErr`. Emit je pryč,
113
+ zůstává `throw classifyPublishError(err, queue)` (`src/transports/rabbitmqClient.js`,
114
+ větev `catch` v `_publishOnce()`). Kanál `error` zůstává tomu, k čemu ho používá zbytek
115
+ klienta — spojení a kanálům.
116
+
117
+ Co konzument vidí jinak:
118
+
119
+ - **`BaseClient.onError(callback)`** (jediná veřejná cesta ke kanálu `error` transportu,
120
+ `BaseClient.connect()` na něj věší `_handleError`) už nedostává `TransientPublishError`
121
+ (`retryable: true`), `PermanentPublishError` (`retryable: false`) ani `QueueNotFoundError`
122
+ (`kind`, `queueName`, `isInfrastructure`). Dřív je dostával za KAŽDÝ neúspěšný pokus,
123
+ tedy i za transientní pokusy, které `PublishLayer` následně zopakoval nebo zabufferoval.
124
+ Ostatní emity na `error` se nemění: spojení (`CONNECTION_CLOSED_UNEXPECTEDLY`), konec
125
+ cyklu obnovy spojení, chyby kanálu vydavatele (406 dál ne — tu odpovídá odesílateli
126
+ publikace) i konzumenta, odmítnutí `consume()`, selhání `ack`/`nack` a odmítnuté zavření
127
+ kanálu či spojení v `disconnect()`.
128
+ - **`publish()` se nemění:** odmítá stejně jako dřív — `PublishError` z `BaseClient.publish()`
129
+ s klasifikovanou chybou v `error.cause`. Čísla o publikacích hlásí dál
130
+ `getPublishMetrics()` / `getPrometheusMetrics()` (plní je události `publish:retry`,
131
+ `publish:success`, `publish:failed`, `publish:buffered`, beze změny).
132
+ - **`RecoveryWorker.handleQueueNotFound()` už na cestě publikace neběží.** Transport ho volal
133
+ z vlastního posluchače kanálu `error` (`_setupPublishMonitoring()`, „Recovery worker -
134
+ handle QueueNotFoundError via error event") a žádné jiné místo v `src/**` ho nevolá; žádný
135
+ zbývající emit na `error` `QueueNotFoundError` nenese. Při publikaci do chybějící fronty
136
+ proto z logu mizí řádek `[RecoveryWorker] Cannot create infrastructure queue '<q>' …` /
137
+ `Cannot create business queue '<q>' …` / `Cannot create queue '<q>' - no section of
138
+ queueConfig declares it …`. Fronta se nezakládá ani dál (nezakládal ji ani ten řádek) a
139
+ volající dostane odmítnutí jako dosud; ubyla jen druhá, logová zpráva o téže události.
140
+ - Filtr v `_waitForReconnection()` (`if (error.retryable !== undefined) return;`) zůstává:
141
+ knihovna do kanálu klasifikovanou chybu už nedává, ale kanál je veřejný a kdo drží
142
+ transport, emitovat na něj může.
143
+
144
+ Dopad ve workspace (změřeno 2026-09-26 nad `api/infra/*`, `api/shared/**` mimo tento balíček
145
+ a `api_biz/*/src`): v produkčním kódu nevolá `onError()` nikdo a nikdo neposlouchá `error` na
146
+ MQ klientovi ani na jeho transportu — pět míst, která sahají na `mqClient._transport`
147
+ (gateway, dispatcher, scheduler, wrapper, `conn-infra-mq` `QueueManager`), berou z něj
148
+ `queueChannel`, `connection` nebo `onChannelClose`, ne kanál `error`. Integrační sady v
149
+ `api/tests/integration` a `api/infra/api_gateway/tests` registrují `onError(() => {})`, který
150
+ nic netvrdí. Proto BREAKING podle kontraktu `onError()`, bez čtenáře, kterého by to zlomilo.
151
+
152
+ ### Changed — BREAKING — `serviceName` je povinný, knihovna si ho nevyrábí (d.770)
153
+
154
+ `src/defaults.js` už nenese `serviceName: 'oa-service'` a `src/config.js` deklaruje klíč
155
+ jako `required: true`. Jméno má dvě nohy, obě volajícího: explicitní `serviceName`
156
+ v konfiguraci klienta, nebo ENV `SERVICE_NAME`. Třetí není. Chybí-li obě,
157
+ `new BaseClient(…)` se odmítne postavit hláškou, jejíž znění vlastní
158
+ `@onlineapps/runtime-config` (`[RuntimeConfig] Missing environment variable - SERVICE_NAME
159
+ is required for config key "serviceName". Fix: …`). Dřív klient postavený bez klíče
160
+ publikoval události monitoringu a pojmenovával fronty pod službou `oa-service`, která
161
+ neexistuje — v `workflow_traces.service_name` pak stálo jméno nikoho.
162
+
163
+ Druhý čtenář téhož faktu je srovnaný: `BaseClient` rozřešené jméno (ať přišlo kteroukoli
164
+ nohou) zapisuje zpět do `config.serviceName` a transport z něj skládá jméno spojení
165
+ `oa-client:<serviceName>:<pid>`, když volající nedal `connectionName` ani `clientName`.
166
+ Dřív ho transport bral z runtime konfigurace, takže explicitní `serviceName` volajícího se
167
+ tam vůbec nečetl. Transport postavený bez jména se odmítne `ValidationError` místo spojení
168
+ pojmenovaného `oa-client:undefined`.
169
+
170
+ Migrace: předejte `serviceName` v konfiguraci klienta, nebo nastavte `SERVICE_NAME`
171
+ v prostředí služby. Změřeno v dávce: service-wrapper, conn-orch-registry,
172
+ api_delivery_dispatcher a api_meta_reader jméno předávají; api_delivery_endpoint stojí na
173
+ noze `SERVICE_NAME`.
174
+
175
+ ### Changed — BREAKING — zprávu, kterou broker pozdě odmítl, vrstva publishe už neposílá znovu (d.801c)
176
+
177
+ `PublishLayer` po dobu jednoho publishe poslouchá událost `publish:refused-late` (d.801b,
178
+ níže) a před každým dalším pokusem se ptá, zda pro tuto frontu nedorazil pozdní
179
+ PERMANENTNÍ verdikt. Pokud ano, smyčka končí: `warn` `[PublishLayer] The broker refused
180
+ this message while attempt <n> was in flight, so attempt <n+1> of <max> is not made …`,
181
+ událost `publish:failed` s `retryable: false` a `refusedLate: true`, a volající dostane ten
182
+ verdikt — `PermanentPublishError` s brokerovým rámcem v `cause`. Žádný další pokus, nic do
183
+ bufferu. Posluchač se odhlašuje v `finally`; nevzniká žádný nový časovač ani evidence. Číslo
184
+ pokusu jde transportu v options jako `attempt` (do AMQP vlastností se nedostane).
185
+
186
+ Co se mění pro konzumenta: zpráva, kterou broker odmítl až poté, co `brokerAnswerTimeout`
187
+ odpověděl volajícímu, se dřív opakovala `publishMaxRetries`krát a skončila v bufferu,
188
+ odkud ji `InMemoryBuffer.flush()` nabízel při každém reconnectu do vypršení TTL. Nově
189
+ skončí jedním pokusem a trvalou chybou. Dopad popsaný v dávce: api_delivery_dispatcher
190
+ takovou zásilku přestane opakovat a pošle ji deklarovanou dead-letter cestou; RPC odpověď
191
+ service-wrapperu změní třídu z transientní na permanentní; kdo počítá pokusy z
192
+ `publish:retry`, uvidí u takové zprávy jeden místo N. Zpráva, u níž potvrzení nedorazí
193
+ vůbec a žádný rámec není, se opakuje beze změny.
194
+
195
+ ### Changed — BREAKING — očíslované odmítnutí odeslání je trvalý verdikt o TÉTO zprávě (d.685a, d.693, d.696, d.723)
196
+
197
+ Broker, který odmítne `basic.publish`, zavře kanál očíslovaným rámcem (`classId 60`,
198
+ `methodId 40`). amqplib ho předá události `error` kanálu, kde ho handler publisher kanálu
199
+ zapíše do `channel._closeError`; callback publishe dostane jen holé `'channel closed'`.
200
+ `_publishOnce()` ten rámec nově čte na obou nohách, frontové i výměnové:
201
+
202
+ - **Číselný kód je odpověď** (d.685a, d.693): 403 ACCESS-REFUSED, 406 PRECONDITION-FAILED
203
+ i 404 NOT-FOUND za publish přes výměnu, kterou nikdo nedeklaroval, končí
204
+ `PermanentPublishError` s rámcem v `cause` — jeden pokus, nic do bufferu. Dřív
205
+ `TransientPublishError`: `publishMaxRetries` pokusů, pak buffer a nabídka při každém
206
+ reconnectu do TTL. Věty má jeden producent, `refusedSendMessage(kind, target, code)`
207
+ v `utils/publishErrors.js`; u 404 nad výměnou říká, že ji nikdo nedeklaroval, a `Fix:`
208
+ posílá do `queueConfig`. Chybějící FRONTA se nemění — dál `QueueNotFoundError`
209
+ z předkontroly.
210
+ - **Rámec patří jedné publikaci** (d.723): nese-li kanál v okamžiku rámce víc
211
+ nepotvrzených publikací, trvalý verdikt dostane jen ta, jejíž výměnu či frontu rámec
212
+ jmenuje; ostatní dostanou `TransientPublishError` s rámcem v `cause`
213
+ (`collateralCloseMessage()`), takže je vrstva zopakuje a mezitím bufferuje. Jméno, které
214
+ se v rámci nenajde, znamená „nepřisouzeno" — přechodný verdikt, tedy nanejvýš duplicitní
215
+ zpráva, nikdy zahozená.
216
+ - **Důvod se zapisuje dřív, než handler větví** (d.696): i rámec `406` „unknown delivery
217
+ tag" handler publisher kanálu zapíše do `channel._closeError`, takže publish na kanálu
218
+ zabitém tímto rámcem je trvalý stejně jako každé jiné očíslované odmítnutí. Na kanál
219
+ `error` klienta rámec dál nejde.
220
+ - Smrt kanálu BEZ číselného kódu (výpadek spojení, soketový `code` jako `'ECONNRESET'`)
221
+ zůstává přechodná: o osudu zprávy se nic neví.
222
+
223
+ Migrace: kdo spoléhal na to, že vrstva odmítnutou zprávu zopakuje nebo zabufferuje,
224
+ dostane ji nově hned jako odmítnutí `publish()`. Oprava je u příčiny, kterou rámec
225
+ v `error.cause` jmenuje — právo účtu ve vhostu, `user_id` zprávy, deklarace výměny.
226
+
227
+ ### Changed — BREAKING — `publishMaxRetries: 0` se odmítá jménem (d.684, d.685b)
228
+
229
+ Klíč počítá POKUSY včetně prvního. `0` proto nebylo „bez opakování", ale publish, ke
230
+ kterému nedojde: smyčka `_publishWithRetry()` neproběhla ani jednou, zpráva se brokeru
231
+ nenabídla, cestou ven se nezabufferovala a závěrečná hláška svalovala vinu na
232
+ `PublishLayer`. Tři komentáře, které `0` vykládaly jako „do not retry", jsou opravené.
233
+
234
+ Nově: `configSchema.js` deklaruje `minimum: 1`, takže `new BaseClient({ …,
235
+ publishMaxRetries: 0 })` padne už při stavbě klienta (`/publishMaxRetries: must be >= 1`
236
+ s `Fix:`); konstruktor `PublishLayer` hodnotu pod 1 odmítá i tam, kam schéma nedosáhne
237
+ (transport postavený přes `transportFactory`, vlastní `options.maxRetries` volajícího),
238
+ `ValidationError` `[PublishLayer] publishMaxRetries must be at least 1 - …`.
239
+
240
+ Migrace: jeden pokus bez opakování je `publishMaxRetries: 1`, opuštění retry koleje
241
+ `publishRetryEnabled: false`. Změřeno v dávce d.685b: jediný zápis klíče mimo balíček je
242
+ `api/tests/integration/message-buffer.test.js` s hodnotou 2.
243
+
244
+ ### Changed — vytíženost prefetch okna se hlásí na přechodu, oběma směry (d.736a)
245
+
246
+ `_checkPrefetchUtilization()` dřív mluvil pokaždé, když podmínku našel platnou (produkce
247
+ 18. 9.: tři řádky za 68 ms o jedné stojící podmínce), a návrat pod práh neřekl nikdy. Nově
248
+ padá verdikt jen na přechodu: nad práh jeden `warn` `Prefetch utilization high: …` a s ním
249
+ jednou událost `prefetch:high-utilization` a jednou `prefetchAlertCallback`; pod práh
250
+ jeden `info` `Prefetch utilization back below threshold: …`; mezi tím ticho, ať se čítač
251
+ nad prahem hýbe jakkoli. Stav drží `tracking.utilizationHigh` a verdikt se bere i při
252
+ snížení čítače. Kdo počítal volání callbacku nebo události, dostane jedno za epizodu místo
253
+ jednoho za každou kontrolu nad prahem. Práh se nemění, hystereze ani druhý callback nevznikají.
254
+
255
+ ### Changed — fronta instance registru vyprší 60 s po posledním konzumentovi (`x-expires`, d.1036)
256
+
257
+ Šablona `registry.{instance_id}` (`src/config/queueConfig.js`) nese `'x-expires': 60000`.
258
+ Každý start kopie služby pojmenuje novou frontu `<svc>.registry.<instance_id>` (UUID na proces,
259
+ pokud `SERVICE_INSTANCE_ID` neurčí jinak) a nic starou nemaže — `deregister()`, shutdown
260
+ wrapperu ani registr, který frontu před odpovědí jen ověří (měřeno d.1032). Bez životnosti by
261
+ po každém restartu, pádu i čistém stopu zůstala trvalá fronta bez konzumenta, kterou monitoring
262
+ hlásí jako `no_owner`. Živá kopie má konzumenta, takže jí fronta nevyprší; po odchodu kopie ji
263
+ broker smaže do 60 s — pod oknem sirotka monitoringu (`min_orphan_minutes`, seed 5), takže
264
+ epizodu neotevře. Sdílená `<svc>.registry` životnost nemá a nemění se. Rozhodnutí vlastníka:
265
+ otázka 14, varianta A (`registry-instance-identity` 003).
266
+
267
+ Není to BREAKING vůči vydané 4.2.0: ta šablonu `registry.{instance_id}` nemá vůbec, takže
268
+ žádný vydaný klient frontu tohoto tvaru nezaložil (konzumaci by odmítl bez dead-letter cesty).
269
+ Změřeno na brokeru (`tests/integration/registry-instance-queue-expires.integration.test.js`):
270
+ fronta tohoto tvaru, kterou dřív kdokoli založil BEZ `x-expires`, se teď při `consume()` odmítne
271
+ `406 PRECONDITION-FAILED` („inequivalent arg 'x-expires'") — týká se jen front z nevydaného
272
+ buildu (na dev brokeru jich je 0, d.1032 bod 5); oprava = frontu smazat, klient ji při
273
+ konzumaci založí znovu.
274
+
275
+ ### Added — fronta jedné instance registru `{service}.registry.{instance_id}` je deklarovaná (d.817b)
276
+
277
+ Odpovědi registru na registraci jdou instanci na `<svc>.registry.<instanceId>` (conf
278
+ `registry-instance-identity` 001); sdílená `<svc>.registry` nese jen podněty registru. Pro
279
+ takové jméno dosud `queueConfig.getDeadLetterRoute()` i `parseBusinessQueue()` vracely `null`
280
+ a `consume()` frontu bez deklarované dead-letter cesty odmítá (conf `mq-consumer-contract`
281
+ 002).
282
+
283
+ Nová šablona v sekci `business` (`src/config/queueConfig.js`):
284
+
285
+ 'registry.{instance_id}': {
286
+ durable: true,
287
+ arguments: {
288
+ 'x-dead-letter-exchange': DEAD_LETTER_EXCHANGE, // 'dlx'
289
+ 'x-dead-letter-routing-key': '{service}.dlq'
290
+ }
291
+ }
292
+
293
+ tedy stejná dead-letter cesta jako u sdílené `{service}.registry`:
294
+ `getDeadLetterRoute('biz-hello.registry.7f3a9c21')` = `{ exchange: 'dlx', routingKey:
295
+ 'biz-hello.dlq' }`. Životnost fronty doplnila d.1036 (`x-expires`, viz Changed níž).
296
+
297
+ Rozhodčí pravidlo `matchBusinessTemplate()`: `<svc>.registry.events` sedí na doslovnou
298
+ šablonu `registry.events` i na zástupnou `registry.{instance_id}`. Doslovná šablona nově
299
+ vyhrává nad zástupnou; dvě shody šablon TÉHOŽ druhu jsou dál vada a jméno se odmítá
300
+ `ValidationError` („matches more than one template"). Stávající šablony, jejich argumenty ani
301
+ cesty se nemění (`registry.events` si drží TTL i strop, sdílená `registry` je beze změny,
302
+ `registry.register` zůstává platformní); jméno, na které nesedí žádná šablona, je dál
303
+ odmítnuto.
304
+
305
+ ### Added — `scheduler.SCHEDULER_REQUEST_INVALID`: odmítnutí builderu se pozná podle kódu, ne podle textu (d.943)
306
+
307
+ Boot fáze 0.7b wrapperu má podle conf `scheduler` 020 izolovat záznam, který builder odmítne
308
+ (selhaný zdroj), a dál zastavit boot, když se požadavek nepodaří ODESLAT (selhání služby). Obě
309
+ cesty dosud házely holé `Error`, takže je šlo rozlišit jen podle znění hlášky. Nově každé
310
+ odmítnutí builderu nese `error.code = 'SCHEDULER_REQUEST_INVALID'` (konstanta pod jmenným
311
+ prostorem `scheduler`, jediný vlastník). Kód nesou: kontrola klíčů v `registerRecurring`,
312
+ `validateRecurrences` (chybějící `source`, dokument není objekt, dokument nedrží tvar, dvakrát
313
+ stejné `id`), `requireIdempotencyKey`, `requireRequestedBy`, `requireTenancy` (tenant i
314
+ workspace) a `buildEventRequest` (chybí `event`, `event.service`/`event.name` mimo vzor) —
315
+ tedy i `buildRecurringRequest` a `buildCancelRequest`, které je volají. NEnesou ho:
316
+ `publishSchedulerRequest` (chybí klient, klient není připojen, cizí druh požadavku, publikace
317
+ odmítnutá brokerem) a stavba portu v `createSchedulerPublisher`. Znění hlášek beze změny.
318
+ Sada `tests/unit/scheduler-request-invalid-code.test.js` (kontrolní případy: selhaná publikace
319
+ a nepřipojený klient kód nemají).
320
+
321
+ ### Added — událost `message_dlq` jmenuje operaci, která umřela (d.641)
322
+
323
+ Mrtvý dopis dosud říkal, ve KTERÉ frontě zemřel (`queue`, `attempts`,
324
+ `classification`, `error`), ale ne CO umřelo. Operátor tak u běhu s víc kroky
325
+ neměl z události jak poznat, která operace to byla; jméno bylo přitom celou dobu
326
+ v obálce, kterou `_emitDeadLetterEvent()` už rozbaluje kvůli `workflow_id`.
327
+
328
+ Nově událost nese `operation` — **jen když ho obálka nesla**, v jediném
329
+ pravopisu invokační obálky (`docs/biz/10-invocation/mq-envelope.md` § Inbound
330
+ envelope). Workflow zpráva (`docs/biz/70-contracts/workflow-message.md` §3.2)
331
+ `operation` nikdy nenese, takže u ní klíč v události prostě chybí; žádný
332
+ `unknown`, žádný `null` — stejně jako se zachází s chybějícím `workflow_id`.
333
+ Hodnota, která není neprázdný řetězec (`''`, číslo, objekt), se bere jako
334
+ nepřítomnost.
335
+
336
+ Konzument existuje už dnes a nový klíč čte beze změny: monitoring bere
337
+ `readReported(data.operation)` (`api/infra/api_monitoring/src/consumer/
338
+ apiflow-extension.js`) a posílá ji do řádku v Loki jako `operation` — u události
339
+ `message_dlq` tam do téhle dávky chodilo `null`. Identita běhu zůstává **jeden
340
+ klíč**, `workflow_id`: `correlation_id` na `monitoring.workflow` nejezdí
341
+ (`docs/biz/70-contracts/monitoring-events.md` § Rules) a tahle dávka ho tam
342
+ nezavádí.
343
+
344
+ Doklad proti realitě: integrační tier posílá přes živý broker obálku s
345
+ `operation`, nechá ji dojít do `<service>.dlq` a čte událost z vlastní kopie
346
+ fanoutu `monitoring.workflow.fanout`; kontrolní případ je tatáž cesta s
347
+ workflow zprávou bez `operation` — klíč v události chybí.
348
+
349
+ ### Added — `decodeMessage()`: jedna veřejná kolej z doručení na zprávu (d.789)
350
+
351
+ Nový export z kořene balíčku (`src/utils/decodeMessage.js`). `consume()` předává handleru
352
+ syrovou zásilku amqplib, takže si překlad psal každý konzument sám — dávka d.788 změřila
353
+ čtyři kopie (dispatcher, plánovač, service-wrapper, registr), které se neshodnou právě
354
+ v tom, co se stane s vadnou zprávou. Dekódování samo dělá `utils/serializer.js`
355
+ `deserialize()` odjakživa; chyběly veřejné dveře.
356
+
357
+ `decodeMessage(raw)` vrací `{ body, headers, properties, fields }` — dekódované tělo
358
+ a všechno ostatní tak, jak přišlo. Odmítá `ValidationError`em, jménem toho, co přišlo:
359
+ zásilku, která není objekt; `content`, který není `Buffer` ani řetězec; prázdné tělo;
360
+ deklarovaný `contentType` jiný než `application/json` (typ se kontroluje, jen když ho
361
+ zpráva deklaruje — obálka platformy ho nevyžaduje). Tělo, které není JSON, propouští
362
+ `SerializationError` z `deserialize()` beze změny (`payload`, `cause`). Pole `retryable`
363
+ nenese: na cestě konzumace ho nic nečte, osud zprávy rozhoduje `classify(error)`
364
+ volajícího podle třídy chyby.
365
+
366
+ Aditivní: `consume()` se nemění a v balíčku funkci nic nevolá. Převod čtyř konzumentů
367
+ mění osud vadné zprávy a jde vlastní dávkou.
368
+
369
+ ### Added — `topology.telemetryExchange` (d.708b)
370
+
371
+ Zmrazený export `topology` nese `telemetryExchange: 'telemetry.exchange'`, složený
372
+ `queueConfig.queueName('telemetry', 'exchange')` ze šablony, kterou výměna má
373
+ v `queueConfig` (d.708) — stejným tvarem jako tři `telemetry*Queue` nad ním. Monitoring
374
+ konzument řeší každé platformní jméno, které sám nevlastní, přes `topology`, a
375
+ `src/config/queueConfig` je interní cesta, ne kontrakt; deklarované, ale nevyexportované
376
+ jméno si proto dosud musel držet jako literál. Přejmenování v `queueConfig` export posune
377
+ s sebou, chybějící záznam selže hned.
378
+
379
+ ### Added — `BaseClient.publishTelemetry()`: vlastní události klienta mají vlastní kanál (d.724)
380
+
381
+ Kanál umírá na první odmítnutí, které potká, a bere s sebou každou nepotvrzenou publikaci.
382
+ Klient přitom vlastní události (`message_dlq` na `monitoring.workflow.fanout`) posílal po
383
+ tomtéž ConfirmChannelu, na kterém čekaly zprávy volajícího — ve vhostu bez toho fanoutu
384
+ tak 404 kvůli události shodil obchodní zprávu (změřeno d.711: tři ze sedmi běhů
385
+ nesouvisející sady).
386
+
387
+ - `publishTelemetry(queue, message, options?)` publikuje stejně jako `publish()`, ale po
388
+ druhém CONFIRM kanálu s rolí `telemetry`. Ten vzniká až s první takovou publikací (u
389
+ klienta, který žádnou neposílá, nikdy) a `disconnect()` ho zavírá spolu s ostatními.
390
+ `getChannelState()` ho hlásí jako čtvrtý klíč `telemetry` (d.1007, výše).
391
+ - Kanál se vybírá výslovně, nikdy podle jména fronty.
392
+ - `publishToMonitoringResilient()` (a `publishToMonitoringWorkflow()` /
393
+ `publishToMonitoringServices()`, které ho volají) publikuje přes
394
+ `mqClient.publishTelemetry()`. Klienta, který tu metodu nemá, ohlásí `warn`
395
+ (`Monitoring publish skipped: the client has no publishTelemetry() …`) a vrátí `false`,
396
+ místo aby publikoval po obchodním kanálu. `BaseClient` i jeho podtřídy (např.
397
+ `ConnectorMQClient` z `@onlineapps/conn-infra-mq`) ji dědí.
398
+
399
+ ### Added — pozdní odmítnutí od brokeru se ohlásí: `warn` a událost `publish:refused-late` (d.801b)
400
+
401
+ Když `brokerAnswerTimeout` odpoví volajícímu dřív než broker, callback publishe běží dál
402
+ a sestaví správný verdikt — a dřív ho předal už vyřízené promise, kde zmizel. Nově se
403
+ takový verdikt ohlásí: `warn` v tvaru `[Context] Problem - Expected/Fix` (cíl, kód brokeru,
404
+ třída verdiktu; `Fix:` říká, že vrstva publishe zprávu brala jako nepotvrzenou, a nabízí
405
+ zvednout strop nad změřenou latenci) a událost `publish:refused-late` s `queue`,
406
+ `exchange`, `target`, `kind`, `code`, `verdict`, `error` (verdikt jako objekt), `attempt`
407
+ a `budgetMs`. Strop ani výsledek promise se tím nemění; podle události jedná `PublishLayer`
408
+ (d.801c, výše).
409
+
410
+ ### Fixed — `_ensureConsumerChannel()` hlásí změřené počty a obnova s nepřipojeným konzumentem není „✓" (d.1003d)
411
+
412
+ - **Počty**: nad kanálem, který už žil (i po obnově, která ho vytvořila), vracela metoda
413
+ `{ reRegistered: <počet sledovaných>, failed: 0 }` — konzument, jehož re-registraci broker
414
+ odmítl (zůstává sledovaný bez tagu, d.262 a d.386b), se počítal jako re-registrovaný.
415
+ Nově se počítá podle `_isConsumerAttached()`: `reRegistered` = připojení, `failed` =
416
+ sledovaní bez připojení. Tvar výsledku beze změny; v produkčním kódu ho nikdo nečte
417
+ (`consume()` a handler zavření kanálu ho zahazují), čtou ho testy.
418
+ - **Souhrn obnovy**: `✓ Consumer channel recreated (re-registered: N, failed: M)` stál na
419
+ INFO se značkou ✓ i při `failed > 0`. Nově je to `warn` bez ✓, který jmenuje fronty bez
420
+ konzumenta; úspěšná obnova má dál ✓ na INFO. Odmítnutí jednotlivé fronty zůstává řádkem
421
+ `error` jako dosud.
422
+ - **Souhrn obnovy spojení** (d.1003e): reconnect logoval `✓ Consumer channel recreated` na INFO
423
+ bez počtů i při `failed > 0`. Totéž pravidlo jako výš — při `failed > 0` `warn` bez ✓ se jmény
424
+ front, jinak ✓ na INFO nově i s počty.
425
+
426
+ ### Fixed — handler zavření frontového kanálu trvalé selhání obnovy nespolkne (d.1003c)
427
+
428
+ Handler `'close'` frontového kanálu logoval selhání okamžité obnovy vždy jen jako `warn`,
429
+ přechodné i trvalé. Nově čte totéž pravidlo jako send a consumer kanál
430
+ (`_connectionIsGoingAway()` nad `cause` chyby z `_ensureQueueChannel()`): spojení, které
431
+ odchází, zůstává `warn`; cokoli jiného jde řádkem `error` do logu a na kanál `error` klienta.
432
+ Líná obnova se nemění — kanál zůstane prázdný a příští operace nad frontou
433
+ (`_requireQueueChannel()`) ho vytvoří jako dosud. Nová událost ani nový tvar chyby nevznikl.
434
+
435
+ ### Fixed — handler zavření obnovuje kanál té role, která zemřela, a consumer kanál už trvalé selhání obnovy nespolkne (d.1003b)
436
+
437
+ - **Role send kanálu**: handler zavření visí na publisher i telemetry kanálu, ale obnovu
438
+ volal bez role — po smrti telemetry kanálu „obnovil" živý publisher (nic) a telemetry
439
+ zůstal prázdný až do příští události. Handler teď nese roli kanálu, na kterém visí, a
440
+ obnoví tu. Smrt publisher kanálu se chová jako dosud.
441
+ - **Consumer kanál**: jeho handler zavření polykal každé selhání obnovy jako `warn`.
442
+ Přechodné (spojení odchází) zůstává `warn`; jakékoli jiné jde na kanál `error` klienta
443
+ a řádkem `error` do logu — konzumenti už tiše nepřestanou dostávat zprávy.
444
+ - **Jedno pravidlo** „odmítnuté otevření kanálu je přechodné" (`IllegalOperationError`,
445
+ nebo spojení, které už ohlásilo `'close'`) má jedno tělo, `_connectionIsGoingAway()`,
446
+ a čtou ho obnova send kanálu i handler consumer kanálu. Nová událost ani nový tvar chyby
447
+ nevznikl.
448
+
449
+ ### Fixed — obnova send kanálu: spojení, které spadlo uprostřed otevírání, je přechodné; trvalé selhání obnovy už handler zavření nespolkne (d.1003)
450
+
451
+ - **`_ensurePublisherChannel()`**: spadne-li spojení UPROSTŘED `createConfirmChannel()`,
452
+ amqplib odmítne čekající RPC holým `Error('Channel ended, no reply will be forthcoming')` —
453
+ bez třídy i `code` — a obnova z něj dělala `PermanentPublishError`, takže PublishLayer
454
+ zprávu zahodil místo aby ji podržel do reconnectu. Nově rozhoduje i STAV: odmítnutí, které
455
+ dorazí, když `_connectionAlive` už říká, že spojení je pryč, je `TransientPublishError`.
456
+ Změřeno nad živým brokerem: zavření spojení brokerem během otevírání dalo tuto chybu 8× z 12,
457
+ pokaždé až po ohlášeném `'close'`. Táž věta nad živým spojením zůstává trvalá — text dál
458
+ nerozhoduje.
459
+ - **Handler zavření publisher kanálu**: selhání obnovy kanálu polykal celé jako `warn`.
460
+ Přechodné (`TransientPublishError`) zůstává `warn` — příští publish kanál po reconnectu
461
+ obnoví; jakékoli jiné jde na kanál `error` klienta (kolej nehlášeného selhání, jako
462
+ v `_reportRecoveryFailure()`) a řádkem `error` do logu. Nová událost ani nový tvar chyby
463
+ nevznikl.
464
+
465
+ ### Fixed — `getBufferState()` hlásí změřené číslo, ne vymyšlenou nulu (d.645)
466
+
467
+ Buffer je jeden a `MessageBuffer.size()` JE `_inMemory.size()` (o dva řádky
468
+ vedle, `src/buffer/MessageBuffer.js`). Hlásící strana přesto brala druhé číslo
469
+ ze soukromého členu toho soukromého členu, za dvěma operátory, které dohromady
470
+ uměly jen zakrývat:
471
+
472
+ inMemory: this._publishLayer._buffer._inMemory?.size() || 0
473
+
474
+ `?.` nemůže zafungovat — konstruktor `MessageBuffer` staví `_inMemory` vždy a
475
+ řádek nad tím volá `_buffer.size()` bez ochrany, takže chybějící buffer spadne
476
+ dřív, než se k výrazu dojde. Co ta dvojice ale uměla vyrobit pro buffer, jehož
477
+ vnitřek není ten předpokládaný, je `inMemory: 0` vedle nenulového `size`:
478
+ „drženo, ale nikde" — stav, ve kterém žádný skutečný buffer být nemůže, ohlášený
479
+ bez hlásky a pro čtenáře nerozeznatelný od prázdného bufferu
480
+ (`architecture-principles.md` §3 No Fallbacks, §8 Explicit Over Implicit;
481
+ `automation-gates.md` §5 mlčení je vada).
482
+
483
+ Nově obě čísla pocházejí z jednoho čtení veřejného `size()`. Tvar odpovědi ani
484
+ hodnoty pro skutečný buffer se nemění — `{ size, inMemory }` dál, stejná čísla —
485
+ takže žádný čtenář se nepřepisuje.
486
+
487
+ Doklad: jednotkový tier RED→GREEN nad bufferem, který na `size()` odpoví 2 a
488
+ `_inMemory` nemá (dřív `{ size: 2, inMemory: 0 }`, nově `{ size: 2, inMemory: 2 }`),
489
+ s kontrolními případy nad skutečným bufferem (držená zpráva 1, prázdný 0).
490
+ Integrační tier drží zprávu ve skutečném `InMemoryBuffer` na klientu živého
491
+ brokeru a hlášené číslo měří proti tomu, co broker po flushi opravdu převzal; je
492
+ zelený na obou stranách téhle dávky a říká to ve své hlavičce — živý běh tenhle
493
+ rozdíl vyrobit neumí, protože skutečný `MessageBuffer` `_inMemory` vždycky má.
494
+
495
+ ### Fixed — o chybě rozhoduje třída a kód, ne text hlášky (d.676, d.677, d.683, d.685c, d.696, d.698, d.711, d.1000)
496
+
497
+ Kontrakt `docs/biz/70-contracts/error-handling.md` §6: větvit podle `err.code`, nikdy podle
498
+ znění. Přeformulovaná věta dosud tiše měnila osud zprávy. Tyto větve nově rozhodují třídou
499
+ a kódem:
500
+
501
+ - **`classifyPublishError()`** (d.677, d.683): už otypovaná chyba projde beze změny;
502
+ `code === 404` → `QueueNotFoundError` se jménem od volajícího; `IllegalOperationError`
503
+ z amqplib, `ConnectionError` a `PublishError` → `TransientPublishError`; všechno ostatní
504
+ `PermanentPublishError`. Textová větev nezbyla ani jedna: holé chyby amqplib
505
+ (`'channel closed'`, `'Channel ended, no reply will be forthcoming'`) typuje místo, kde
506
+ přijdou — výměnová noha `_publishOnce()` odpoví na mrtvém kanálu vlastním `PublishError`
507
+ s původní chybou v `cause`, předkontrola fronty odliší očíslované odmítnutí (např. 405
508
+ RESOURCE_LOCKED — trvalé) od bezkódové chyby na mrtvém kanálu (přechodná). Mění se dva
509
+ tvary, které dřív rozhodovala náhoda: chyba se správnou třídou a jiným zněním dostane
510
+ verdikt podle třídy; cizí chyba, jejíž věta obsahuje některé z dřívějších klíčových slov,
511
+ je trvalá.
512
+ - **`isQueueUnavailableError()`** (d.676): prochází řetěz `cause` (nejvýš 8 článků,
513
+ smyčka na sebe sama se zastaví) a ptá se jen na `QueueNotFoundError`, `ConnectionError`,
514
+ `TransientPublishError`, `code === 404` a soketové kódy `ECONNREFUSED` / `ENOTFOUND` /
515
+ `ETIMEDOUT`. Mění se tři vstupy: fronta bez vlastníka (`kind: 'unowned'`) a publikace po
516
+ vyčerpání pokusů jsou nově `true`, chyba, která `404`/`NOT_FOUND` jen zmiňuje v textu, je
517
+ nově `false`.
518
+ - **Synchronní výjimka odeslání** v `_publishOnce()` (d.685c): o tom, zda počkat na
519
+ reconnect a zprávu poslat znovu, rozhoduje `_isChannelAlive()` kanálu, který odesílal,
520
+ ne slova `'Channel ended'` / `'Channel closed'`. Chyba, která ta slova jen obsahuje,
521
+ vyhozená na živém kanálu, se už znovu neposílá, ale vrací volajícímu.
522
+ - **Vypořádání a zavírání** (d.696, d.698): automatický ack v `consume()`, veřejné `ack()`
523
+ a `nack()` poznají zavřený či zavírající se kanál podle `instanceof IllegalOperationError`;
524
+ handler publisher kanálu pozná odmítnutí podle `code === AMQP_PRECONDITION_FAILED` (406,
525
+ nová konstanta v `utils/publishErrors.js` vedle `AMQP_NOT_FOUND`); `disconnect()` bere
526
+ odmítnuté zavření spojení za očekávané jen při `IllegalOperationError` BEZ zapsané
527
+ poruchy (`connection._closeError`, nově zapisované stejně jako u kanálů); obnova
528
+ frontového kanálu čte `err.cause instanceof IllegalOperationError`. Chyba, která větu
529
+ `'Connection closed (by client)'` jen cituje, už se nespolkne jako čekaná. Nedosažitelná
530
+ větev confirm callbacku („unknown delivery tag" / `PRECONDITION_FAILED` → čekat na
531
+ reconnect a publikovat znovu) je smazaná. `_isBrokerRefusal()` textovou půlku drží
532
+ a komentář říká proč: odmítnutý handshake (403 při špatném hesle) přijde jako holý
533
+ `Error` bez `code`.
534
+ - **Kód 404 má jednoho vlastníka** (d.711): `monitoring-publish.js` a
535
+ `transports/rabbitmqClient.js` čtou `AMQP_NOT_FOUND` z `utils/publishErrors.js` místo
536
+ vlastní konstanty a holé `404`. Chování beze změny.
537
+ - **Obnova send kanálu** (d.1000) — poslední textová větev publikační klasifikace:
538
+ `_ensurePublisherChannel()` dávala `TransientPublishError` podle slov `'Connection
539
+ closing'` / `'Connection closed'`, nově podle `instanceof IllegalOperationError`
540
+ (amqplib 0.10.9 tu třídu vydá z `createConfirmChannel()` nad zavírajícím i zavřeným
541
+ spojením, bez `code` — změřeno nad živým brokerem). Mění se dva tvary: jiná chyba, jejíž
542
+ věta ta slova jen obsahuje, je nově `PermanentPublishError`; `IllegalOperationError`
543
+ s jiným zněním (`'Channel closed'` nového kanálu, který zavřelo odcházející spojení) je
544
+ nově přechodná. Text v rozhodování zůstal jen v `_isBrokerRefusal()`, z důvodu, který
545
+ tam stojí (odmítnutý handshake nemá `code`).
546
+
547
+ ### Fixed — odmítnutá konfigurace transportu není nedostupný broker (d.736b)
548
+
549
+ `BaseClient.connect()` stavěl transport uvnitř `try`, jehož `catch` balil všechno do
550
+ `ConnectionError` „a reachable broker … check the MQ url, credentials and network" —
551
+ i odmítnutí, která vydal konstruktor transportu, tedy vady konfigurace (cíl, který není
552
+ neprázdný řetězec, URL s vlastním `heartbeat`, `onFatal`, který není funkce, nepodporovaný
553
+ `type`). `ServiceWrapper._initializeMQ()` takovou chybu četl jako přechodnou a opakoval
554
+ `connect()` podle `wrapper.mq.connectionRetries` (~200 s čekání). Stavba transportu je nově
555
+ před `try` a odmítnutí projde tak, jak je — se svou třídou a hláškou, která jmenuje klíč
556
+ i opravu. `ConnectionError` zůstává tomu, co se stalo na spojení (ECONNREFUSED; odmítnutí
557
+ brokerem dál nese `MQ_CONNECTION_FATAL` / `broker-refused`).
558
+
559
+ ### Fixed — čítač zpráv v letu je svázaný s doručením (d.680)
560
+
561
+ Produkce 20. 9. (dispatcher): `Prefetch utilization high: queue 'workflow.completed' has
562
+ 43/5 messages in-flight (860%)` a číslo rostlo s každým workflow. Snížení čítače sedělo ve
563
+ větvích, které zprávu vypořádají přímo v `consume()`; handler, který si zprávu vypořádá
564
+ sám (`ack()`/`nack()`), konzument s `noAck` a předčasné návraty po smrti kanálu nechaly
565
+ zvýšení stát. Nově jedno zvýšení na začátku callbacku doručení a jedno snížení v jeho
566
+ `finally`. Veřejné API ani tvar události `prefetch:high-utilization` se nemění.
567
+
568
+ ### Fixed — každý časovač klienta je v jedné evidenci `_activeTimers` (d.759e)
569
+
570
+ Opakující se časovače health a prefetch monitoringu vznikaly holým `setInterval`, mimo
571
+ evidenci, kterou `_clearAllTimers()` při teardownu smete a kterou sady tvrdí
572
+ (`_activeTimers.size`). Nově jdou přes `_setInterval()` / `_clearInterval()`;
573
+ `_clearAllTimers()` nejdřív zastaví oba monitory (aby jejich reference nezůstaly ukazovat
574
+ na mrtvý handle a příští start je neodbyl jako „already started"). `connect()` po
575
+ neúspěšném spojení ruší svůj časovač `_clearTimeout()`, takže v evidenci nezůstane handle,
576
+ který už nic nehlídá.
577
+
578
+ ### Tests — komentáře v `src/**` k sadám publishe a prefetch (d.738, d.766)
579
+
580
+ `layers/PublishLayer.js` říká, proč spánek mezi pokusy publishe je holý `setTimeout`
581
+ a ne `_setTimeout()` transportu (čeká `publish()` volajícího, ne smyčka klienta, a
582
+ teardown ho ukončit nemá), se strážní sadou (d.766); `transports/rabbitmqClient.js`
583
+ popisuje, co po d.736a zbývá říct periodické kontrole prefetch (d.738, spolu s převodem tří
584
+ integračních sad na čekací kolej tieru). Chování beze změny.
585
+
586
+ ### Documentation — komentáře v `src/**` bez čísel řádků a ručních verzí (d.646, d.668, d.981)
587
+
588
+ Hlavička `src/index.js` už neuvádí verzi ručně (d.646); komentáře `config/queueConfig.js`
589
+ už netvrdí, že `initInfrastructureQueues()` odvozuje seznam front ze sekcí (od d.668
590
+ vyžaduje `queues`); čísla řádků v komentářích `src/**` nahradily symboly (d.981). Chování
591
+ beze změny.
592
+
593
+ ### Documentation — README: routing key telemetrie má tvar `telemetry.<signal>.<service_name>` (d.994b)
594
+
595
+ Řádek `telemetryLogsQueue` v tabulce `topology` tvrdil, že služby publikují na
596
+ `telemetry.exchange` „se svým jménem jako routing key". Změřeno: `monitoring-core` publikuje
597
+ logy pod `telemetry.logs.<serviceName>` (`src/logger.js`) a exportér pod
598
+ `telemetry.<type>.<service_name>` (`src/exporters/rabbitmq.js`), monitoring konzument váže
599
+ fronty vzorem `telemetry.<type>.*`. Pravdu měl komentář exportu v `src/index.js`; opravena je
600
+ věta v README.
601
+
602
+ ### Changed — tarball už nenese `jest.config.js` ani `jest.integration.config.js` (d.1020)
603
+
604
+ - `.npmignore` je vylučuje. Spouštějí jen sady balíčku (`tests/` se nebalí) a po instalaci je nic nečte;
605
+ `npm pack --dry-run` ubral právě tyto dva soubory.
606
+
7
607
  ## [4.2.0] — 2026-09-25
8
608
 
9
609
  ### Added — služba deklaruje své rekurence a mluví s plánovačem jednou kolejí (d.833, d.840)
@@ -94,6 +694,7 @@ Při té příležitosti se rozšířil tvar `id` v záznamu rekurence: schéma
94
694
  na malá písmena a pomlčky, což je přísnější než konzument (jakýkoli řetězec 1–255 znaků) a
95
695
  odmítalo by to konvenci prvního volajícího `<source_uuid>:<cookbook_ref>`. Nově se odmítá
96
696
  jen bílé místo, které by z klíče udělalo větu.
697
+
97
698
  ### Changed — port `registerRecurring()` klíč, který požadavek nedeklaruje, odmítne (d.864b)
98
699
 
99
700
  `createSchedulerPublisher(…).registerRecurring()` bral jen pojmenované klíče a cokoli
@@ -224,6 +825,28 @@ deklarace není pokyn k vytvoření — zakládá ji dál vlastník prefixu). Pr
224
825
  z toho plyne, že povinný klíč `MONITORING_RESOURCE_EXCHANGE` může po re-pinu
225
826
  zmizet stejně jako pět jmen před ním.
226
827
 
828
+ ### Added — výměna `telemetry.exchange` má šablonu v `queueConfig` (d.708) — doplněno 27. 9. 2026, změna vyšla bez záznamu
829
+
830
+ `telemetry.exchange` žila v `src/config/queueConfig.js` jen v komentáři, zatímco jméno mělo
831
+ tři vlastníky: konstantu v monitoring konzumentu (`infra/api_monitoring/src/consumer/topology.js`)
832
+ a výchozí hodnotu v obou publikátorech `@onlineapps/monitoring-core`. Sekce `telemetry` nese
833
+ nově záznam `exchange` = `{ type: 'exchange', exchangeType: 'topic', durable: true }`, bez
834
+ argumentů a bez dead-letter cesty; jméno se skládá `queueConfig.queueName('telemetry',
835
+ 'exchange')`. Typ a trvanlivost jsou změřené na všech zakládajících stranách, které volaly
836
+ `assertExchange(<jméno>, 'topic', { durable: true })` — broker obě vlastnosti porovnává při
837
+ každé deklaraci, jiná hodnota by byla 406 PRECONDITION-FAILED. Klasifikace prefixu
838
+ `telemetry.` se nemění; producenty ani konzumenta tahle změna nepřepíná. Veřejný export
839
+ jména (`topology.telemetryExchange`) přišel až po vydání — viz `[Unreleased]`, d.708b.
840
+
841
+ ### Changed — šablona výměny `registry.changes` nese typ `fanout` (d.712) — doplněno 27. 9. 2026, změna vyšla bez záznamu
842
+
843
+ `registry.changes` byla jediná výměna v `queueConfig` bez `exchangeType`, takže z ní
844
+ zakladatel deklarovat nemohl — typ je jedna ze dvou vlastností, které broker porovnává při
845
+ každém `assertExchange`. Šablona nově nese `exchangeType: 'fanout'`, hodnotu změřenou
846
+ u jediného zakladatele (`@onlineapps/conn-orch-registry`, `src/registryEventConsumer.js`:
847
+ `assertExchange(<jméno>, 'fanout', { durable: true })`, binding vzorem `''`). Argumenty ani
848
+ dead-letter cestu výměna nezískává; zakladatele změna nepřepíná.
849
+
227
850
  ## [4.0.1] — 2026-09-17
228
851
 
229
852
  ### Fixed — zrušení konzumenta brokerem přestalo být tiché (d.605)