@nodefony/realtime 10.0.0-alpha.9 → 10.0.0-beta.2

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.
@@ -126,7 +126,7 @@ Nodefony étiquette avec **JSON-RPC 2.0**, une norme publique plutôt qu'un form
126
126
  Le point remarquable : le **même** moteur de protocole tourne des deux côtés du fil. La
127
127
  classe `JsonRpcPeer` (`JsonRpcPeer.ts:271`) est du code isomorphe du cœur — le navigateur
128
128
  l'exécute dans `RealtimeClient`, le serveur l'instancie une fois par connexion dans
129
- `RealtimeController.onHandshake()` (`RealtimeController.ts:328`). Le serveur peut donc
129
+ `RealtimeController.onHandshake()` (`RealtimeController.ts:374`). Le serveur peut donc
130
130
  appeler le client, pas seulement l'inverse : c'est du vrai duplex, pas un aller-retour
131
131
  déguisé.
132
132
 
@@ -141,7 +141,7 @@ Trois partis pris distinguent cette pile d'un simple « serveur WebSocket ».
141
141
  et explicite : `ServerRealtimeSocket.request()` (`ServerRealtimeSocket.ts:131`) **rejette
142
142
  toujours** — un handle posé sur le hub n'a pas d'interlocuteur unique, puisque le hub est
143
143
  multi-clients. Pour un appel serveur→client ciblé, on passe par la connexion :
144
- `RealtimeController.requestClient()` (`RealtimeController.ts:279`).
144
+ `RealtimeController.requestClient()` (`RealtimeController.ts:329`).
145
145
 
146
146
  **Un provider par canal, pas un par client.** Si mille onglets s'abonnent au même canal de
147
147
  santé, le calcul ne doit tourner qu'une fois. Le hub crée le producteur au **premier**
@@ -325,7 +325,7 @@ La pile se lit de haut en bas ; chaque étage ne connaît que son voisin du dess
325
325
  | Protocole | `JsonRpcPeer` (`JsonRpcPeer.ts:271`) | étiqueter, corréler, refuser une frame | par où passent les octets |
326
326
  | Transport | `WsConnectionTransport` (`WsConnectionTransport.ts:46`) | déplacer des octets, mesurer la file | ce que veut dire un message |
327
327
  | Hub | `RealtimeHub` (`RealtimeHub.ts:213`) | table des abonnés locaux + fan-out | qu'il existe d'autres process |
328
- | Backplane | `IBackplane` (`IBackplane.ts:75`) | porter un message aux autres process | ce qu'est un abonné |
328
+ | Backplane | `IBackplane` (`IBackplane.ts:107`) | porter un message aux autres process | ce qu'est un abonné |
329
329
 
330
330
  > [!NOTE]
331
331
  > Une part de la documentation historique décrit cette pile en « 5 étages » en comptant
@@ -388,7 +388,7 @@ Le schéma ci-dessous rend ces étages vivants : active le temps réel et il res
388
388
 
389
389
  ## 🔌 Le cycle de vie d'une connexion
390
390
 
391
- Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:328`), appelé
391
+ Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:374`), appelé
392
392
  une seule fois par connexion, en chemin froid.
393
393
 
