@nodefony/realtime 10.0.0-beta.1 → 10.0.0-beta.3

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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +17 -10
  5. package/dist/nodefony/config/config.js +6 -3
  6. package/dist/nodefony/config/configSchema.js +19 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +42 -15
  8. package/dist/nodefony/decorators/realtimeDecorators.js +68 -17
  9. package/dist/nodefony/src/backplane/RedisBackplane.js +21 -4
  10. package/dist/nodefony/src/backplane/bootNotices.js +43 -0
  11. package/dist/nodefony/src/backplane/publishQueue.js +1 -4
  12. package/dist/nodefony/src/cluster/ClusterProbeClient.js +15 -4
  13. package/dist/nodefony/src/guards.js +41 -0
  14. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -4
  15. package/dist/nodefony/src/server/RealtimeController.js +120 -50
  16. package/dist/nodefony/src/server/RealtimeHub.js +29 -51
  17. package/dist/nodefony/src/server/ServerRealtimeSocket.js +13 -5
  18. package/dist/nodefony/src/server/syslogUplink.js +4 -2
  19. package/dist/nodefony/src/service/RealtimeService.js +3 -2
  20. package/dist/nodefony/testing/index.js +41 -1
  21. package/dist/types/nodefony/config/configSchema.d.ts +12 -0
  22. package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
  23. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
  24. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
  25. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
  26. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
  27. package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
  28. package/dist/types/nodefony/src/guards.d.ts +32 -0
  29. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
  30. package/dist/types/nodefony/src/server/RealtimeController.d.ts +24 -4
  31. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
  32. package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
  33. package/dist/types/nodefony/testing/index.d.ts +2 -2
  34. package/docs/actions.md +124 -41
  35. package/docs/architecture.md +93 -91
  36. package/docs/cadence-et-contre-pression.md +634 -0
  37. package/docs/configuration.md +66 -56
  38. package/docs/cookbook-chat.md +39 -33
  39. package/docs/index.md +47 -31
  40. package/docs/observabilite.md +21 -18
  41. package/docs/protocole.md +79 -65
  42. package/docs/securite.md +69 -65
  43. package/docs/vocabulaire.md +80 -79
  44. package/package.json +16 -12
  45. package/docs/couverture-bancs.md +0 -139
package/docs/index.md CHANGED
@@ -37,7 +37,7 @@ coverageModule: realtime
37
37
 
38
38
  ## 🧭 Par où commencer
39
39
 
40
- Trois parcours selon ce que tu viens faire. L'ordre compte : chaque étape suppose la précédente.
40
+ Quatre parcours selon ce que tu viens faire. L'ordre compte : chaque étape suppose la précédente.
41
41
 
42
42
  **Je découvre le temps réel Nodefony** — comprendre le modèle avant d'écrire quoi que ce soit.
43
43
 
@@ -69,20 +69,31 @@ abonné connecté à l'autre.
69
69
  3. [`@nodefony/security`](../../security/docs/index.md) — le pare-feu applicatif, qui protège le
70
70
  temps réel avec **le même** modèle de zones que le web.
71
71
 
72
+ **Un client ne suit plus** — l'écran prend du retard, la mémoire du serveur monte, une connexion
73
+ se ferme en `1013`.
74
+
75
+ 1. [Cadence et contre-pression](./cadence-et-contre-pression.md) — les deux étages qui encaissent
76
+ un client lent : la cadence que le client ralentit lui-même, la file que le serveur refuse de
77
+ laisser grossir. **Commence par le schéma général**, puis les courbes.
78
+ 2. [Observabilité](./observabilite.md) — lire les clients lents et les trames refusées dans la
79
+ sonde, avant que quelqu'un ne se plaigne.
80
+ 3. [Configuration](./configuration.md) — serrer les seuils, sur les **deux** serveurs WebSocket.
81
+
72
82
  ## 🗂️ Les pages du module
73
83
 
74
84
  Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce qu'on y trouve.
75
85
 
