@nodefony/realtime 10.0.0-beta.2 → 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.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.152.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 +111 -28
  35. package/docs/architecture.md +77 -75
  36. package/docs/cadence-et-contre-pression.md +634 -0
  37. package/docs/configuration.md +39 -34
  38. package/docs/cookbook-chat.md +31 -27
  39. package/docs/index.md +35 -21
  40. package/docs/observabilite.md +17 -15
  41. package/docs/protocole.md +74 -63
  42. package/docs/securite.md +49 -49
  43. package/docs/vocabulaire.md +76 -75
  44. package/package.json +16 -12
  45. package/docs/couverture-bancs.md +0 -139
@@ -57,8 +57,8 @@ est validé puis **gelé** avant que le moindre canal existe.
57
57
  flowchart TD
58
58
  SCHEMA["Schéma Zod du module<br/>realtimeConfigSchema — les valeurs d'usine"]
59
59
  APP["Ton application<br/>use('@nodefony/realtime', { … }) dans nodefony.config.ts"]
60
- ENVGEN["Déploiement<br/>NF__REALTIME__&lt;CHEMIN&gt;=valeur"]
61
- BUILD["Builder<br/>defineRealtimeConfig() : parse Zod → NF_REALTIME_DRIVER → freeze"]
60
+ ENVGEN["Déploiement<br/>NF_REALTIME_DRIVER · NF__REALTIME__&lt;CHEMIN&gt;=valeur"]
61
+ BUILD["Builder<br/>defineRealtimeConfig() : parse Zod → freeze"]
62
62
  HUB["RealtimeHub<br/>garde d'origine · plafond de canaux · seuil de sonde"]
63
63
  BP["Driver de backplane<br/>résolu par nom dans le registre"]
64
64
  SCHEMA --> APP --> ENVGEN --> BUILD
@@ -131,10 +131,11 @@ explication en `.describe()`. Les défauts effectifs sont **dérivés** du sché
131
131
  d'usine serait écrite, donc pas de dérive silencieuse possible entre la doc du champ et son
132
132
  comportement.
133
133
 
134
- Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts:35`) reste **pur** : il analyse l'entrée,
135
- applique les variables d'environnement dédiées (`NF_REALTIME_DRIVER`,
136
- `NF_REALTIME_BACKPLANE_NAMESPACE`, `NF_REALTIME_BACKPLANE_SECRET` — toutes prioritaires sur le
137
- fichier), et gèle le résultat. Il ne décide rien.
134
+ Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts:39`) reste **pur** : il analyse l'entrée
135
+ et gèle le résultat. Il ne lit aucune variable d'environnement. Les trois variables du module
136
+ (`NF_REALTIME_DRIVER`, `NF_REALTIME_BACKPLANE_NAMESPACE`, `NF_REALTIME_BACKPLANE_SECRET`) sont
137
+ déclarées sur leur champ dans le schéma (`.meta({ env })`, `config.ts:59`, `:76`, `:93`) et posées
138
+ par le Kernel **avant** l'analyse : le schéma les valide comme une valeur écrite dans le fichier.
138
139
 
139
140
  Le module **augmente le registre de types** (`index.ts:167`) : `use("@nodefony/realtime", { … })`
140
141
  propose ses clés en autocomplétion et **refuse** une clé inconnue à la compilation.
@@ -251,7 +252,7 @@ regarder quand un message ne traverse pas.
251
252
 
252
253
  > [!TIP]
253
254
  > Ces quatre lignes viennent du même endroit du code (`Realtime.#wireBackplane()`,
254
- > `src/packages/@nodefony/realtime/index.ts:279`) et de la carte d'identité que chaque driver publie.
255
+ > `src/packages/@nodefony/realtime/index.ts:292`) et de la carte d'identité que chaque driver publie.
255
256
  > Aucun repli n'est silencieux : si le fan-out est dégradé, c'est écrit.
256
257
 
257
258
  ## ⚙️ Le schéma, clé par clé
@@ -288,7 +289,7 @@ contre-pression, eux, se règlent sur le serveur WebSocket de `@nodefony/http`
288
289
 