394
394
  ```mermaid
@@ -419,19 +419,19 @@ sequenceDiagram
419
419
 
420
420
  Les étapes, dans l'ordre exact du code :
421
421
 
422
- 1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:970`). Refus →
422
+ 1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:986`). Refus →
423
423
  fermeture `4003`. La politique vient de la configuration ; sans politique, tout passe.
424
424
  2. **Résolution de l'identité** — `RealtimeHub.resolveAuthenticator()`
425
- (`RealtimeHub.ts:945`) parcourt les authentificateurs enregistrés : **le premier motif
425
+ (`RealtimeHub.ts:961`) parcourt les authentificateurs enregistrés : **le premier motif
426
426
  qui correspond capture**. Aucun ne correspond ? Le jeton anonyme gelé est posé
427
427
  (`ANONYMOUS_REALTIME_TOKEN`) — la lecture d'identité ne rend donc jamais `null`. Un
428
428
  échec d'authentification ferme en `4001`.
429
429
  3. **Création du peer et du transport**, puis association `peer → jeton`
430
- (`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:1002`), stockée dans une `WeakMap` : le
430
+ (`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:1018`), stockée dans une `WeakMap` : le
431
431
  jeton disparaît avec le peer, sans fuite.
432
432
  4. **Enregistrement des actions** — celles des décorateurs `@RealtimeAction`, puis celles
433
433
  de la surcharge `realtimeActions()`, qui gagne en cas de conflit. Le pont API
434
- `api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts:219`)
434
+ `api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts:276`)
435
435
  rend `true`.
436
436
  5. **Inscription aux registres** : sonde de connexion, révocation périodique si le jeton
437
437
  est révocable, préfixes de canaux broadcast, canaux entrants.
@@ -439,7 +439,7 @@ Les étapes, dans l'ordre exact du code :
439
439
  l'identité résolue (type, authentifié ou non, rôles, portées). Le client sait **qui il
440
440
  est** sans appeler la moindre route.
441
441
 
442
- À la fermeture, un unique `onFinish` (`RealtimeController.ts:578`) fait le ménage complet :
442
+ À la fermeture, un unique `onFinish` (`RealtimeController.ts:715`) fait le ménage complet :
443
443
  désabonnement de chaque canal tenu, retrait des deux registres, `fireClose()` du transport,
444
444
  `dispose()` du peer. C'est ce qui garantit qu'aucun minuteur ni écouteur ne survit à une
445
445
  déconnexion.
@@ -459,13 +459,13 @@ déconnexion.
459
459
  La seconde mérite une explication. Le verrou de frame est synchrone et lit une identité
460
460
  figée au handshake : il ne peut donc pas voir une session qui meurt en cours de route (une
461
461
  déconnexion HTTP, par exemple). Un minuteur — démarré au premier inscrit, arrêté dès que le
462
- registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:778`) — relit
462
+ registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:794`) — relit
463
463
  périodiquement ces identités et coupe les sockets orphelines. Seules les identités
464
464
  **révocables** y entrent : un visiteur anonyme ne coûte rien.
465
465
 
466
466
  ## Le hub — canaux partagés et fan-out
467
467
 
468
- Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1435`). Il ne
468
+ Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1451`). Il ne
469
469
  dépend de rien : ce sont les fabriques fournies par les contrôleurs qui portent les
470
470
  dépendances.
471
471
 
@@ -478,7 +478,7 @@ dépendances.
478
478
  paquet du producteur atteigne bien ce premier abonné.
479
479
  3. La fabrique du contrôleur rend `null` (canal inconnu de lui) ? Dernier recours : le
480
480
  registre des **canaux système** (`RealtimeHub.registerSystemChannel()`,
481
- `RealtimeHub.ts:1281`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
481
+ `RealtimeHub.ts:1297`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
482
482
  l'abonnement est refusé et rien n'est alloué.
483
483
 
484
484
  Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts:576`) appelle le
@@ -547,12 +547,12 @@ coûte chaque abonné supplémentaire.
547
547
 
548
548
  ### Le contrat, en cinq méthodes
549
549
 
550
- `IBackplane` (`IBackplane.ts:75`) est délibérément minuscule : le backplane ne connaît ni
550
+ `IBackplane` (`IBackplane.ts:107`) est délibérément minuscule : le backplane ne connaît ni
551
551
  les abonnés ni les canaux logiques. Tout l'état vit dans le hub.
552
552
 
553
553
  | Membre | Rôle |
554
554
  | ------------------------------------------------- | --------------------------------------------------------------- |
555
- | `originId` (`IBackplane.ts:77`) | l'étiquette de CE process, lue par l'anti-écho |
555
+ | `originId` (`IBackplane.ts:109`) | l'étiquette de CE process, lue par l'anti-écho |
556
556
  | `start()` (`IBackplane.ts:115`) | ouvrir le transport. Idempotent, synchrone ou asynchrone |
557
557
  | `publish(channel, payload)` (`IBackplane.ts:121`) | propager aux **autres** pairs. Ne refait pas le fan-out local |
558
558
  | `onMessage(handler)` (`IBackplane.ts:127`) | recevoir des pairs. Un seul gestionnaire à la fois |
@@ -718,8 +718,8 @@ code qui **appelle le builder lui-même** (un `Module` sur mesure, une intégrat
718
718
  programmatique). Une application qui configure via `use("@nodefony/realtime", …)` **ne
719
719
  l'atteint pas** : elle ne construit pas la config (le module appelle
720
720
  `defineRealtimeConfig(this.options)` sans second argument, `index.ts:216`), et une
721
- instance posée dans l'objet de config est retirée par la validation Zod (elle n'est pas
722
- au schéma). Pour une app, la voie « instance » est l'option 2 ci-dessus.
721
+ instance posée dans l'objet de config est refusée par la validation Zod (clé inconnue du
722
+ schéma strict). Pour une app, la voie « instance » est l'option 2 ci-dessus.
723
723
 
724
724
  Le registre s'introspecte : `listBackplaneDrivers()` (`backplaneRegistry.ts:68`) rend les
725
725
  noms disponibles — c'est ce que le message d'erreur affiche quand un driver déclaré est
@@ -743,19 +743,19 @@ Deux propriétés architecturales méritent d'être notées ici, parce qu'elles
743
743
  choix de conception visibles partout dans le module :
744
744
 
745
745
  **Coût nul quand la sécurité est absente.** Le verrou de frame n'est branché sur le peer que
746
- si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:1042`, testé une
746
+ si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:1058`, testé une
747
747
  fois au handshake). Sans module de sécurité, `beforeDispatch` reste indéfini et le chemin
748
748
  chaud ne paie **rien** du tout.
749
749
 
750
750
  **Échec bruyant plutôt que faux sentiment de sécurité.** Si des canaux déclarent une