76
- | Page | Ce qu'elle résout | Tu en as besoin quand… |
77
- | ---------------------------------------- | -------------------------------------------------- | -------------------------------------------- |
78
- | [Vocabulaire](./vocabulaire.md) | les mots du domaine, une bonne fois | tu lis la doc, ou tu discutes archi |
79
- | [Architecture](./architecture.md) | le trajet d'une frame, étage par étage | tu veux comprendre plutôt que régler |
80
- | [Configuration](./configuration.md) | drivers, cloisonnement, bornes, contrôle d'origine | tu déploies, ou tu changes de topologie |
81
- | [Sécurité](./securite.md) | identité à la poignée de main, droits par canal | ta socket est joignable depuis un navigateur |
82
- | [Protocole](./protocole.md) | la grammaire d'une frame, et les codes d'erreur | tu débogues le fil, ou tu écris un client |
83
- | [Actions RPC](./actions.md) | appeler le serveur et attendre une réponse | tu veux savoir si l'appel a marché |
84
- | [Observabilité](./observabilite.md) | la sonde, les canaux de santé, les écrans | tu te demandes si ta socket va bien |
85
- | [Cookbook — un chat](./cookbook-chat.md) | l'exemple complet, client et serveur | tu veux du code qui marche tout de suite |
86
+ | Page | Ce qu'elle résout | Tu en as besoin quand… |
87
+ | ------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------- |
88
+ | [Vocabulaire](./vocabulaire.md) | les mots du domaine, une bonne fois | tu lis la doc, ou tu discutes archi |
89
+ | [Architecture](./architecture.md) | le trajet d'une frame, étage par étage | tu veux comprendre plutôt que régler |
90
+ | [Configuration](./configuration.md) | drivers, cloisonnement, bornes, contrôle d'origine | tu déploies, ou tu changes de topologie |
91
+ | [Sécurité](./securite.md) | identité à la poignée de main, droits par canal | ta socket est joignable depuis un navigateur |
92
+ | [Protocole](./protocole.md) | la grammaire d'une frame, et les codes d'erreur | tu débogues le fil, ou tu écris un client |
93
+ | [Actions RPC](./actions.md) | appeler le serveur et attendre une réponse | tu veux savoir si l'appel a marché |
94
+ | [Observabilité](./observabilite.md) | la sonde, les canaux de santé, les écrans | tu te demandes si ta socket va bien |
95
+ | [Cadence et contre-pression](./cadence-et-contre-pression.md) | un client trop lent : ralentir, refuser, couper | un écran prend du retard, ou la mémoire monte |
96
+ | [Cookbook — un chat](./cookbook-chat.md) | l'exemple complet, client et serveur | tu veux du code qui marche tout de suite |
86
97
 