289
290
  Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
290
291
  enregistrée, pas de backplane câblé, pas de sonde de pod
291
- (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:239`), et le service devient
292
+ (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:246`), et le service devient
292
293
  inerte (`RealtimeService.init()`, `RealtimeService.ts:77`).
293
294
 
294
295
  > [!WARNING]
@@ -329,7 +330,7 @@ Ne concerne que les drivers qui traversent les machines. Le canal de transport d
329
330
  `nodefony:realtime:<namespace>` (`resolveRedisChannel()`, `RedisBackplane.ts:31`).
330
331
 
331
332
  Le défaut est **dérivé du nom de l'application**
332
- (`src/packages/@nodefony/realtime/index.ts:132`) — ce qui suffit à séparer deux applications
333
+ (`src/packages/@nodefony/realtime/index.ts:139`) — ce qui suffit à séparer deux applications
333
334
  distinctes sur un Redis mutualisé, et **ne suffit pas** à séparer deux déploiements de la **même**
334
335
  application. Préproduction et production portent le même nom d'application, donc le même canal
335
336
  dérivé, donc le même flux.
@@ -377,13 +378,13 @@ jamais instancié, donc zéro minuteur, zéro message IPC — et l'endpoint de s
377
378
  de l'instance courante.
378
379
 
379
380
  Deux leviers coupent la sonde, et **l'un ou l'autre suffit** : cette clé, ou la variable
380
- `NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts:391`). C'est ce qui permet
381
+ `NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts:404`). C'est ce qui permet
381
382
  d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
382
383
 
383
384
  ### `slowConsumer.bytes` — un compteur, pas un frein
384
385
 
385
386
  Seuil de `bufferedAmount` — les octets en attente d'envoi sur une socket — au-delà duquel la sonde