751
751
  politique sans qu'aucun décideur ne soit câblé, `hasUnenforcedChannelPolicies()`
752
- (`RealtimeHub.ts:1101`) le détecte et un avertissement est émis une fois par process
752
+ (`RealtimeHub.ts:1117`) le détecte et un avertissement est émis une fois par process
753
753
  (`RealtimeController.ts:487`). Un canal qui **se croit** gardé alors qu'il est ouvert est
754
754
  bien plus dangereux qu'un canal ouvertement public.
755
755
 
756
756
  ## 📡 Observabilité — la sonde et Studio
757
757
 
758
- `RealtimeHub.probe()` (`RealtimeHub.ts:849`) rend un instantané en **lecture pure** : aucune
758
+ `RealtimeHub.probe()` (`RealtimeHub.ts:865`) rend un instantané en **lecture pure** : aucune
759
759
  allocation sur le chemin chaud, jamais d'exception. Les compteurs sont des primitives
760
760
  incrémentées en O(1) ; ils sont **monotones**, ce qui laisse au lecteur le soin de dériver
761
761
  un débit.
@@ -808,7 +808,7 @@ révocables, authentificateurs, politiques de canal, association peer→jeton :
808
808
  `null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts:141` et suivants).
809
809
 
810
810
  **Aucun minuteur au repos.** Le tick de révocation démarre au premier inscrit et s'arrête
811
- dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:793`).
811
+ dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:809`).
812
812
  Les minuteurs sont détachés de la boucle d'événements : ils ne retiennent jamais l'arrêt du
813
813
  process.
814
814
 
@@ -824,25 +824,25 @@ désabonné.
824
824
 
825
825
  **Le vrai point de vigilance est la back-pressure**, pas le processeur : une file d'envoi
826
826
  non bornée multipliée par le nombre de clients, c'est la panne mémoire — et le multiplexage
827
- concentre le risque. D'où les deux seuils du transport, le comptage des consommateurs lents
827
+ concentre le risque. D'où les réglages de contre-pression du transport, le comptage des consommateurs lents
828
828
  dans la sonde, et le plafond de canaux par connexion.
829
829
 
830
830
  ## ⚠️ Pièges
831
831
 
832
- | Symptôme | Cause | Correction |
833
- | -------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
834
- | Le chat marche en local, plus rien en cluster | le canal n'est pas déclaré broadcast (défaut : instance-local) | déclarer le préfixe avec `@RealtimeBroadcast` |
835
- | Le client ne reçoit rien alors qu'il a un handler `on(...)` | `on` reçoit, `subscribe` demande — il faut les **deux** | appeler `socket.subscribe(canal)` |
836
- | Chaque message arrive **en double** en cluster | `publishLocal` court-circuité, ou `originId` non unique entre pods | ne jamais republier une arrivée backplane ; vérifier `NF_POD_NAME`/hostname |
837
- | Deux applications se parlent sur un Redis mutualisé | même canal dérivé (le numéro de base Redis ne cloisonne pas le pub/sub) | poser un `backplane.namespace` explicite et distinct |
838
- | Producteur planté après le départ du premier abonné | la fabrique a capturé le contexte de la connexion créatrice | ne capturer que des valeurs à longue durée de vie |
839
- | Abonnement refusé avec `realtime:denied` motif `limit` | plafond de canaux par connexion atteint (256 par défaut) | regrouper les canaux, ou relever le plafond en connaissance de cause |
840
- | Fermeture `1013` sur un client lent | file d'envoi ≥ 8 MiB, jugée irrécupérable | attendu ; le client se reconnecte et se resynchronise |
841
- | Fermeture `4001` en cours de session | identité révoquée, détectée par le tick de re-validation | se réauthentifier ; le comportement est voulu |
842
- | Frames envoyées juste après `connect()` perdues | le transport n'est pas branché tant que le handshake n'est pas fini | attendre `realtime:welcome` (`RealtimeClient` le fait déjà) |
843
- | Avertissement « channel policies … NOT enforced » au démarrage | des canaux déclarent une politique sans décideur câblé | charger `@nodefony/security` avec une zone realtime |
844
- | `ServerRealtimeSocket.request()` rejette systématiquement | un handle posé sur le hub n'a pas d'interlocuteur unique | utiliser `RealtimeController.requestClient()` pour un appel ciblé |
845
- | `TS2550: RegExp.escape does not exist` à la compilation | `lib` du tsconfig sous ES2025 (les paquets exposent leurs types en source) | `"lib": ["ESNext", …]`, comme le tsconfig généré par `nodefony create app` |
832
+ | Symptôme | Cause | Correction |
833
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
834
+ | Le chat marche en local, plus rien en cluster | le canal n'est pas déclaré broadcast (défaut : instance-local) | déclarer le préfixe avec `@RealtimeBroadcast` |
835
+ | Le client ne reçoit rien alors qu'il a un handler `on(...)` | `on` reçoit, `subscribe` demande — il faut les **deux** | appeler `socket.subscribe(canal)` |
836
+ | Chaque message arrive **en double** en cluster | `publishLocal` court-circuité, ou `originId` non unique entre pods | ne jamais republier une arrivée backplane ; vérifier `NF_POD_NAME`/hostname |
837
+ | Deux applications se parlent sur un Redis mutualisé | même canal dérivé (le numéro de base Redis ne cloisonne pas le pub/sub) | poser un `backplane.namespace` explicite et distinct |
838
+ | Producteur planté après le départ du premier abonné | la fabrique a capturé le contexte de la connexion créatrice | ne capturer que des valeurs à longue durée de vie |
839
+ | Abonnement refusé avec `realtime:denied` motif `limit` | plafond de canaux par connexion atteint (256 par défaut) | regrouper les canaux, ou relever le plafond en connaissance de cause |
840
+ | Fermeture `1013` sur un client lent | file au-delà de `websocket.maxBackpressure` (4 MiB) pendant 1000 frames jetées d'affilée | attendu ; le client se reconnecte et se resynchronise |
841
+ | Fermeture `4001` en cours de session | identité révoquée, détectée par le tick de re-validation | se réauthentifier ; le comportement est voulu |
842
+ | Frames envoyées juste après `connect()` perdues | le transport n'est pas branché tant que le handshake n'est pas fini | attendre `realtime:welcome` (`RealtimeClient` le fait déjà) |
843
+ | Avertissement « channel policies … NOT enforced » au démarrage | des canaux déclarent une politique sans décideur câblé | charger `@nodefony/security` avec une zone realtime |
844
+ | `ServerRealtimeSocket.request()` rejette systématiquement | un handle posé sur le hub n'a pas d'interlocuteur unique | utiliser `RealtimeController.requestClient()` pour un appel ciblé |
845
+ | `TS2550: RegExp.escape does not exist` à la compilation | `lib` du tsconfig sous ES2025 (les paquets exposent leurs types en source) | `"lib": ["ESNext", …]`, comme le tsconfig généré par `nodefony create app` |
846
846
 