87
98
  ```nodefony-cards
88
99
  [
@@ -107,6 +118,9 @@ Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce q
107
118
  { "icon": "📡", "title": "observabilite", "href": "./observabilite.md",
108
119
  "desc": "Ce que la sonde du hub expose, les canaux de santé et leur cadence, ce que Studio en montre — et comment écrire la sienne sans faire fuir la mémoire ni empêcher le processus de s'arrêter.",
109
120
  "meta": "tu te demandes si ta socket va bien" },
121
+ { "icon": "🚦", "title": "cadence-et-contre-pression", "href": "./cadence-et-contre-pression.md",
122
+ "desc": "Deux étages qui ne se parlent pas et se rencontrent quand même : le client ralentit la cadence de ses canaux d'état (AIMD), le serveur refuse d'empiler au-delà de 4 Mio puis ferme en 1013. Avec les courbes calculées par le vrai code, et les sondes qui le montrent en production.",
123
+ "meta": "un écran prend du retard, ou la mémoire du serveur monte" },
110
124
  { "icon": "🍳", "title": "cookbook-chat", "href": "./cookbook-chat.md",
111
125
  "desc": "Un salon de discussion complet : configuration, service métier, contrôleur, client navigateur, puis le passage en cluster.",
112
126
  "meta": "du code qui tourne, à déformer vers ton besoin" }
@@ -119,18 +133,18 @@ Quatre propriétés, toutes vérifiables dans le code — c'est ce qui justifie
119
133
  WebSocket brute.
120
134
 
121
135
  **Un contrôleur temps réel est un contrôleur.** `RealtimeController`
122
- (`RealtimeController.ts:144`) étend le `Controller` du framework : il se déclare avec les mêmes
136
+ (`RealtimeController.ts:203`) étend le `Controller` du framework : il se déclare avec les mêmes
123
137
  décorateurs de route, reçoit la même injection, passe par le même pare-feu. HTTP et WebSocket ne sont
124
138
  pas deux applications à écrire deux fois, mais deux entrées du même pipeline.
125
139
 
126
140
  **Une connexion, N canaux, dans les deux sens.** Le client s'abonne à autant de canaux qu'il veut sur
127
141
  la même socket. Trois formes de trafic coexistent : le serveur diffuse (`@RealtimeChannel`,
128
- `realtimeDecorators.ts:192`), le client appelle et attend une réponse (`@RealtimeAction`,
129
- `realtimeDecorators.ts:142`), le client pousse sans attendre (`@RealtimeInbound`,
130
- `realtimeDecorators.ts:231`). Rien n'est ouvert qui n'ait été déclaré.
142
+ `realtimeDecorators.ts:240`), le client appelle et attend une réponse (`@RealtimeAction`,
143
+ `realtimeDecorators.ts:191`), le client pousse sans attendre (`@RealtimeInbound`,
144
+ `realtimeDecorators.ts:278`). Rien n'est ouvert qui n'ait été déclaré.
131
145
 
132
146
  **Le travail est fait une fois par processus, pas une fois par client.** Le `RealtimeHub`
133
- (`RealtimeHub.ts:213`) tient **un seul producteur par canal** : le premier abonné le démarre, le
147
+ (`RealtimeHub.ts:214`) tient **un seul producteur par canal** : le premier abonné le démarre, le
134
148
  dernier départ l'arrête. Chaque connexion supplémentaire n'ajoute qu'une destination de diffusion.
135
149
  Un salon suivi par mille personnes coûte un producteur, pas mille.
136
150
 
@@ -143,7 +157,7 @@ toucher au cœur.
143
157
  > [!IMPORTANT]
144
158
  > **Rien ne franchit la frontière du processus sans intention explicite.** Par défaut, un canal reste
145
159
  > local. Il faut le déclarer diffusable (`@RealtimeBroadcast`,
146
- > `realtimeDecorators.ts:342`) pour qu'il emprunte le backplane. C'est volontaire : un canal
160
+ > `realtimeDecorators.ts:420`) pour qu'il emprunte le backplane. C'est volontaire : un canal
147
161
  > d'observation qui décrit l'état d'**un** pod n'aurait aucun sens répliqué sur les autres.
148
162
 
149
163
  Le module se déclare par ailleurs **non critique** (`Realtime.critical`, `index.ts:144`) et son
@@ -239,11 +253,11 @@ export default ChatController;
239
253
  Le client est isomorphe : c'est le cœur `nodefony` lui-même, importable côté navigateur.
240
254
 
241
255
  ```ts
242
- import { RealtimeClient } from "nodefony/client";
256
+ import { NodefonySocket } from "nodefony/client";
243
257
 
244
258
  // `shared` réutilise la connexion existante pour une même URL : dix composants
245
259
  // qui écoutent dix canaux ouvrent UNE socket, pas dix.