386
- compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
387
+ compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:847`).
387
388
 
388
389
  > [!IMPORTANT]
389
390
  > **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
@@ -439,7 +440,7 @@ des canaux d'état, où seul le dernier instantané compte.
439
440
  Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
440
441
  côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
441
442
  processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
442
- `RealtimeController.ts:815`).
443
+ `RealtimeController.ts:869`).
443
444
 
444
445
  Quatre propriétés qui décident du bon réglage :
445
446
 
@@ -450,7 +451,7 @@ Quatre propriétés qui décident du bon réglage :
450
451
  - Un réabonnement à un canal **déjà tenu** ne consomme pas de place — l'idempotence est vérifiée
451
452
  avant le plafond. Un client qui redemande son canal n'est pas puni.
452
453
  - La garde existe **même sans configuration** : le hub porte le même défaut de 256
453
- (`RealtimeHub.ts:217`), il n'y a pas de fenêtre où elle serait absente.
454
+ (`RealtimeHub.ts:218`), il n'y a pas de fenêtre où elle serait absente.
454
455
 
455
456
  `null` retire la borne. C'est un retrait explicite, à réserver aux déploiements dont tu maîtrises
456
457
  les clients — un tableau de bord interne qui compose des dizaines de flux, par exemple.
@@ -462,7 +463,7 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
462
463
  ### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
463
464
 
464
465
  Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
465
- (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:986`). Une origine refusée ferme la socket avec le code
466
+ (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:967`). Une origine refusée ferme la socket avec le code
466
467
  `4003`.
467
468
 
468
469
  Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
@@ -527,7 +528,7 @@ use("@nodefony/realtime", { backplane: { driver: "cluster" } });
527
528
  **Ce qui se passe.** Un worker Node ne peut parler qu'au maître. Il lui envoie ses publications, et le
528
529
  maître les redistribue aux autres workers. Le driver ne s'active qu'en **rôle worker** et avec la
529
530
  variable `NF_CLUSTER=1`, posée par la commande de cluster
530
- (`src/packages/@nodefony/realtime/index.ts:99`) ; ailleurs, il rend `null` et le hub reste local.
531
+ (`src/packages/@nodefony/realtime/index.ts:106`) ; ailleurs, il rend `null` et le hub reste local.
531
532
 
532
533
  **Pourquoi c'est précieux.** C'est le banc d'essai qui stabilise l'architecture multi-processus avant
533
534
  d'ajouter du réseau. Ton contrôleur, tes canaux diffusables et ton client voient un vrai fan-out
@@ -552,7 +553,7 @@ use("@nodefony/realtime", {
552
553
 
553
554
  **Comment il se branche.** Le driver ne dépend pas de la bibliothèque `redis` : il consomme deux
554
555
  connexions du module `@nodefony/redis` par un adaptateur purement structurel
555
- (`createRedisServiceTransport()`, `RedisBackplane.ts:100`). Deux et non une, parce qu'un client Redis
556
+ (`createRedisServiceTransport()`, `RedisBackplane.ts:137`). Deux et non une, parce qu'un client Redis
556
557
  abonné ne peut plus émettre de commandes ordinaires — le module fournit précisément des connexions
557
558
  nommées `publish` et `subscribe` dans ses défauts.
558
559
 
@@ -562,7 +563,7 @@ coupure. C'est acceptable pour un salon de discussion — le message est en base
562
563
  un bus d'événements critiques. Ne construis pas au-dessus une fiabilité que le support n'offre pas.
563
564
 
564
565
  **Si Redis est injoignable.** Le démarrage du backplane est attendu **explicitement**, et **borné à
565
- cinq secondes** (`src/packages/@nodefony/realtime/index.ts:83`). Au-delà, ou en cas d'échec, le
566
+ cinq secondes** (`src/packages/@nodefony/realtime/index.ts:90`). Au-delà, ou en cas d'échec, le
566
567
  module renonce, ferme proprement ce qu'il avait ouvert, et laisse le hub local — le démarrage
567
568
  continue. Une base de messages en panne ne doit pas empêcher tes serveurs HTTP de monter.
568
569
 
@@ -650,7 +651,7 @@ présente court-circuite la sélection par nom.
650
651
  > Le builder accepte aussi une instance en second argument (`defineRealtimeConfig(config, {
651
652
  backplane })`), et la documentation d'architecture présente cette voie. En pratique elle n'est pas
652
653
  > atteignable depuis `nodefony.config.ts` : le module appelle le builder avec la seule configuration
653
- > fusionnée (`src/packages/@nodefony/realtime/index.ts:216`), et le schéma Zod est strict : une clé
654
+ > fusionnée (`src/packages/@nodefony/realtime/index.ts:230`), et le schéma Zod est strict : une clé
654
655
  > inconnue, dont une instance de classe posée dans `backplane`, fait échouer le boot. Pour brancher un objet déjà construit, utilise
655
656
  > le service `realtimeBackplane`.
656
657
 
@@ -665,7 +666,7 @@ processus se raccorde — et c'est pour cela qu'elles ne vivent pas dans un fich
665
666
 
666
667
  | Variable | Portée | Effet |
667
668
  | --------------------------------- | ---------------------- | -------------------------------------------------------------------------- |
668
- | `NF_REALTIME_DRIVER` | ce module | remplace `backplane.driver`. **Précédence maximale** |
669
+ | `NF_REALTIME_DRIVER` | ce module | remplace `backplane.driver` — nom libre, un pilote inconnu est signalé |
669
670
  | `NF_REALTIME_BACKPLANE_NAMESPACE` | ce module | remplace `backplane.namespace` — sépare deux déploiements de la même app |
670
671
  | `NF_REALTIME_BACKPLANE_SECRET` | ce module | remplace `backplane.secret` — scelle le transport partagé (≥ 32 car.) |
671
672
  | `NF__REALTIME__<CHEMIN>` | mécanisme du cœur | remplace n'importe quelle clé, par son chemin. Ex. `NF__REALTIME__ENABLED` |
@@ -677,13 +678,15 @@ L'ordre de recouvrement, du plus faible au plus fort :
677
678
 
678
679
  1. **Les valeurs d'usine du schéma** — ce que tu obtiens sans rien écrire.
679
680
  2. **La configuration de l'application** — ton `use("@nodefony/realtime", { … })`.
680
- 3. **`NF__REALTIME__<CHEMIN>`** — appliqué après la fusion et **avant** la validation Zod, donc la
681
- valeur venue de l'environnement est validée comme les autres. Un chemin introuvable produit un
682
- avertissement avec une suggestion, jamais une clé fantôme.
683
- 4. **`NF_REALTIME_DRIVER`**, **`NF_REALTIME_BACKPLANE_NAMESPACE`** et
684
- **`NF_REALTIME_BACKPLANE_SECRET`** — appliqués **après** l'analyse, dans le builder
685
- (`defineModuleConfig.ts:41`). Ils gagnent sur tout le reste, y compris sur le mécanisme
686
- générique `NF__REALTIME__BACKPLANE__…`.
681
+ 3. **`NF_REALTIME_DRIVER`**, **`NF_REALTIME_BACKPLANE_NAMESPACE`** et
682
+ **`NF_REALTIME_BACKPLANE_SECRET`** — posées par le Kernel après la fusion et **avant** la
683
+ validation Zod (`applyEnvShortcuts`, `Kernel.ts:1989`) : une valeur fausse **arrête le démarrage** en nommant la
684
+ variable (un secret de moins de 32 caractères, un espace de noms qui contient `:`). Chacune
685
+ accepte aussi sa forme `<NOM>_FILE` (secret monté par Docker ou Kubernetes).
686
+ 4. **`NF__REALTIME__<CHEMIN>`** — même moment, même validation. Un chemin introuvable produit un
687
+ avertissement avec une suggestion, jamais une clé fantôme. Viser le **même** champ qu'une des
688
+ trois variables ci-dessus est une ambiguïté : refusée en production, et en développement
689
+ `NF__…` l'emporte avec un avertissement qui nomme les deux.
687
690
 
688
691
  Le mécanisme générique mérite d'être connu : le double tiret bas sépare les niveaux, les segments
689
692
  sont insensibles à la casse, et les valeurs sont converties (booléens, nombres, listes séparées par
@@ -700,17 +703,19 @@ NF_CLUSTER_PROBE=0 # couper la sonde de pod sur une réplique en incident
700
703
 
701
704
  > [!TIP]
702
705
  > `NF_REALTIME_DRIVER` existe **en plus** du mécanisme générique parce qu'il est la variable qu'on
703
- > pose en premier sur un déploiement, et qu'elle doit rester fiable quel que soit le moment où la
704
- > configuration de l'application fusionne. C'est le levier d'urgence : ramener un cluster à
705
- > `loopback` pour isoler une panne de bus se fait par une variable, sans redéployer de code.
706
+ > pose en premier sur un déploiement : son nom ne dépend pas de la structure interne de la
707
+ > configuration. C'est le levier d'urgence : ramener un cluster à `loopback` pour isoler une panne
708
+ > de bus se fait par une variable, sans redéployer de code. `npx nodefony env` la liste avec sa
709
+ > valeur et sa provenance.
706
710
 
707
711
  ## 🧰 Le builder et le schéma exportable
708
712
 
709
713
  Deux fonctions publiques, utiles surtout aux tests et aux outils.
710
714
 
711
- `defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:35`) analyse, applique la variable
712
- de driver et **gèle**. Le module l'appelle lui-même à l'enregistrement
713
- (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:213`) : tu n'as pas à
715
+ `defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:39`) analyse et **gèle**. Appelé
716
+ hors Kernel (un test, un outil), il ne voit pas les variables d'environnement : c'est le Kernel
717
+ qui les pose. Le module l'appelle lui-même à l'enregistrement
718
+ (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:221`) : tu n'as pas à
714
719
  l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
715
720
  sans démarrer un serveur.
716
721
 
@@ -722,7 +727,7 @@ const cfg = defineRealtimeConfig({ backplane: { driver: "redis" } });
722
727
  // Une valeur invalide ou une clé inconnue lève une BootConfigurationError, avec le chemin exact du champ fautif.
723
728
  ```
724
729
 
725
- `realtimeConfigJsonSchema()` (`defineModuleConfig.ts:69`) produit le schéma JSON du module, chaque
730
+ `realtimeConfigJsonSchema()` (`defineModuleConfig.ts:99`) produit le schéma JSON du module, chaque
726
731
  champ portant sa description. C'est ce que consomme la page module de Studio pour afficher la
727
732
  configuration attendue. L'instance de backplane éventuelle en est absente : une classe n'a rien à
728
733
  faire dans un schéma sérialisable.
@@ -762,7 +767,7 @@ Trois endroits, par ordre de fiabilité décroissante.
762
767
  | Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde | régler `websocketSecure.maxBackpressure` / `.backpressureCloseAfterDrops` (@nodefony/http) |
763
768
  | Toutes les connexions tombent en `4003` après activation du contrôle d'origine | `allowList` vide, ou origine non identique au caractère près (port, schéma, sous-domaine) | inscrire l'origine exacte ; un client non-navigateur relève de `allowMissingOrigin` |
764
769
  | `enabled: false` posé « pour désactiver », et la garde d'origine ne s'applique plus | le service devient inerte et ne pose plus les politiques sur le hub | retirer le module du manifeste plutôt que l'éteindre |
765
- | Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts:217`) | vérifier le motif de refus `realtime:denied` côté client |
770
+ | Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts:218`) | vérifier le motif de refus `realtime:denied` côté client |
766
771
 