847
847
  ## 🧪 Tests & couverture
848
848
 
@@ -29,7 +29,7 @@ source: "src/packages/@nodefony/realtime/docs/configuration.md"
29
29
 
30
30
  # Configuration — le fond de panier, les bornes, la porte d'entrée
31
31
 
32
- > Le module temps réel expose **six réglages**, pas un de plus. Un seul décide vraiment de quelque
32
+ > Le module temps réel expose **sept blocs de réglages**, pas un de plus. Un seul décide vraiment de quelque
33
33
  > chose : le **driver de backplane**, c'est-à-dire la façon dont deux processus de ton application se
34
34
  > transmettent une publication. Les autres bornent la connexion et gardent la porte. Cette page les
35
35
  > donne tous, avec leur valeur d'usine lue dans le schéma, l'effet observable de chacun, et les trois
@@ -140,10 +140,9 @@ Le module **augmente le registre de types** (`index.ts:167`) : `use("@nodefony/r
140
140
  propose ses clés en autocomplétion et **refuse** une clé inconnue à la compilation.
141
141
 
142
142
  > [!IMPORTANT]
143
- > C'est ce filet qui compte, bien plus que le confort d'écriture. Sans lui, une faute de frappe ne se
144
- > verrait ni à l'écriture ni au démarrage : le schéma **retire** les clés inconnues sans se plaindre.
145
- > `slowConsummer: { bytes: 4096 }` donnerait le défaut, en silence. Avec l'augmentation, la même
146
- > faute ne compile pas.
143
+ > Ce filet attrape la faute dès l'écriture. Le schéma, lui, est strict : une clé inconnue arrête le
144
+ > démarrage (`BootConfigurationError`, avec le chemin complet de la clé). `slowConsummer: { bytes:
145
+ 4096 }` est donc refusé au boot ; avec l'augmentation, la même faute ne compile même pas.
147
146
 
148
147
  ## 🚀 Démarrage rapide
149
148
 
@@ -257,7 +256,7 @@ regarder quand un message ne traverse pas.
257
256
 
258
257
  ## ⚙️ Le schéma, clé par clé
259
258
 
260
- Six blocs, douze clés au total. Le tableau donne la vérité complète ; les sections qui suivent
259
+ Sept blocs, seize clés au total. Le tableau donne la vérité complète ; les sections qui suivent
261
260
  expliquent quand et pourquoi en changer.
262
261
 
263
262
  <!-- prettier-ignore -->
@@ -274,16 +273,22 @@ expliquent quand et pourquoi en changer.
274
273
  | `csrf.checkOrigin.enabled` | `boolean` | `false` | active le contrôle d'`Origin` à l'ouverture de la socket |
275
274
  | `csrf.checkOrigin.allowList` | `string[]` | `[]` | origines acceptées, **comparaison exacte**. Vide + activé = tout est refusé |
276
275
  | `csrf.checkOrigin.allowMissingOrigin` | `boolean` | `false` | accepter une ouverture sans en-tête `Origin` (clients non-navigateur) |
276
+ | `clientLogs.enabled` | `boolean` | `false` | accepte les journaux du navigateur sur le canal montant `nodefony:syslog:uplink`. Surface d'écriture, fermée par défaut ; une connexion anonyme ne pousse rien |
277
+ | `clientLogs.maxEntriesPerBatch` | `number` | `50` | entrées acceptées par lot |
278
+ | `clientLogs.maxEntriesPerWindow` | `number` | `300` | entrées acceptées par fenêtre de débit |
279
+ | `clientLogs.windowMs` | `number` | `10000` | durée de la fenêtre de débit, en ms |
280
+ | `clientLogs.maxStringLength` | `number` | `4096` | longueur maximale d'une chaîne reçue |
277
281
 
278
282
  C'est **tout**. Il n'existe ni réglage de ping, ni de cadence, ni de fréquence d'échantillonnage de
279
- la sonde, ni de seuil de contre-pression configurable : ces comportements existent, mais leurs
280
- valeurs sont des constantes du code, pas des clés.
283
+ la sonde : ces comportements existent, mais leurs valeurs sont des constantes du code. Les seuils de
284
+ contre-pression, eux, se règlent sur le serveur WebSocket de `@nodefony/http`
285
+ (`websocket.maxBackpressure`, `.backpressurePolicy`, `.backpressureCloseAfterDrops`).
281
286
 
282
287
  ### `enabled` — le module au repos
283
288
 
284
289
  Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
285
290
  enregistrée, pas de backplane câblé, pas de sonde de pod
286
- (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:213`), et le service devient
291
+ (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:239`), et le service devient
287
292
  inerte (`RealtimeService.init()`, `RealtimeService.ts:77`).
288
293
 
289
294
  > [!WARNING]
@@ -344,7 +349,7 @@ canal exotique.
344
349
  Ne concerne, là aussi, que les transports **partagés**. Un bus pub/sub ne dit pas _qui_ a publié :
345
350
  sans secret, tout ce qui atterrit sur le canal — venu d'un pod ou d'un tiers qui sait écrire dans ce
346
351
  Redis — est traité comme un message légitime. Poser un secret fait porter à chaque message un sceau
347
- HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts:96`) : un message non
352
+ HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts:112`) : un message non
348
353
  scellé, altéré ou repointé vers un autre canal est **ignoré**, sans exception possible.
349
354
 
350
355
  Le secret doit être **identique sur tous les pods** — c'est lui qui les reconnaît entre eux. En
@@ -378,7 +383,7 @@ d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
378
383
  ### `slowConsumer.bytes` — un compteur, pas un frein
379
384
 
380
385
  Seuil de `bufferedAmount` — les octets en attente d'envoi sur une socket — au-delà duquel la sonde
381
- compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:849`).
386
+ compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
382
387
 