246
- const socket = RealtimeClient.shared({
260
+ const socket = NodefonySocket.shared({
247
261
  url: "wss://127.0.0.1:5152/chat/realtime",
248
262
  });
249
263
 
@@ -295,7 +309,7 @@ Côté serveur : `RealtimeController` (la classe à étendre), les décorateurs
295
309
  publier depuis n'importe quel service injecté, le `RealtimeHub` et sa sonde, les trois backplanes
296
310
  natifs, et le registre de drivers pour brancher le tien.
297
311
 
298
- Côté client : `RealtimeClient` via le subpath `nodefony/client`, et les hooks React via
312
+ Côté client : `NodefonySocket` via le subpath `nodefony/client`, et les hooks React via
299
313
  `nodefony/react`.
300
314
 
301
315
  Les signatures exactes vivent dans le graphe généré — `jq '.symbols.RealtimeHub' .ai/symbols.json` —
@@ -304,7 +318,7 @@ jamais recopiées ici : elles divergeraient en silence.
304
318
  ## ⚙️ Configuration
305
319
 
306
320
  Un seul point d'entrée : `use("@nodefony/realtime", { … })` dans `nodefony.config.ts`, validé au boot
307
- contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Cinq blocs :
321
+ contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Six blocs :
308
322
 
309
323
  - `backplane` — le driver de fan-out et son espace de nommage. Ce cloisonnement devient
310
324
  indispensable dès que **deux déploiements partagent le même Redis** : sans lui, leurs publications
@@ -313,6 +327,7 @@ contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Cinq bloc
313
327
  - `limits` — le plafond de canaux par connexion, garde anti-saturation mémoire.
314
328
  - `slowConsumer` — le seuil à partir duquel un client trop lent est signalé par la sonde.
315
329
  - `cluster.probe` — la sonde agrégée du pod, en mode multi-workers.
330
+ - `clientLogs` — la réception des journaux du navigateur (canal montant), fermée par défaut.
316
331
 
317
332
  Chaque bloc, ses valeurs d'usine et les situations qui justifient d'en changer sont détaillés dans
318
333
  [Configuration](./configuration.md).
@@ -325,7 +340,7 @@ vue de tous les workers d'un pod.
325
340
 
326
341
  Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
327
342
  diffusion, connexions, octets et frames, pression d'écriture. C'est la même donnée que celle rendue à
328
- l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
343
+ l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:847`).
329
344
 
330
345
  ## 🧪 Tests & couverture
331
346
 
@@ -333,24 +348,25 @@ Les compteurs sont régénérés depuis vitest, jamais figés dans cette prose.
333
348
  ici, c'est **ce que les suites prouvent** — et ce qu'elles ne prouvent qu'à condition d'avoir
334
349
  l'infrastructure sous la main.
335
350
 
336
- | Type | Où | Ce qui est prouvé |
337
- | --------------- | ------------------------------- | ----------------------------------------------------------------- |
338
- | Unitaire | `nodefony/tests/unit/**` | hub, registre de drivers, schéma de configuration, décorateurs |
339
- | Intégration | `nodefony/tests/integration/**` | socket réelle : abonnements, chemins de contrôleur, autorisation |
340
- | Multi-processus | `clusterIpc.e2e.test.ts` | fan-out entre workers, sans aucune infrastructure externe |
341
- | Cross-machine | `redisCluster.e2e.test.ts` | fan-out entre pods via pub/sub Redis |
342
- | Tests d'attaque | `*.attack.test.ts` | plafond de canaux, révocation d'identité, politique non appliquée |
351
+ | Type | Où | Ce qui est prouvé |
352
+ | --------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
353
+ | Unitaire | `nodefony/tests/unit/**` | hub, registre de drivers, schéma de configuration, décorateurs |
354
+ | Intégration | `nodefony/tests/integration/**` | socket réelle : abonnements, chemins de contrôleur, autorisation |
355
+ | Multi-processus | `clusterIpc.e2e.test.ts` | fan-out entre workers, sans infrastructure externe (`npm run test:cluster`) |
356
+ | Cross-machine | `redisCluster.e2e.test.ts` | fan-out entre pods via pub/sub Redis |
357
+ | Tests d'attaque | `*.attack.test.ts` | plafond de canaux, révocation d'identité, politique non appliquée, injection sur le bus |
343
358
 
344
359
  > [!WARNING]
345
360
  > **Une suite verte ne prouve rien sur le fan-out cross-machine.** Le banc Redis est doublement
346
361
  > conditionnel : il ne s'exécute que si tu le demandes (`NF_RUN_CLUSTER_E2E=1`), et il se **saute** de
347
362
  > lui-même si aucun Redis n'est joignable. Or un test sauté compte comme un succès — on peut donc
348
363
  > lire « tout est vert » sur une suite qui n'a jamais ouvert une seule connexion. Le fan-out entre
349
- > workers d'un même pod, lui, ne demande aucune infrastructure et tourne toujours.
364
+ > workers d'un même pod ne demande aucune infrastructure externe, mais ne tourne que dans
365
+ > `npm run test:cluster` avec `NF_RUN_CLUSTER_E2E=1`.
350
366
  >
351
367
  > ```bash
352
368
  > # Le seul run qui prouve réellement le fan-out entre machines.
353
- > NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm test
369
+ > NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm run test:cluster
354
370
  > ```
355
371
 
356
372
  ## 🔗 Pour aller plus loin
@@ -96,7 +96,7 @@ temps de réponse — elle mesure **ce qui n'est pas encore parti**.
96
96
  Trois partis pris, tous lisibles dans le code.
97
97
 
98
98
  **1. La sonde est une lecture pure, jamais un collecteur.** `RealtimeHub.probe()`
99
- (`RealtimeHub.ts:865`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
99
+ (`RealtimeHub.ts:847`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
100
100
  Aucune entrée-sortie, aucune exception possible, aucun état de lecture conservé. Appeler la sonde
101
101
  mille fois par seconde ne changerait rien à ce que le hub fait par ailleurs.
102
102
 
@@ -228,10 +228,10 @@ Côté navigateur, deux lignes suffisent à consommer le canal :
228
228
 
229
229
  ```ts
230
230
  // frontend/src/shopHealth.ts
231
- import { RealtimeClient } from "nodefony/client";
231
+ import { NodefonySocket } from "nodefony/client";
232
232
 
233
233
  const scheme = window.location.protocol === "https:" ? "wss" : "ws";
234
- const socket = new RealtimeClient({
234
+ const socket = new NodefonySocket({
235
235
  url: `${scheme}://${window.location.host}/shop/realtime`,
236
236
  });
237
237
 
@@ -265,7 +265,7 @@ Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts:61`). Quatre familles, un di
265
265
  | ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
266
266
  | `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
267
267
  | `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
268
- | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:854`). |
268
+ | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:836`). |
269
269
  | `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
270
270
 
271
271
  Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
@@ -316,13 +316,15 @@ deux autres. Le débit en octets par seconde se dérive de deux photos.
316
316
 
317
317
  La source par connexion est `IRealtimeConnProbe` (`IRealtimeProbe.ts:25`), implémentée par le
318
318
  transport. Les seuils d'ACTION sont distincts du seuil de comptage, et ils sont
319
- **configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:1044`) : au-delà de
319
+ **configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:1226`) : au-delà de
320
320
  `websocket.maxBackpressure` (4 MiB par défaut) la politique `websocket.backpressurePolicy`
321
- s'applique — `drop` par défaut, la frame est **jetée** puisque les canaux d'état sont « le
322
- dernier gagne » et que le prochain instantané la remplacera. La connexion est **fermée** en
323
- `1013` (« réessaie plus tard ») après `websocket.backpressureCloseAfterDrops` frames jetées
324
- CONSÉCUTIVES (1000 par défaut) — une seule frame qui repart remet le compteur à zéro — ou
325
- dès le premier dépassement si la politique est `close`.
321
+ s'applique — `drop` par défaut, la frame est **jetée**. Sur un canal d'état c'est sans
322
+ conséquence, le prochain instantané la remplace ; mais le rejet ne trie rien, et une réponse RPC
323
+ ou un événement jetés sont **perdus**. La connexion est **fermée** en `1013` (« réessaie plus
324
+ tard ») quand le **solde** de refus atteint `websocket.backpressureCloseAfterDrops` (1000 par
325
+ défaut) — +1 par frame jetée, −1 par frame envoyée, si bien qu'un pic passager redescend — ou
326
+ dès le premier dépassement si la politique est `close`. Le mécanisme complet, avec ses courbes :
327
+ [Cadence et contre-pression](./cadence-et-contre-pression.md).
326
328
 
327
329
  > [!IMPORTANT]
328
330
  > `drops` qui croît n'est **pas** une panne : c'est la protection qui fonctionne. Ce qui serait une
@@ -447,7 +449,8 @@ après fusion et validation — la seule vue qui dit ce qui s'applique, plutôt
447
449
 
448
450
  > [!CAUTION]
449
451
  > `slowConsumer.bytes` ne pilote **que la métrique**. Les seuils qui jettent une frame ou ferment une
450
- > connexion sont des constantes du transport. Baisser cette clé rend le signalement plus précoce ; ça
452
+ > connexion se règlent côté `@nodefony/http` (`websocket.maxBackpressure`, `.backpressurePolicy`,
453
+ > `.backpressureCloseAfterDrops`). Baisser cette clé rend le signalement plus précoce ; ça
451
454
  > ne change rien au comportement de la socket.
452
455
 
453
456
  Couper la sonde cluster est un **contournement total**, pas une mise en sourdine : sans elle, il n'y a
@@ -461,11 +464,11 @@ instantané est exact — et ne dit rien des trois autres. Pire : deux appels co
461
464
  sur deux workers et sembler se contredire.
462
465
 
463
466
  **La réponse de Nodefony est un modèle _push_.** Chaque worker remonte périodiquement sa santé au
464
- maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts:170`) ; le maître fusionne et
467
+ maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts:190`) ; le maître fusionne et
465
468
  rediffuse ; chaque worker met le résultat en cache. **N'importe lequel** sert alors la vue pod en temps
466
- constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:291`).
469
+ constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:306`).
467
470
 
468
- La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:46`), est une fonction pure — et sa règle
471
+ La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:60`), est une fonction pure — et sa règle
469
472
  mérite d'être connue :
470
473
 
471
474
  | Grandeur | Agrégation | Pourquoi |
@@ -482,7 +485,7 @@ garde le détail par worker pour le forage.
482
485
  ### On paie ce qu'on regarde
483
486
 
484
487
  Les sondes coûteuses ne tournent pas en permanence. Le maître peut demander à un worker précis
485
- d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts:239`) — quand
488
+ d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts:259`) — quand
486
489
  un humain ouvre le forage de ce `pid`. La sonde riche (espaces mémoire, cycles de ramasse-miettes,
487
490
  descripteurs actifs) est alors allouée ; à la fermeture de l'écran, elle est libérée et son observateur
488
491
  détaché.
@@ -503,7 +506,7 @@ l'orchestrateur finit par le tuer de force à l'expiration du délai de grâce.
503
506
 
504
507
  La règle est tenue partout dans le code, y compris là où on ne l'attendrait pas — le tick de
505
508
  revalidation d'identité du hub, qui ne démarre qu'au premier inscrit, est lui aussi détaché
506
- (`RealtimeHub.ts:437`), tout comme le report de la sonde cluster (`ClusterProbeClient.ts:170`) et le
509
+ (`RealtimeHub.ts:443`), tout comme le report de la sonde cluster (`ClusterProbeClient.ts:170`) et le
507
510
  minuteur générique des canaux (`providers.ts:465`).
508
511
 
509
512
  ```ts ignore
@@ -552,7 +555,7 @@ aux sondes qui instrumentent chaque opération et se paient donc à l'usage.
552
555
  | `publishTotal` grimpe, `fanoutTotal` stagne | Tu publies dans le vide : personne n'est abonné au canal visé | Comparer le nom publié et le nom souscrit — presque toujours une faute de frappe |
553
556
  | Deux appels consécutifs à `/api/health` donnent des chiffres contradictoires | En cluster, la requête tombe sur un worker au hasard et rend une vue per-instance | Lire le champ `cluster` ; si absent, la sonde pod est coupée ou en démarrage à froid |
554
557
  | La mémoire du processus grimpe sans fuite apparente | Une sonde alloue à chaque tick, ou un client lent fait grossir la file d'envoi | Lire `backpressure.totalBufferedAmount` ; simplifier le corps du tick |
555
- | Baisser `slowConsumer.bytes` ne ferme pas les clients lents | Cette clé pilote le **comptage** de la sonde ; les seuils d'action sont des constantes du transport | Rien à régler : ce comportement n'est pas configurable |
558
+ | Baisser `slowConsumer.bytes` ne ferme pas les clients lents | Cette clé pilote le **comptage** de la sonde ; les seuils d'action sont ceux du serveur WebSocket | Régler `maxBackpressure` / `backpressureCloseAfterDrops` dans `@nodefony/http` |
556
559
  | La cadence demandée n'est pas respectée | Elle a été **ramenée dans les bornes** du canal, silencieusement | Lire les bornes du canal ; vérifier le nom réellement souscrit dans `channels[]` |
557
560
  | Deux abonnés au même canal, deux minuteurs | Ils ont demandé des cadences différentes — un canal cadencé est distinct par cadence | Laisser la cadence par défaut (aucun suffixe) quand elle convient : les abonnés partagent un minuteur |
558
561
  | Rien n'est visible alors que le trafic est réel | La sonde n'observe **jamais** ce à quoi personne n'est abonné : un canal sans abonné n'existe pas | Ouvrir l'écran ou s'abonner ; c'est le comportement voulu, pas une panne |
@@ -566,7 +569,7 @@ infrastructure, puisque la sonde est une lecture pure.
566
569
  | Suite | Ce qui est prouvé |
567
570
  | --- | --- |
568
571
  | `RealtimeHub.test.ts` | Hub vide → instantané à zéro ; abonnés et publications par canal ; `publishTotal` vs `fanoutTotal` ; comptage des consommateurs lents et `maxBufferedAmount` ; sortie du registre à la déconnexion ; frames entrantes ; descripteur `local` du backplane |
569
- | `WsConnectionTransport.test.ts` | Les deux seuils de contre-pression : abandon « le dernier gagne », puis fermeture `1013` |
572
+ | `WsConnectionTransport.test.ts` | Les seuils de contre-pression : abandon « le dernier gagne », puis fermeture `1013` |
570
573
  | `ClusterProbeClient.test.ts` | Fusion pod (sommes vs maximum), report périodique et cache du snapshot, forage `enrich`/`rich`, repli per-instance quand la sonde est absente |
571
574
 
572
575
  **Ce qui manque, et qu'il faut savoir** : il n'existe pas de banc de charge dédié à la sonde