767
772
  ## 🧪 Tests & couverture
768
773
 
@@ -353,7 +353,7 @@ gaspillerait une socket par écran.
353
353
 
354
354
  ```ts
355
355
  // frontend/src/realtime.ts
356
- import { RealtimeClient } from "nodefony/client";
356
+ import { NodefonySocket } from "nodefony/client";
357
357
 
358
358
  // `wss` si la page est en HTTPS, `ws` sinon. Une application Nodefony sert en
359
359
  // HTTPS par défaut, y compris en développement : ce sera donc `wss` chez toi.
@@ -366,7 +366,7 @@ const scheme = window.location.protocol === "https:" ? "wss" : "ws";
366
366
  * La reconnexion automatique, le renvoi des abonnements après une coupure et le
367
367
  * heartbeat sont inclus — rien à écrire pour ça.
368
368
  */
369
- export const chatSocket = new RealtimeClient({
369
+ export const chatSocket = new NodefonySocket({
370
370
  url: `${scheme}://${window.location.host}/chat/realtime`,
371
371
  });
372
372
  ```
@@ -445,7 +445,9 @@ import { ChatRoom } from "./ChatRoom";
445
445
 
446
446
  // La connexion s'ouvre UNE fois, au démarrage de l'application. Les hooks ne
447
447
  // gèrent que les abonnements : le cycle de vie de la socket reste à toi.
448
- void chatSocket.connect();
448
+ // `connect()` rejette si cette première tentative échoue (serveur absent) ;
449
+ // la socket se reconnecte seule ensuite, et l'état se lit par les hooks.
450
+ chatSocket.connect().catch(() => {});
449
451
 
450
452
  createRoot(document.getElementById("app")!).render(
451
453
  <NodefonyProvider client={chatSocket}>
@@ -847,6 +849,7 @@ fonctionnent :
847
849
  // modules/chat/nodefony/controllers/ChatController.ts
848
850
  // → remplace `@RealtimeInbound("chat:send")` par une ACTION
849
851
  import { Body, CurrentUser, IsGranted } from "@nodefony/framework";
852
+ import type { IUser } from "@nodefony/user";
850
853
 
851
854
  /**
852
855
  * L'envoi devient une action, joignable en POST HTTP comme par la socket.
@@ -860,7 +863,7 @@ import { Body, CurrentUser, IsGranted } from "@nodefony/framework";
860
863
  @IsGranted(["ROLE_USER"])
861
864
  async send(
862
865
  @Body() payload: { text?: string },
863
- @CurrentUser() user?: { identifier?: string },
866
+ @CurrentUser() user: IUser,
864
867
  ) {
865
868
  const text = typeof payload?.text === "string" ? payload.text.trim() : "";
866
869
  if (text.length === 0 || text.length > 2000) {
@@ -869,7 +872,7 @@ async send(
869
872
 
870
873
  // L'auteur vient du SERVEUR. Le client ne peut plus mentir dessus.
871
874
  const message: ChatMessage = {
872
- author: user?.identifier ?? "inconnu",
875
+ author: user.identifier,
873
876
  text,
874
877
  sentAt: Date.now(),
875
878
  };
@@ -1218,6 +1221,7 @@ import {
1218
1221
  import type { RealtimePublish } from "@nodefony/realtime";
1219
1222
  import { HttpError } from "@nodefony/http";
1220
1223
  import type { ContextType, IUploadedFile } from "@nodefony/http";
1224
+ import type { IUser } from "@nodefony/user";
1221
1225
 
1222
1226
  /** Un message tel qu'il circule sur le canal du salon. */
1223
1227
  export interface ChatMessage {
@@ -1288,14 +1292,14 @@ class ChatController extends RealtimeController {
1288
1292
  @IsGranted(["ROLE_USER"])
1289
1293
  async send(
1290
1294
  @Body() payload: { text?: string },
1291
- @CurrentUser() user?: { identifier?: string },
1295
+ @CurrentUser() user: IUser,
1292
1296
  ): Promise<{ ok: true; sentAt: number }> {
1293
1297
  const text = typeof payload?.text === "string" ? payload.text.trim() : "";
1294
1298
  if (text.length === 0 || text.length > 2000) {
1295
1299
  throw new HttpError("message vide ou trop long", 400);
1296
1300
  }
1297
1301
  const message: ChatMessage = {
1298
- author: user?.identifier ?? "inconnu",
1302
+ author: user.identifier,
1299
1303
  text,
1300
1304
  sentAt: Date.now(),
1301
1305
  };
@@ -1312,13 +1316,13 @@ class ChatController extends RealtimeController {
1312
1316
  @IsGranted(["ROLE_USER"])
1313
1317
  async upload(
1314
1318
  @UploadedFile() file?: IUploadedFile,
1315
- @CurrentUser() user?: { identifier?: string },
1319
+ @CurrentUser() user: IUser,
1316
1320
  ): Promise<{ ok: true; filename: string; size: number }> {
1317
1321
  if (!file) {
1318
1322
  throw new HttpError("aucun fichier reçu", 400);
1319
1323
  }
1320
1324
  const announcement: ChatAttachment = {
1321
- author: user?.identifier ?? "inconnu",
1325
+ author: user.identifier,
1322
1326
  filename: file.filename,
1323
1327
  size: file.size,
1324
1328
  mimeType: file.mimeType,
@@ -1599,23 +1603,23 @@ Pour qui veut lire l'implémentation derrière chaque étape :
1599
1603
  <!-- prettier-ignore -->
1600
1604
  | Ce que tu as utilisé | Où c'est écrit |
1601
1605
  | --- | --- |
1602
- | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:192`), `RealtimeInbound()` (`realtimeDecorators.ts:231`) |
1603
- | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:299`) |
1604
- | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:276`) |
1605
- | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:462`), `RealtimeController.startChannel()` (`RealtimeController.ts:815`) |
1606
- | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:623`) |
1607
- | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:668`) |
1608
- | Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
1609
- | Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1143`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1117`) |
1610
- | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:279`) |
1611
- | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:417`), fermetures `origin not allowed` (`RealtimeController.ts:327`) et `unauthorized` (`RealtimeController.ts:397`) |
1612
- | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:826`) |
1613
- | Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:56`), fermeture `slow consumer` (`RealtimeHub.ts:63`) |
1614
- | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:1000`) |
1606
+ | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:191`), `RealtimeChannel()` (`realtimeDecorators.ts:240`), `RealtimeInbound()` (`realtimeDecorators.ts:278`) |
1607
+ | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:344`) |
1608
+ | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:321`) |
1609
+ | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:464`), `RealtimeController.startChannel()` (`RealtimeController.ts:869`) |
1610
+ | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:610`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:629`) |
1611
+ | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:420`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:674`) |
1612
+ | Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:720`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
1613
+ | Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1124`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1098`) |
1614
+ | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:306`) |
1615
+ | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:449`), fermetures `origin not allowed` (`RealtimeController.ts:422`) et `unauthorized` (`RealtimeController.ts:444`) |
1616
+ | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:815`) |
1617
+ | Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:66`), fermeture `slow consumer` (`RealtimeHub.ts:64`) |
1618
+ | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:981`) |
1615
1619
  | L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