383
388
  > [!IMPORTANT]
384
389
  > **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
@@ -434,7 +439,7 @@ des canaux d'état, où seul le dernier instantané compte.
434
439
  Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
435
440
  côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
436
441
  processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
437
- `RealtimeController.ts:746`).
442
+ `RealtimeController.ts:815`).
438
443
 
439
444
  Quatre propriétés qui décident du bon réglage :
440
445
 
@@ -457,7 +462,7 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
457
462
  ### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
458
463
 
459
464
  Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
460
- (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:970`). Une origine refusée ferme la socket avec le code
465
+ (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:986`). Une origine refusée ferme la socket avec le code
461
466
  `4003`.
462
467
 
463
468
  Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
@@ -645,8 +650,8 @@ présente court-circuite la sélection par nom.
645
650
  > Le builder accepte aussi une instance en second argument (`defineRealtimeConfig(config, {
646
651
  backplane })`), et la documentation d'architecture présente cette voie. En pratique elle n'est pas
647
652
  > atteignable depuis `nodefony.config.ts` : le module appelle le builder avec la seule configuration
648
- > fusionnée (`src/packages/@nodefony/realtime/index.ts:216`), et le schéma Zod **retire** toute clé
649
- > qu'il ne connaît pas — dont une instance de classe. Pour brancher un objet déjà construit, utilise
653
+ > fusionnée (`src/packages/@nodefony/realtime/index.ts:216`), et le schéma Zod est strict : une clé
654
+ > inconnue, dont une instance de classe posée dans `backplane`, fait échouer le boot. Pour brancher un objet déjà construit, utilise
650
655
  > le service `realtimeBackplane`.
651
656
 
652
657
  Le détail du contrat, de l'anti-écho et du cycle de vie d'un driver est dans
@@ -705,7 +710,7 @@ Deux fonctions publiques, utiles surtout aux tests et aux outils.
705
710
 
706
711
  `defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:35`) analyse, applique la variable
707
712
  de driver et **gèle**. Le module l'appelle lui-même à l'enregistrement
708
- (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:219`) : tu n'as pas à
713
+ (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:213`) : tu n'as pas à
709
714
  l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
710
715
  sans démarrer un serveur.
711
716
 
@@ -714,7 +719,7 @@ import { defineRealtimeConfig } from "@nodefony/realtime";
714
719
 
