@nodefony/realtime 10.0.0-beta.1 → 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.
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorateMetadata.js
2
2
  function __decorateMetadata(k, v) {
3
3
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
4
  }
package/dist/index.js CHANGED
@@ -15,8 +15,8 @@ import { ClusterBackplane, processIpcTransport } from "./nodefony/src/backplane/
15
15
  import { getBackplaneDriver, listBackplaneDrivers, registerBackplaneDriver } from "./nodefony/src/backplane/backplaneRegistry.js";
16
16
  import { ClusterProbeClient, clusterProbeHealth, clusterProbeInstance, clusterProbeRequestEnrich, mergeClusterHealth, processProbeTransport, setClusterProbeClient } from "./nodefony/src/cluster/ClusterProbeClient.js";
17
17
  import { buildOwnHealth, buildRealtimeHealth, createRealtimeAdminApi } from "./nodefony/src/server/RealtimeAdminApi.js";
18
- import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
19
- import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
18
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
19
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
20
20
  import { Kernel, Module, services, withTimeout } from "nodefony";
21
21
  //#region index.ts
22
22
  /**
package/docs/actions.md CHANGED
@@ -485,7 +485,7 @@ prudente est donc de **s'abonner avant** de lancer l'appel.
485
485
 
486
486
  Nodefony a choisi la garantie côté serveur plutôt que la discipline côté client : le producteur du
487
487
  canal **rejoue son historique** au nouvel abonné, de sorte qu'un arrivant tardif voit tout depuis
488
- le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:425`). C'est ce qui autorise le front
488
+ le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:456`). C'est ce qui autorise le front
489
489
  à faire l'appel d'abord et à s'abonner ensuite, sans rien perdre.
490
490
 
491
491
  > [!TIP]
@@ -524,18 +524,18 @@ passe par le pont pour tout ce qui est déjà une route. Le détail du pont vit
524
524
 
525
525
  ## ⚠️ Pièges
526
526
 
527
- | Symptôme | Cause | Correction |
528
- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
529
- | `RPC timeout: <méthode>` alors que le serveur a bien répondu | le handler dure plus que 30 s, le défaut | passer le délai en **3ᵉ argument** : `request(m, p, 120_000)` |
530
- | Le client attend indéfiniment, aucune erreur | le handler ne rend jamais (il publie au lieu de retourner) — pas de frame `result` | toujours `return` une valeur ; publier **en plus**, jamais **à la place** |
531
- | `-32601 method not found` | nom mal orthographié, ou action déclarée sur un **autre** endpoint | vérifier `socket.serverMethods` — c'est la liste réelle de CETTE connexion |
532
- | `-32603 internal error` sans détail | un `throw` ordinaire est rendu opaque au client (Zero Trust) | lever une `RpcError` avec un code et un message publiables |
533
- | `-32001 unauthorized` sur une action légitime | le nom commence par `nodefony:`, le namespace réservé à la plateforme | renommer hors de `nodefony:`, ou obtenir `ROLE_NODEFONY_ADMIN` |
534
- | Une action sensible est appelable par un anonyme | une action applicative est **libre** tant qu'aucune politique ne la couvre | ajouter une règle de préfixe (`security/nodefony/config/config.ts:906`) |
535
- | Un travail relancé crée deux jobs | action non idempotente rejouée après une reconnexion | action compagnon d'annulation, ou identifiant fourni par l'appelant + mémorisation |
536
- | `abort()` introuvable sur la socket | il n'existe pas — et n'arrêterait pas le serveur de toute façon | exposer une action d'annulation qui prend l'identifiant du travail |
537
- | Les premières lignes d'un job manquent | abonnement au canal après le début de la production, sans historique rejoué | s'abonner **avant** de lancer, ou faire rejouer l'historique par le producteur |
538
- | Toutes les `Promise` rejettent d'un coup | la connexion s'est fermée : `dispose()` vide la table des appels en attente | attendu ; relancer après reconnexion, l'appel n'a pas survécu |
527
+ | Symptôme | Cause | Correction |
528
+ | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
529
+ | `RPC timeout: <méthode>` alors que le serveur a bien répondu | le handler dure plus que 30 s, le défaut | passer le délai en **3ᵉ argument** : `request(m, p, 120_000)` |
530
+ | Le client attend indéfiniment, aucune erreur | le handler ne rend jamais (il publie au lieu de retourner) — pas de frame `result` | toujours `return` une valeur ; publier **en plus**, jamais **à la place** |
531
+ | `-32601 method not found` | nom mal orthographié, ou action déclarée sur un **autre** endpoint | vérifier `socket.serverMethods` — c'est la liste réelle de CETTE connexion |
532
+ | `-32603 internal error` sans détail | un `throw` ordinaire est rendu opaque au client (Zero Trust) | lever une `RpcError` avec un code et un message publiables |
533
+ | `-32001 unauthorized` sur une action légitime | le nom commence par `nodefony:`, le namespace réservé à la plateforme | renommer hors de `nodefony:`, ou obtenir `ROLE_NODEFONY_ADMIN` |
534
+ | Une action est refusée à un anonyme alors qu'on la croyait publique | une action est fermée par défaut (`{ authenticated: true }`) | déclarer `{ authenticated: false }` pour l'ouvrir ; un rôle se pose par une règle de préfixe |
535
+ | Un travail relancé crée deux jobs | action non idempotente rejouée après une reconnexion | action compagnon d'annulation, ou identifiant fourni par l'appelant + mémorisation |
536
+ | `abort()` introuvable sur la socket | il n'existe pas — et n'arrêterait pas le serveur de toute façon | exposer une action d'annulation qui prend l'identifiant du travail |
537
+ | Les premières lignes d'un job manquent | abonnement au canal après le début de la production, sans historique rejoué | s'abonner **avant** de lancer, ou faire rejouer l'historique par le producteur |
538
+ | Toutes les `Promise` rejettent d'un coup | la connexion s'est fermée : `dispose()` vide la table des appels en attente | attendu ; relancer après reconnexion, l'appel n'a pas survécu |
539
539
 
540
540
  ## 🧪 Tests & couverture
541
541
 
@@ -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
@@ -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,10 +273,16 @@ 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
 
@@ -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
@@ -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
 
@@ -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.
@@ -1616,4 +1618,4 @@ Pour qui veut lire l'implémentation derrière chaque étape :
1616
1618
  | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:213`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
1617
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:1286`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:200`) |
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).
@@ -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
@@ -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
package/docs/protocole.md CHANGED
@@ -348,8 +348,9 @@ ouvertes à l'application. Colonne `id` : présent = requête (réponse due), ab
348
348
  | `<action>` | client→server | **oui** | Toute action déclarée par `@RealtimeAction` | `realtimeDecorators.ts:101` |
349
349
 
350
350
  > [!TIP]
351
- > **L'accueil est ta carte du territoire.** `methods` et `channels` sont construits à partir de ce
352
- > que l'endpoint expose réellement (`RealtimeController.ts:710`) : un client peut activer ou griser
351
+ > **L'accueil est ta carte du territoire.** `channels` ne liste que les canaux que ce visiteur
352
+ > pourrait obtenir (filtrés par le même verrou que `subscribe`), `methods` les actions de l'endpoint
353
+ > (`RealtimeController.ts:710`) : un client peut activer ou griser
353
354
  > ses commandes sans rien coder en dur. Côté navigateur, ils se lisent en `socket.serverMethods` et
354
355
  > `socket.serverChannels`.
355
356
 
@@ -437,8 +438,10 @@ radicalement :
437
438
 
438
439
  `IRealtimeDenied` (`RealtimeEventMap.ts:269`) porte `channel` et `reason` — plus un `detail` optionnel, posé hors production seulement — et le motif
439
440
  est **générique**. Jamais « il te manque `ROLE_ADMIN` » : ce serait un oracle d'autorisation, un
440
- attaquant y lirait la carte des droits. Deux motifs circulent : `forbidden` (le verrou a dit non) et
441
- `limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts:811`). Côté client,
441
+ attaquant y lirait la carte des droits. Trois motifs circulent : `forbidden` (le verrou a dit non),
442
+ `limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts:811`) et `unknown`
443
+ (aucun producteur ne sert ce nom — jamais un oracle : un canal gardé rend `forbidden`, qu'il existe ou
444
+ non). Côté client,
442
445
  `onDenied()` (`RealtimeClient.ts:469`) branche un handler dessus.
443
446
 
444
447
  > [!CAUTION]
package/docs/securite.md CHANGED
@@ -93,16 +93,16 @@ Les étapes 1 et 2 appartiennent à `@nodefony/http` et `@nodefony/security` : l
93
93
  Une WebSocket authentifiée est une cible de choix : elle porte une identité, vit longtemps et
94
94
  diffuse en continu.
95
95
 
96
- | Attaque | Ce qui la bloque | Où |
97
- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
98
- | **CSWSH** — `evil.com` fait `new WebSocket("wss://app.exemple.com/rt")`, le navigateur joint le cookie | Contrôle d'`Origin` same-origin par défaut, puis allowlist stricte | `@nodefony/http` transport, puis `csrf.checkOrigin` realtime |
99
- | **Écoute des flux internes** — un visiteur s'abonne à `nodefony:syslog` et lit les logs du pod | Plancher des namespaces réservés (authentifié + `ROLE_NODEFONY_ADMIN`) | Verrou de frame `@nodefony/security` |
100
- | **Élévation par canal métier** — un `ROLE_USER` s'abonne au canal admin d'un autre module | Policy déclarée sur le canal, évaluée avec la hiérarchie de rôles | `@RealtimeChannel(name, { roles })` + verrou de frame |
101
- | **Pont API plus permissif que REST** — `api.request {path}` pour contourner un 401 HTTP | Re-match de la MÊME zone firewall que `GET {path}` | Verrou de frame, surface `api.request` |
102
- | **Socket zombie** — un admin se déconnecte, sa socket continue de diffuser | Re-validation périodique de l'identité, fermeture `4001` | Tick de révocation du hub |
103
- | **DoS mémoire par abonnements** — une connexion ouvre des milliers de canaux | Plafond de canaux par connexion (256 par défaut) | `limits.maxChannelsPerConnection` |
104
- | **DoS mémoire par lenteur** — un client ne lit pas, la file d'envoi enfle jusqu'à l'OOM | Jet de frames à 1 MiB, fermeture `1013` à 8 MiB | Back-pressure du transport WS |
105
- | **Oracle d'autorisation** — sonder les canaux pour cartographier les droits | Motif de refus **générique** (`forbidden`), jamais le détail | `realtime:denied` |
96
+ | Attaque | Ce qui la bloque | Où |
97
+ | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------ |
98
+ | **CSWSH** — `evil.com` fait `new WebSocket("wss://app.exemple.com/rt")`, le navigateur joint le cookie | Contrôle d'`Origin` same-origin par défaut, puis allowlist stricte | `@nodefony/http` transport, puis `csrf.checkOrigin` realtime |
99
+ | **Écoute des flux internes** — un visiteur s'abonne à `nodefony:syslog` et lit les logs du pod | Plancher des namespaces réservés (authentifié + `ROLE_NODEFONY_ADMIN`) | Verrou de frame `@nodefony/security` |
100
+ | **Élévation par canal métier** — un `ROLE_USER` s'abonne au canal admin d'un autre module | Policy déclarée sur le canal, évaluée avec la hiérarchie de rôles | `@RealtimeChannel(name, { roles })` + verrou de frame |
101
+ | **Pont API plus permissif que REST** — `api.request {path}` pour contourner un 401 HTTP | Re-match de la MÊME zone firewall que `GET {path}` | Verrou de frame, surface `api.request` |
102
+ | **Socket zombie** — un admin se déconnecte, sa socket continue de diffuser | Re-validation périodique de l'identité, fermeture `4001` | Tick de révocation du hub |
103
+ | **DoS mémoire par abonnements** — une connexion ouvre des milliers de canaux | Plafond de canaux par connexion (256 par défaut) | `limits.maxChannelsPerConnection` |
104
+ | **DoS mémoire par lenteur** — un client ne lit pas, la file d'envoi enfle jusqu'à l'OOM | Jet de frames au-delà de 4 MiB, fermeture `1013` après 1000 jets d'affilée | Back-pressure du transport WS |
105
+ | **Oracle d'autorisation** — sonder les canaux pour cartographier les droits | Motif de refus **générique** (`forbidden`), jamais le détail | `realtime:denied` |
106
106
 
107
107
  > [!IMPORTANT]
108
108
  > Les lignes 2 à 5 supposent `@nodefony/security` chargé **avec au moins une zone protégée**. Sans
@@ -517,12 +517,16 @@ politique déclarée, alors que ce module en déclare pour d'autres canaux. […
517
517
 
518
518
  Deux façons de fermer, selon l'intention :
519
519
 
520
- - **le canal doit être gardé** → l'exposer sous un nom déclaré (un canal par nom exact), ou porter
521
- la vérification **dans la fabrique** elle-même, qui reçoit le nom demandé et peut refuser (`null`) ;
520
+ - **la famille doit être gardée** → déclarer un **motif** : `@RealtimeChannel("chat:room:*", { roles })`
521
+ garde tous les canaux de la famille ; l'avertissement propose le motif à écrire et se tait dès
522
+ qu'un motif couvre le canal ;
523
+ - **le canal doit être gardé seul** → l'exposer sous un nom déclaré (un canal par nom exact), ou
524
+ porter la vérification **dans la fabrique** elle-même, qui reçoit le nom demandé et peut refuser
525
+ (`null`) ;
522
526
  - **le canal est public** → rien à faire, l'avertissement dit seulement qu'il n'est pas gardé.
523
527
 
524
- > Une déclaration par **motif** (`chat:*`) lèverait la limite ; elle n'existe pas encore et ne se
525
- > conçoit pas seule — elle fait partie d'un chantier plus large sur les canaux à membres.
528
+ > Le nom exact l'emporte sur un motif ; entre deux motifs, le plus spécifique gagne. Un `*` dans un
529
+ > nom souscrit est refusé : il désigne un motif de politique, jamais un canal.
526
530
 
527
531
  ### ⚠️ La condition d'activation — le point critique
528
532
 
@@ -676,7 +680,7 @@ canaux). `WsConnectionTransport.send()` (`WsConnectionTransport.ts:82`) applique
676
680
 
677
681
  | `bufferedAmount` | Action |
678
682
  | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
679
- | > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:1044`) | politique `websocket.backpressurePolicy` : `drop` (défaut) jette la frame — canaux d'état, le prochain snapshot la remplace |
683
+ | > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:1048`) | politique `websocket.backpressurePolicy` : `drop` (défaut) jette la frame — canaux d'état, le prochain snapshot la remplace |
680
684
  | `websocket.backpressureCloseAfterDrops` drops CONSÉCUTIFS — 1000 par défaut | `close(1013)` « Try Again Later » ; le client se reconnecte et resynchronise |
681
685
 
682
686
  ### Taille des messages entrants
@@ -693,7 +697,7 @@ au défaut de la librairie `ws`.
693
697
  | Limite de **fréquence** des frames entrantes | Un client authentifié peut inonder le peer ; seul le coût CPU le freine |
694
698
  | Limite de **connexions par IP ou par utilisateur** | Rien n'empêche N sockets par client au niveau du module |
695
699
  | Plafond **global** de canaux du process | Le plafond est par connexion ; M connexions × 256 canaux reste possible |
696
- | Seuils de back-pressure **configurables** | `slowConsumer.bytes` ne pilote que le **comptage** de la sonde, pas les seuils de drop/close (`WsConnectionTransport` est construit sans override, `RealtimeController.ts:421`) |
700
+ | Seuils de back-pressure dans ce module | aucun réglage dans `@nodefony/realtime` : ils se règlent sur le serveur WebSocket de `@nodefony/http` (`websocket.maxBackpressure`, `.backpressureCloseAfterDrops`) |
697
701
 
698
702
  ## ⚙️ Configuration de sécurité
699
703
 
@@ -795,7 +799,7 @@ n'est pas appliquée.
795
799
  | Un admin déconnecté garde ses flux pendant une dizaine de secondes | Le tick de révocation est périodique (30 s), pas immédiat | Comportement attendu ; pour une coupure immédiate, fermer la socket côté serveur |
796
800
  | Une session révoquée ne ferme **jamais** la socket | La session n'était pas lisible au handshake → revalidateur `null`, `isValid()` répond toujours `true` | Vérifier que le handshake traverse bien la zone (session chargée avant le controller realtime) |
797
801
  | Le client se croit abonné, ne reçoit rien | Refus d'autorisation, plafond atteint, **ou nom de canal sans producteur** (pas de réponse RPC) | Écouter `realtime:denied` : `forbidden` (droits), `limit` (borne), `unknown` (le nom ne désigne rien) |
798
- | `slowConsumer.bytes` augmenté, les frames sont toujours jetées à 1 MiB | Cette clé pilote le **comptage** de la sonde, pas les seuils de drop/close du transport | Les seuils de back-pressure ne sont pas configurables aujourd'hui |
802
+ | `slowConsumer.bytes` augmenté, les frames sont toujours jetées à 4 MiB | Cette clé pilote le **comptage** de la sonde, pas les seuils de drop/close du transport | Régler `websocket.maxBackpressure` / `.backpressureCloseAfterDrops` dans `@nodefony/http` |
799
803
  | Deux déploiements se parlent en cross-talk | Pas de `backplane.namespace` sur un Redis mutualisé (la base Redis ne cloisonne pas le pub/sub) | Poser un `namespace` explicite par déploiement |
800
804
  | Le fan-out cross-pod s'arrête après avoir posé un secret | `backplane.secret` différent d'un pod à l'autre : les messages sont scellés, aucun ne se vérifie | Le **même** secret sur tous les pods (`NF_REALTIME_BACKPLANE_SECRET`) ; suivre `ingressRejectedTotal` |
801
805
  | Un canal broadcast ne reçoit rien des autres pods | Le préfixe n'est pas déclaré côté receveur → l'entrée refuse le canal (compté) | Déclarer le préfixe (`broadcast` du controller) sur **tous** les pods, pas seulement l'émetteur |
@@ -31,7 +31,7 @@ source: "src/packages/@nodefony/realtime/docs/vocabulaire.md"
31
31
  # Vocabulaire — le lexique de la socket Nodefony
32
32
 
33
33
  > Cette page est le **dictionnaire** du temps réel Nodefony : chaque mot y a une définition, une
34
- > raison d'être ce mot-là, et le symbole du code qui l'incarne. Les cinq autres pages du module
34
+ > raison d'être ce mot-là, et le symbole du code qui l'incarne. Les huit autres pages du module
35
35
  > l'emploient sans le redéfinir. Règle de lecture : quand une discussion realtime s'embrouille,
36
36
  > demande-toi **de quoi on parle — de la prise, de l'autocom, ou du fond de panier ?** Neuf fois
37
37
  > sur dix, la confusion tenait à un seul mot.
@@ -302,7 +302,7 @@ front**. Une différence assumée : `request()` n'y est pas supporté — au-des
302
302
  ### `accueil` (welcome) — la première frame
303
303
 
304
304
  La notification `realtime:welcome`, poussée par le serveur juste après le handshake. Elle annonce le
305
- protocole, les canaux et actions **découvrables** de l'endpoint, et l'identité résolue. Un client
305
+ protocole, les canaux que ce visiteur peut obtenir, les actions de l'endpoint, et l'identité résolue. Un client
306
306
  attend cette frame avant de pousser quoi que ce soit.
307
307
 
308
308
  `IRealtimeWelcome` (`RealtimeEventMap.ts:231`).
@@ -762,7 +762,7 @@ n° 1 : il monte quand le client n'absorbe plus. Deux seuils distincts existent
762
762
  Seuil de comptage `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:63`) — une constante, car il ne
763
763
  décide de rien. Les seuils d'ACTION, eux, sont des réglages : `websocket.maxBackpressure`,
764
764
  `websocket.backpressurePolicy` et `websocket.backpressureCloseAfterDrops` (`http`,
765
- `http/nodefony/config/config.ts:1044`).
765
+ `http/nodefony/config/config.ts:1048`).
766
766
 
767
767
  ### `consommateur lent` — la connexion qui décroche
768
768
 
@@ -829,8 +829,8 @@ l'aperçu, régénérée depuis vitest et jamais figée ici :
829
829
  - **intégration** : le driver Redis contre un serveur réel ;
830
830
  - **bout en bout** : boucle locale, chemins de contrôleur, autorisation de canal, câblage du
831
831
  pare-feu, cluster par communication inter-process, cluster Redis ;
832
- - **attaque** : plafond de canaux, révocation, politique non appliquée — les trois pièges de sécurité
833
- du lexique ont chacun leur banc hostile.
832
+ - **attaque** : plafond de canaux, révocation, politique non appliquée, injection par le bus — les
833
+ pièges de sécurité du lexique ont chacun leur banc hostile.
834
834
 
835
835
  Couverture : `npm run coverage` dans `@nodefony/realtime`.
836
836
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/realtime",
3
- "version": "10.0.0-beta.1",
3
+ "version": "10.0.0-beta.2",
4
4
  "description": "Couche temps réel serveur de Nodefony : canaux, appels bidirectionnels JSON-RPC 2.0, contre-pression, et diffusion entre plusieurs instances (boucle locale, IPC de grappe, Redis)",
5
5
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
6
  "type": "module",
@@ -41,20 +41,20 @@
41
41
  "esm"
42
42
  ],
43
43
  "peerDependencies": {
44
- "@nodefony/framework": "^10.0.0-beta.1",
45
- "@nodefony/http": "^10.0.0-beta.1",
46
- "nodefony": "^10.0.0-beta.1",
44
+ "@nodefony/framework": "^10.0.0-beta.2",
45
+ "@nodefony/http": "^10.0.0-beta.2",
46
+ "nodefony": "^10.0.0-beta.2",
47
47
  "reflect-metadata": "0.2.2",
48
48
  "zod": "^4.6.5"
49
49
  },
50
50
  "devDependencies": {
51
- "@nodefony/framework": "^10.0.0-beta.1",
52
- "@nodefony/http": "^10.0.0-beta.1",
51
+ "@nodefony/framework": "^10.0.0-beta.2",
52
+ "@nodefony/http": "^10.0.0-beta.2",
53
53
  "@types/node": "26.6.3",
54
- "@vitest/coverage-v8": "5.0.2",
55
- "nodefony": "^10.0.0-beta.1",
54
+ "@vitest/coverage-v8": "5.0.3",
55
+ "nodefony": "^10.0.0-beta.2",
56
56
  "reflect-metadata": "0.2.2",
57
- "vitest": "5.0.2"
57
+ "vitest": "5.0.3"
58
58
  },
59
59
  "private": false,
60
60
  "engines": {