1616
- | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:865`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1620
+ | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:847`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1617
1621
  | Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
1618
- | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:213`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
1619
- | Les hooks React | `useNodefony()` (`client/react/index.ts:145`), `useNodefonyState()` (`client/react/index.ts:163`), `useNodefonyIdentity()` (`client/react/index.ts:182`), `useNodefonyChannel()` (`client/react/index.ts:198`), `useNodefonyChannelData()` (`client/react/index.ts:221`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts:255`) |
1620
- | Le client navigateur | `RealtimeClient.connect()` (`RealtimeClient.ts:389`), `RealtimeClient.subscribe()` (`RealtimeClient.ts:530`), `RealtimeClient.request()` (`RealtimeClient.ts:729`), `RealtimeClient.onDenied()` (`RealtimeClient.ts:471`) |
1621
- | L'upload | `UploadedFile` (`routerDecorators.ts:1286`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:226`) |
1622
+ | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:218`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
1623
+ | Les hooks React | `useNodefony()` (`client/react/index.ts:255`), `useNodefonyState()` (`client/react/index.ts:273`), `useNodefonyIdentity()` (`client/react/index.ts:292`), `useNodefonyChannel()` (`client/react/index.ts:308`), `useNodefonyChannelData()` (`client/react/index.ts:332`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts:367`) |
1624
+ | Le client navigateur | `NodefonySocket.connect()` (`NodefonySocket.ts:592`), `NodefonySocket.subscribe()` (`NodefonySocket.ts:739`), `NodefonySocket.request()` (`NodefonySocket.ts:936`), `NodefonySocket.onDenied()` (`NodefonySocket.ts:678`) |
1625
+ | L'upload | `UploadedFile` (`routerDecorators.ts:1311`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:215`) |
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` —
@@ -326,7 +340,7 @@ vue de tous les workers d'un pod.
326
340
 
327
341
  Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
328
342
  diffusion, connexions, octets et frames, pression d'écriture. C'est la même donnée que celle rendue à
329
- 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`).
330
344
 