715
720
  const cfg = defineRealtimeConfig({ backplane: { driver: "redis" } });
716
721
  // cfg.limits.maxChannelsPerConnection === 256 — les sections omises gardent leurs défauts.
717
- // Une valeur invalide lève une ZodError, avec le chemin exact du champ fautif.
722
+ // Une valeur invalide ou une clé inconnue lève une BootConfigurationError, avec le chemin exact du champ fautif.
718
723
  ```
719
724
 
720
725
  `realtimeConfigJsonSchema()` (`defineModuleConfig.ts:69`) produit le schéma JSON du module, chaque
@@ -748,16 +753,16 @@ Trois endroits, par ordre de fiabilité décroissante.
748
753
 
749
754
  ## ⚠️ Pièges
750
755
 
751
- | Symptôme | Cause | Correction |
752
- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
753
- | Un message n'arrive qu'à la moitié des clients, sans aucune erreur | plusieurs répliques avec `driver: "loopback"` | passer à `redis` et déclarer `@nodefony/redis` dans le manifeste |
754
- | Driver `redis` configuré, journal `RealtimeHub reste local` | module `@nodefony/redis` absent du manifeste, ou connexions `publish`/`subscribe` indisponibles | déclarer le module ; vérifier que Redis répond |
755
- | Le fan-out traverse, mais des messages d'un autre environnement arrivent | `backplane.namespace` non posé : deux déploiements de la même application dérivent le même canal | poser un namespace explicite et distinct par environnement |
756
- | Une clé écrite dans `use()` n'a aucun effet | nom de clé fautif : le registre de types n'étant pas augmenté, l'éditeur ne corrige pas, et Zod **retire** l'inconnu | relire la configuration effective dans Studio, onglet Config |
757
- | 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) |
758
- | 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` |
759
- | `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 |
760
- | 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 |
756
+ | Symptôme | Cause | Correction |
757
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
758
+ | Un message n'arrive qu'à la moitié des clients, sans aucune erreur | plusieurs répliques avec `driver: "loopback"` | passer à `redis` et déclarer `@nodefony/redis` dans le manifeste |
759
+ | Driver `redis` configuré, journal `RealtimeHub reste local` | module `@nodefony/redis` absent du manifeste, ou connexions `publish`/`subscribe` indisponibles | déclarer le module ; vérifier que Redis répond |
760
+ | Le fan-out traverse, mais des messages d'un autre environnement arrivent | `backplane.namespace` non posé : deux déploiements de la même application dérivent le même canal | poser un namespace explicite et distinct par environnement |
761
+ | Le démarrage échoue avec `clé inconnue : …` | clé absente du schéma strict (faute de frappe, ou clé retirée) | corriger ou retirer la clé que nomme le message |
762
+ | 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
+ | 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
+ | `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 |
761
766
 
762
767
  ## 🧪 Tests & couverture
763
768
 
@@ -778,12 +783,12 @@ qui compte ici, c'est **ce que les suites prouvent sur la configuration**.
778
783
  > **Un run vert ne prouve pas le fan-out entre machines.** Le banc Redis est doublement conditionnel :
779
784
  > il n'est lancé que sur demande, et il se **saute** de lui-même si aucun Redis ne répond. Un test
780
785
  > sauté compte comme un succès — on peut donc lire « tout est vert » sur une suite qui n'a jamais
781
- > ouvert une connexion. Le fan-out entre workers, lui, ne demande rien et tourne toujours.
786
+ > ouvert une connexion. Le fan-out entre workers ne demande aucune infrastructure externe, mais ne tourne que dans `npm run test:cluster` (avec `NF_RUN_CLUSTER_E2E=1`).
782
787
  >
783
788
  > ```bash
784
789
  > cd src/packages/@nodefony/realtime
785
- > npm test # unitaires + IPC entre workers
786
- > NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm test # + le fan-out entre machines
790
+ > npm test # unitaires et intégration
791
+ > NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm run test:cluster # IPC entre workers + fan-out entre machines
787
792
  > npm run coverage # couverture (vitest, v8)
788
793
  > ```
789
794
 
@@ -134,7 +134,7 @@ le moment venu.
134
134
 
135
135
  ```bash
136
136
  # 1. Créer l'application (profil complet : http, framework, realtime, sécurité, frontend)
137
- npx nodefony create app mon-chat --complete
137
+ npx nodefony create app mon-chat --preset complete
138
138
  cd mon-chat
139
139
 
140
140
  # 2. Créer le module qui portera le salon
@@ -1496,7 +1496,7 @@ pour lire ta propre sortie :
1496
1496
  | `backpressure.maxBufferedAmount` | La **pire** file d'envoi observée à l'instant de la mesure. C'est l'indicateur d'alerte. |
1497
1497
  | `backpressure.slowConsumers` | Combien de connexions dépassent le seuil de consommateur lent. |
1498
1498
  | `backpressure.drops` | Messages abandonnés faute de pouvoir être livrés. |
1499
- | `backplane` | Présent uniquement si un relais est branché : son driver, son type, s'il traverse les processus. |
1499
+ | `backplane` | Toujours présent : carte d'identité du relais (driver, nature, `crossPod`) ; sans relais, descripteur `local` / `loopback`. |
1500
1500
 
1501
1501
  Deux lectures suffisent à diagnostiquer :
1502
1502
 
@@ -1546,12 +1546,14 @@ Ce qui est couvert, par nature de test :
1546
1546
  - **Unitaires** — le hub (abonnement, fan-out, cycle de vie des providers), les décorateurs et leurs
1547
1547
  métadonnées, le registre de drivers de backplane, le contrôleur, le service, le transport
1548
1548
  WebSocket, la validation de configuration, la dérivation de l'identifiant d'origine.
1549
- - **Tests d'attaque** (`*.attack.test.ts`) — trois scénarios verrouillés en non-régression : le
1550
- plafond de canaux par connexion, la révocation d'une session en cours de connexion, et le cas
1551
- d'une politique de canal déclarée **sans** gardien branché (la dégradation doit rester bruyante).
1549
+ - **Tests d'attaque** (`*.attack.test.ts`) — quatre scénarios verrouillés en non-régression : le
1550
+ plafond de canaux par connexion, la révocation d'une session en cours de connexion, le cas
1551
+ d'une politique de canal déclarée **sans** gardien branché (la dégradation doit rester bruyante),
1552
+ et l'injection par le bus (un pair ne pousse pas sur un canal non diffusable, un message forgé sur
1553
+ un bus scellé est ignoré).
1552
1554
  - **Intégration de bout en bout** (`*.e2e.test.ts`) — le cheminement complet sur boucle locale, les
1553
- chemins de contrôleur, l'autorisation de canal, le branchement du firewall, et le cluster IPC avec
1554
- de vrais processus enfants.
1555
+ chemins de contrôleur, l'autorisation de canal, le branchement du firewall. Le cluster IPC, avec
1556
+ de vrais processus enfants, se lance à part : `npm run test:cluster` avec `NF_RUN_CLUSTER_E2E=1`.
1555
1557
  - **Cluster Redis** — exercé de bout en bout, mais **conditionné à la présence d'un Redis**. Sans
1556
1558
  lui, ces cas sont ignorés, et un test ignoré compte comme réussi : ne conclus pas d'un « tout est
1557
1559
  vert » que le chemin Redis a été vérifié chez toi.
@@ -1598,22 +1600,22 @@ Pour qui veut lire l'implémentation derrière chaque étape :
1598
1600
  | Ce que tu as utilisé | Où c'est écrit |
1599
1601
  | --- | --- |
1600
1602
  | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:192`), `RealtimeInbound()` (`realtimeDecorators.ts:231`) |
1601
- | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:249`) |
1602
- | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:219`) |
1603
- | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:462`), `RealtimeController.startChannel()` (`RealtimeController.ts:746`) |
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`) |
1604
1606
  | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:623`) |
1605
1607
  | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:668`) |
1606
1608
  | Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
1607
- | Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1127`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1127`) |
1609
+ | Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1143`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1117`) |
1608
1610
  | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:279`) |
1609
- | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:400`), fermetures `origin not allowed` (`RealtimeController.ts:327`) et `unauthorized` (`RealtimeController.ts:349`) |
1610
- | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:810`) |
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`) |
1611
1613
  | Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:56`), fermeture `slow consumer` (`RealtimeHub.ts:63`) |
1612
- | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:984`) |
1614
+ | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:1000`) |
1613
1615
  | L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
1614
- | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:849`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1616
+ | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:865`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1615
1617
  | Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
1616
1618
  | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:213`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
1617
- | 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:182`) |
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`) |
1618
1620
  | Le client navigateur | `RealtimeClient.connect()` (`RealtimeClient.ts:389`), `RealtimeClient.subscribe()` (`RealtimeClient.ts:530`), `RealtimeClient.request()` (`RealtimeClient.ts:729`), `RealtimeClient.onDenied()` (`RealtimeClient.ts:471`) |
1619
- | L'upload | `UploadedFile` (`routerDecorators.ts:1253`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:185`) |
1621
+ | L'upload | `UploadedFile` (`routerDecorators.ts:1286`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:226`) |
package/docs/index.md CHANGED
@@ -304,7 +304,7 @@ jamais recopiées ici : elles divergeraient en silence.
304
304
  ## ⚙️ Configuration
305
305
 
306
306
  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 :
307
+ contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Six blocs :
308
308
 
309
309
  - `backplane` — le driver de fan-out et son espace de nommage. Ce cloisonnement devient
310
310
  indispensable dès que **deux déploiements partagent le même Redis** : sans lui, leurs publications
@@ -313,6 +313,7 @@ contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Cinq bloc
313
313
  - `limits` — le plafond de canaux par connexion, garde anti-saturation mémoire.
314
314
  - `slowConsumer` — le seuil à partir duquel un client trop lent est signalé par la sonde.