331
345
  ## 🧪 Tests & couverture
332
346
 
@@ -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
@@ -462,11 +464,11 @@ instantané est exact — et ne dit rien des trois autres. Pire : deux appels co
462
464
  sur deux workers et sembler se contredire.
463
465
 
464
466
  **La réponse de Nodefony est un modèle _push_.** Chaque worker remonte périodiquement sa santé au
465
- 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
466
468
  rediffuse ; chaque worker met le résultat en cache. **N'importe lequel** sert alors la vue pod en temps
467
- constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:291`).
469
+ constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:306`).
468
470
 
469
- 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
470
472
  mérite d'être connue :
471
473
 
472
474
  | Grandeur | Agrégation | Pourquoi |
@@ -483,7 +485,7 @@ garde le détail par worker pour le forage.
483
485
  ### On paie ce qu'on regarde
484
486
 
485
487
  Les sondes coûteuses ne tournent pas en permanence. Le maître peut demander à un worker précis
486
- d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts:239`) — quand
488
+ d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts:259`) — quand
487
489
  un humain ouvre le forage de ce `pid`. La sonde riche (espaces mémoire, cycles de ramasse-miettes,
488
490
  descripteurs actifs) est alors allouée ; à la fermeture de l'écran, elle est libérée et son observateur
489
491
  détaché.
@@ -504,7 +506,7 @@ l'orchestrateur finit par le tuer de force à l'expiration du délai de grâce.
504
506
 
505
507
  La règle est tenue partout dans le code, y compris là où on ne l'attendrait pas — le tick de
506
508
  revalidation d'identité du hub, qui ne démarre qu'au premier inscrit, est lui aussi détaché
507
- (`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
508
510
  minuteur générique des canaux (`providers.ts:465`).
509
511
 
510
512
  ```ts ignore