315
315
  - `cluster.probe` — la sonde agrégée du pod, en mode multi-workers.
316
+ - `clientLogs` — la réception des journaux du navigateur (canal montant), fermée par défaut.
316
317
 
317
318
  Chaque bloc, ses valeurs d'usine et les situations qui justifient d'en changer sont détaillés dans
318
319
  [Configuration](./configuration.md).
@@ -325,7 +326,7 @@ vue de tous les workers d'un pod.
325
326
 
326
327
  Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
327
328
  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:849`).
329
+ l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
329
330
 
330
331
  ## 🧪 Tests & couverture
331
332
 
@@ -333,24 +334,25 @@ Les compteurs sont régénérés depuis vitest, jamais figés dans cette prose.
333
334
  ici, c'est **ce que les suites prouvent** — et ce qu'elles ne prouvent qu'à condition d'avoir
334
335
  l'infrastructure sous la main.
335
336
 
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 |
337
+ | Type | Où | Ce qui est prouvé |
338
+ | --------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
339
+ | Unitaire | `nodefony/tests/unit/**` | hub, registre de drivers, schéma de configuration, décorateurs |
340
+ | Intégration | `nodefony/tests/integration/**` | socket réelle : abonnements, chemins de contrôleur, autorisation |
341
+ | Multi-processus | `clusterIpc.e2e.test.ts` | fan-out entre workers, sans infrastructure externe (`npm run test:cluster`) |
342
+ | Cross-machine | `redisCluster.e2e.test.ts` | fan-out entre pods via pub/sub Redis |
343
+ | Tests d'attaque | `*.attack.test.ts` | plafond de canaux, révocation d'identité, politique non appliquée, injection sur le bus |
343
344
 
344
345
  > [!WARNING]
345
346
  > **Une suite verte ne prouve rien sur le fan-out cross-machine.** Le banc Redis est doublement
346
347
  > conditionnel : il ne s'exécute que si tu le demandes (`NF_RUN_CLUSTER_E2E=1`), et il se **saute** de
347
348
  > lui-même si aucun Redis n'est joignable. Or un test sauté compte comme un succès — on peut donc
348
349
  > 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.
350
+ > workers d'un même pod ne demande aucune infrastructure externe, mais ne tourne que dans
351
+ > `npm run test:cluster` avec `NF_RUN_CLUSTER_E2E=1`.
350
352
  >
351
353
  > ```bash
352
354
  > # 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
355
+ > NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm run test:cluster
354
356
  > ```
355
357
 
356
358
  ## 🔗 Pour aller plus loin
@@ -367,6 +369,3 @@ l'infrastructure sous la main.
367
369
  - 🏛️ **Transverse** : [vue d'ensemble du framework](../../../../../docs/architecture/vue-ensemble.md) ·
368
370
  [le pipeline d'une requête](../../../../../docs/architecture/pipeline-requete.md)
369
371
  - 📖 [Lexique général](../../../../../docs/lexique.md) du framework.
370
- </content>
371
-
372
- </invoke>
@@ -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:849`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
99
+ (`RealtimeHub.ts:865`) 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
 
@@ -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:838`). |
268
+ | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:854`). |
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
@@ -447,7 +447,8 @@ après fusion et validation — la seule vue qui dit ce qui s'applique, plutôt
447
447
 
448
448
  > [!CAUTION]
449
449
  > `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
450
+ > connexion se règlent côté `@nodefony/http` (`websocket.maxBackpressure`, `.backpressurePolicy`,
451
+ > `.backpressureCloseAfterDrops`). Baisser cette clé rend le signalement plus précoce ; ça
451
452
  > ne change rien au comportement de la socket.
452
453
 
453
454
  Couper la sonde cluster est un **contournement total**, pas une mise en sourdine : sans elle, il n'y a
@@ -552,7 +553,7 @@ aux sondes qui instrumentent chaque opération et se paient donc à l'usage.
552
553
  | `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
554
  | 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
555
  | 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 |
556
+ | 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
557
  | 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
558
  | 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
559
  | 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 +567,7 @@ infrastructure, puisque la sonde est une lecture pure.
566
567
  | Suite | Ce qui est prouvé |
567
568
  | --- | --- |
568
569
  | `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` |
570
+ | `WsConnectionTransport.test.ts` | Les seuils de contre-pression : abandon « le dernier gagne », puis fermeture `1013` |
570
571
  | `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
572
 
572
573
  **Ce qui manque, et qu'il faut savoir** : il n'existe pas de banc de charge dédié à la sonde
@@ -588,6 +589,3 @@ npm run coverage # rapport vitest — jamais de pourcentage figé dans cette p
588
589
  - [Sécurité](securite.md) — qui a le droit de s'abonner à un canal de santé, et pourquoi ça compte.
589
590
  - [Cookbook — un chat](cookbook-chat.md) — le patron appliqué de bout en bout sur une vraie fonctionnalité.
590
591
  - [Vocabulaire](vocabulaire.md) — les douze mots de la socket.
591
- </content>
592
-
593
- </invoke>