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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +17 -10
  5. package/dist/nodefony/config/config.js +6 -3
  6. package/dist/nodefony/config/configSchema.js +19 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +42 -15
  8. package/dist/nodefony/decorators/realtimeDecorators.js +68 -17
  9. package/dist/nodefony/src/backplane/RedisBackplane.js +21 -4
  10. package/dist/nodefony/src/backplane/bootNotices.js +43 -0
  11. package/dist/nodefony/src/backplane/publishQueue.js +1 -4
  12. package/dist/nodefony/src/cluster/ClusterProbeClient.js +15 -4
  13. package/dist/nodefony/src/guards.js +41 -0
  14. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -4
  15. package/dist/nodefony/src/server/RealtimeController.js +120 -50
  16. package/dist/nodefony/src/server/RealtimeHub.js +29 -51
  17. package/dist/nodefony/src/server/ServerRealtimeSocket.js +13 -5
  18. package/dist/nodefony/src/server/syslogUplink.js +4 -2
  19. package/dist/nodefony/src/service/RealtimeService.js +3 -2
  20. package/dist/nodefony/testing/index.js +41 -1
  21. package/dist/types/nodefony/config/configSchema.d.ts +12 -0
  22. package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
  23. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
  24. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
  25. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
  26. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
  27. package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
  28. package/dist/types/nodefony/src/guards.d.ts +32 -0
  29. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
  30. package/dist/types/nodefony/src/server/RealtimeController.d.ts +24 -4
  31. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
  32. package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
  33. package/dist/types/nodefony/testing/index.d.ts +2 -2
  34. package/docs/actions.md +124 -41
  35. package/docs/architecture.md +93 -91
  36. package/docs/cadence-et-contre-pression.md +634 -0
  37. package/docs/configuration.md +66 -56
  38. package/docs/cookbook-chat.md +39 -33
  39. package/docs/index.md +47 -31
  40. package/docs/observabilite.md +21 -18
  41. package/docs/protocole.md +79 -65
  42. package/docs/securite.md +69 -65
  43. package/docs/vocabulaire.md +80 -79
  44. package/package.json +16 -12
  45. package/docs/couverture-bancs.md +0 -139
@@ -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
@@ -57,8 +57,8 @@ est validé puis **gelé** avant que le moindre canal existe.
57
57
  flowchart TD
58
58
  SCHEMA["Schéma Zod du module<br/>realtimeConfigSchema — les valeurs d'usine"]
59
59
  APP["Ton application<br/>use('@nodefony/realtime', { … }) dans nodefony.config.ts"]
60
- ENVGEN["Déploiement<br/>NF__REALTIME__&lt;CHEMIN&gt;=valeur"]
61
- BUILD["Builder<br/>defineRealtimeConfig() : parse Zod → NF_REALTIME_DRIVER → freeze"]
60
+ ENVGEN["Déploiement<br/>NF_REALTIME_DRIVER · NF__REALTIME__&lt;CHEMIN&gt;=valeur"]
61
+ BUILD["Builder<br/>defineRealtimeConfig() : parse Zod → freeze"]
62
62
  HUB["RealtimeHub<br/>garde d'origine · plafond de canaux · seuil de sonde"]
63
63
  BP["Driver de backplane<br/>résolu par nom dans le registre"]
64
64
  SCHEMA --> APP --> ENVGEN --> BUILD
@@ -131,19 +131,19 @@ explication en `.describe()`. Les défauts effectifs sont **dérivés** du sché
131
131
  d'usine serait écrite, donc pas de dérive silencieuse possible entre la doc du champ et son
132
132
  comportement.
133
133
 
134
- Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts:35`) reste **pur** : il analyse l'entrée,
135
- applique les variables d'environnement dédiées (`NF_REALTIME_DRIVER`,
136
- `NF_REALTIME_BACKPLANE_NAMESPACE`, `NF_REALTIME_BACKPLANE_SECRET` — toutes prioritaires sur le
137
- fichier), et gèle le résultat. Il ne décide rien.
134
+ Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts:39`) reste **pur** : il analyse l'entrée
135
+ et gèle le résultat. Il ne lit aucune variable d'environnement. Les trois variables du module
136
+ (`NF_REALTIME_DRIVER`, `NF_REALTIME_BACKPLANE_NAMESPACE`, `NF_REALTIME_BACKPLANE_SECRET`) sont
137
+ déclarées sur leur champ dans le schéma (`.meta({ env })`, `config.ts:59`, `:76`, `:93`) et posées
138
+ par le Kernel **avant** l'analyse : le schéma les valide comme une valeur écrite dans le fichier.
138
139
 
139
140
  Le module **augmente le registre de types** (`index.ts:167`) : `use("@nodefony/realtime", { … })`
140
141
  propose ses clés en autocomplétion et **refuse** une clé inconnue à la compilation.
141
142
 
142
143
  > [!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.
144
+ > Ce filet attrape la faute dès l'écriture. Le schéma, lui, est strict : une clé inconnue arrête le
145
+ > démarrage (`BootConfigurationError`, avec le chemin complet de la clé). `slowConsummer: { bytes:
146
+ 4096 }` est donc refusé au boot ; avec l'augmentation, la même faute ne compile même pas.
147
147
 
148
148
  ## 🚀 Démarrage rapide
149
149
 
@@ -252,12 +252,12 @@ regarder quand un message ne traverse pas.
252
252
 
253
253
  > [!TIP]
254
254
  > Ces quatre lignes viennent du même endroit du code (`Realtime.#wireBackplane()`,
255
- > `src/packages/@nodefony/realtime/index.ts:279`) et de la carte d'identité que chaque driver publie.
255
+ > `src/packages/@nodefony/realtime/index.ts:292`) et de la carte d'identité que chaque driver publie.
256
256
  > Aucun repli n'est silencieux : si le fan-out est dégradé, c'est écrit.
257
257
 
258
258
  ## ⚙️ Le schéma, clé par clé
259
259
 
260
- Six blocs, douze clés au total. Le tableau donne la vérité complète ; les sections qui suivent
260
+ Sept blocs, seize clés au total. Le tableau donne la vérité complète ; les sections qui suivent
261
261
  expliquent quand et pourquoi en changer.
262
262
 
263
263
  <!-- prettier-ignore -->
@@ -274,16 +274,22 @@ expliquent quand et pourquoi en changer.
274
274
  | `csrf.checkOrigin.enabled` | `boolean` | `false` | active le contrôle d'`Origin` à l'ouverture de la socket |
275
275
  | `csrf.checkOrigin.allowList` | `string[]` | `[]` | origines acceptées, **comparaison exacte**. Vide + activé = tout est refusé |
276
276
  | `csrf.checkOrigin.allowMissingOrigin` | `boolean` | `false` | accepter une ouverture sans en-tête `Origin` (clients non-navigateur) |
277
+ | `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 |
278
+ | `clientLogs.maxEntriesPerBatch` | `number` | `50` | entrées acceptées par lot |
279
+ | `clientLogs.maxEntriesPerWindow` | `number` | `300` | entrées acceptées par fenêtre de débit |
280
+ | `clientLogs.windowMs` | `number` | `10000` | durée de la fenêtre de débit, en ms |
281
+ | `clientLogs.maxStringLength` | `number` | `4096` | longueur maximale d'une chaîne reçue |
277
282
 
278
283
  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.
284
+ la sonde : ces comportements existent, mais leurs valeurs sont des constantes du code. Les seuils de
285
+ contre-pression, eux, se règlent sur le serveur WebSocket de `@nodefony/http`
286
+ (`websocket.maxBackpressure`, `.backpressurePolicy`, `.backpressureCloseAfterDrops`).
281
287
 
282
288
  ### `enabled` — le module au repos
283
289
 
284
290
  Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
285
291
  enregistrée, pas de backplane câblé, pas de sonde de pod
286
- (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:239`), et le service devient
292
+ (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:246`), et le service devient
287
293
  inerte (`RealtimeService.init()`, `RealtimeService.ts:77`).
288
294
 
289
295
  > [!WARNING]
@@ -324,7 +330,7 @@ Ne concerne que les drivers qui traversent les machines. Le canal de transport d
324
330
  `nodefony:realtime:<namespace>` (`resolveRedisChannel()`, `RedisBackplane.ts:31`).
325
331
 
326
332
  Le défaut est **dérivé du nom de l'application**
327
- (`src/packages/@nodefony/realtime/index.ts:132`) — ce qui suffit à séparer deux applications
333
+ (`src/packages/@nodefony/realtime/index.ts:139`) — ce qui suffit à séparer deux applications
328
334
  distinctes sur un Redis mutualisé, et **ne suffit pas** à séparer deux déploiements de la **même**
329
335
  application. Préproduction et production portent le même nom d'application, donc le même canal
330
336
  dérivé, donc le même flux.
@@ -372,13 +378,13 @@ jamais instancié, donc zéro minuteur, zéro message IPC — et l'endpoint de s
372
378
  de l'instance courante.
373
379
 
374
380
  Deux leviers coupent la sonde, et **l'un ou l'autre suffit** : cette clé, ou la variable
375
- `NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts:391`). C'est ce qui permet
381
+ `NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts:404`). C'est ce qui permet
376
382
  d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
377
383
 
378
384
  ### `slowConsumer.bytes` — un compteur, pas un frein
379
385
 
380
386
  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:865`).
387
+ compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:847`).
382
388
 
383
389
  > [!IMPORTANT]
384
390
  > **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
@@ -434,7 +440,7 @@ des canaux d'état, où seul le dernier instantané compte.
434
440
  Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
435
441
  côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
436
442
  processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
437
- `RealtimeController.ts:815`).
443
+ `RealtimeController.ts:869`).
438
444
 
439
445
  Quatre propriétés qui décident du bon réglage :
440
446
 
@@ -445,7 +451,7 @@ Quatre propriétés qui décident du bon réglage :
445
451
  - Un réabonnement à un canal **déjà tenu** ne consomme pas de place — l'idempotence est vérifiée
446
452
  avant le plafond. Un client qui redemande son canal n'est pas puni.
447
453
  - La garde existe **même sans configuration** : le hub porte le même défaut de 256
448
- (`RealtimeHub.ts:217`), il n'y a pas de fenêtre où elle serait absente.
454
+ (`RealtimeHub.ts:218`), il n'y a pas de fenêtre où elle serait absente.
449
455
 
450
456
  `null` retire la borne. C'est un retrait explicite, à réserver aux déploiements dont tu maîtrises
451
457
  les clients — un tableau de bord interne qui compose des dizaines de flux, par exemple.
@@ -457,7 +463,7 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
457
463
  ### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
458
464
 
459
465
  Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
460
- (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:986`). Une origine refusée ferme la socket avec le code
466
+ (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:967`). Une origine refusée ferme la socket avec le code
461
467
  `4003`.
462
468
 
463
469
  Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
@@ -522,7 +528,7 @@ use("@nodefony/realtime", { backplane: { driver: "cluster" } });
522
528
  **Ce qui se passe.** Un worker Node ne peut parler qu'au maître. Il lui envoie ses publications, et le
523
529
  maître les redistribue aux autres workers. Le driver ne s'active qu'en **rôle worker** et avec la
524
530
  variable `NF_CLUSTER=1`, posée par la commande de cluster
525
- (`src/packages/@nodefony/realtime/index.ts:99`) ; ailleurs, il rend `null` et le hub reste local.
531
+ (`src/packages/@nodefony/realtime/index.ts:106`) ; ailleurs, il rend `null` et le hub reste local.
526
532
 
527
533
  **Pourquoi c'est précieux.** C'est le banc d'essai qui stabilise l'architecture multi-processus avant
528
534
  d'ajouter du réseau. Ton contrôleur, tes canaux diffusables et ton client voient un vrai fan-out
@@ -547,7 +553,7 @@ use("@nodefony/realtime", {
547
553
 
548
554
  **Comment il se branche.** Le driver ne dépend pas de la bibliothèque `redis` : il consomme deux
549
555
  connexions du module `@nodefony/redis` par un adaptateur purement structurel
550
- (`createRedisServiceTransport()`, `RedisBackplane.ts:100`). Deux et non une, parce qu'un client Redis
556
+ (`createRedisServiceTransport()`, `RedisBackplane.ts:137`). Deux et non une, parce qu'un client Redis
551
557
  abonné ne peut plus émettre de commandes ordinaires — le module fournit précisément des connexions
552
558
  nommées `publish` et `subscribe` dans ses défauts.
553
559
 
@@ -557,7 +563,7 @@ coupure. C'est acceptable pour un salon de discussion — le message est en base
557
563
  un bus d'événements critiques. Ne construis pas au-dessus une fiabilité que le support n'offre pas.
558
564
 
559
565
  **Si Redis est injoignable.** Le démarrage du backplane est attendu **explicitement**, et **borné à
560
- cinq secondes** (`src/packages/@nodefony/realtime/index.ts:83`). Au-delà, ou en cas d'échec, le
566
+ cinq secondes** (`src/packages/@nodefony/realtime/index.ts:90`). Au-delà, ou en cas d'échec, le
561
567
  module renonce, ferme proprement ce qu'il avait ouvert, et laisse le hub local — le démarrage
562
568
  continue. Une base de messages en panne ne doit pas empêcher tes serveurs HTTP de monter.
563
569
 
@@ -645,8 +651,8 @@ présente court-circuite la sélection par nom.
645
651
  > Le builder accepte aussi une instance en second argument (`defineRealtimeConfig(config, {
646
652
  backplane })`), et la documentation d'architecture présente cette voie. En pratique elle n'est pas
647
653
  > 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
654
+ > fusionnée (`src/packages/@nodefony/realtime/index.ts:230`), et le schéma Zod est strict : une clé
655
+ > inconnue, dont une instance de classe posée dans `backplane`, fait échouer le boot. Pour brancher un objet déjà construit, utilise
650
656
  > le service `realtimeBackplane`.
651
657
 
652
658
  Le détail du contrat, de l'anti-écho et du cycle de vie d'un driver est dans
@@ -660,7 +666,7 @@ processus se raccorde — et c'est pour cela qu'elles ne vivent pas dans un fich
660
666
 
661
667
  | Variable | Portée | Effet |
662
668
  | --------------------------------- | ---------------------- | -------------------------------------------------------------------------- |
663
- | `NF_REALTIME_DRIVER` | ce module | remplace `backplane.driver`. **Précédence maximale** |
669
+ | `NF_REALTIME_DRIVER` | ce module | remplace `backplane.driver` — nom libre, un pilote inconnu est signalé |
664
670
  | `NF_REALTIME_BACKPLANE_NAMESPACE` | ce module | remplace `backplane.namespace` — sépare deux déploiements de la même app |
665
671
  | `NF_REALTIME_BACKPLANE_SECRET` | ce module | remplace `backplane.secret` — scelle le transport partagé (≥ 32 car.) |
666
672
  | `NF__REALTIME__<CHEMIN>` | mécanisme du cœur | remplace n'importe quelle clé, par son chemin. Ex. `NF__REALTIME__ENABLED` |
@@ -672,13 +678,15 @@ L'ordre de recouvrement, du plus faible au plus fort :
672
678
 
673
679
  1. **Les valeurs d'usine du schéma** — ce que tu obtiens sans rien écrire.
674
680
  2. **La configuration de l'application** — ton `use("@nodefony/realtime", { … })`.
675
- 3. **`NF__REALTIME__<CHEMIN>`** — appliqué après la fusion et **avant** la validation Zod, donc la
676
- valeur venue de l'environnement est validée comme les autres. Un chemin introuvable produit un
677
- avertissement avec une suggestion, jamais une clé fantôme.
678
- 4. **`NF_REALTIME_DRIVER`**, **`NF_REALTIME_BACKPLANE_NAMESPACE`** et
679
- **`NF_REALTIME_BACKPLANE_SECRET`** — appliqués **après** l'analyse, dans le builder
680
- (`defineModuleConfig.ts:41`). Ils gagnent sur tout le reste, y compris sur le mécanisme
681
- générique `NF__REALTIME__BACKPLANE__…`.
681
+ 3. **`NF_REALTIME_DRIVER`**, **`NF_REALTIME_BACKPLANE_NAMESPACE`** et
682
+ **`NF_REALTIME_BACKPLANE_SECRET`** — posées par le Kernel après la fusion et **avant** la
683
+ validation Zod (`applyEnvShortcuts`, `Kernel.ts:1989`) : une valeur fausse **arrête le démarrage** en nommant la
684
+ variable (un secret de moins de 32 caractères, un espace de noms qui contient `:`). Chacune
685
+ accepte aussi sa forme `<NOM>_FILE` (secret monté par Docker ou Kubernetes).
686
+ 4. **`NF__REALTIME__<CHEMIN>`** — même moment, même validation. Un chemin introuvable produit un
687
+ avertissement avec une suggestion, jamais une clé fantôme. Viser le **même** champ qu'une des
688
+ trois variables ci-dessus est une ambiguïté : refusée en production, et en développement
689
+ `NF__…` l'emporte avec un avertissement qui nomme les deux.
682
690
 
683
691
  Le mécanisme générique mérite d'être connu : le double tiret bas sépare les niveaux, les segments
684
692
  sont insensibles à la casse, et les valeurs sont converties (booléens, nombres, listes séparées par
@@ -695,17 +703,19 @@ NF_CLUSTER_PROBE=0 # couper la sonde de pod sur une réplique en incident
695
703
 
696
704
  > [!TIP]
697
705
  > `NF_REALTIME_DRIVER` existe **en plus** du mécanisme générique parce qu'il est la variable qu'on
698
- > pose en premier sur un déploiement, et qu'elle doit rester fiable quel que soit le moment où la
699
- > configuration de l'application fusionne. C'est le levier d'urgence : ramener un cluster à
700
- > `loopback` pour isoler une panne de bus se fait par une variable, sans redéployer de code.
706
+ > pose en premier sur un déploiement : son nom ne dépend pas de la structure interne de la
707
+ > configuration. C'est le levier d'urgence : ramener un cluster à `loopback` pour isoler une panne
708
+ > de bus se fait par une variable, sans redéployer de code. `npx nodefony env` la liste avec sa
709
+ > valeur et sa provenance.
701
710
 
702
711
  ## 🧰 Le builder et le schéma exportable
703
712
 
704
713
  Deux fonctions publiques, utiles surtout aux tests et aux outils.
705
714
 
706
- `defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:35`) analyse, applique la variable
707
- de driver et **gèle**. Le module l'appelle lui-même à l'enregistrement
708
- (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:213`) : tu n'as pas à
715
+ `defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:39`) analyse et **gèle**. Appelé
716
+ hors Kernel (un test, un outil), il ne voit pas les variables d'environnement : c'est le Kernel
717
+ qui les pose. Le module l'appelle lui-même à l'enregistrement
718
+ (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:221`) : tu n'as pas à
709
719
  l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
710
720
  sans démarrer un serveur.
711
721
 
@@ -714,10 +724,10 @@ import { defineRealtimeConfig } from "@nodefony/realtime";
714
724
 
715
725
  const cfg = defineRealtimeConfig({ backplane: { driver: "redis" } });
716
726
  // 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.
727
+ // Une valeur invalide ou une clé inconnue lève une BootConfigurationError, avec le chemin exact du champ fautif.
718
728
  ```
719
729
 
720
- `realtimeConfigJsonSchema()` (`defineModuleConfig.ts:69`) produit le schéma JSON du module, chaque
730
+ `realtimeConfigJsonSchema()` (`defineModuleConfig.ts:99`) produit le schéma JSON du module, chaque
721
731
  champ portant sa description. C'est ce que consomme la page module de Studio pour afficher la
722
732
  configuration attendue. L'instance de backplane éventuelle en est absente : une classe n'a rien à
723
733
  faire dans un schéma sérialisable.
@@ -748,16 +758,16 @@ Trois endroits, par ordre de fiabilité décroissante.
748
758
 
749
759
  ## ⚠️ Pièges
750
760
 
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 |
761
+ | Symptôme | Cause | Correction |
762
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
763
+ | 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 |
764
+ | 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 |
765
+ | 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 |
766
+ | 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 |
767
+ | Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde | régler `websocketSecure.maxBackpressure` / `.backpressureCloseAfterDrops` (@nodefony/http) |
768
+ | Toutes les connexions tombent en `4003` après activation du contrôle d'origine | `allowList` vide, ou origine non identique au caractère près (port, schéma, sous-domaine) | inscrire l'origine exacte ; un client non-navigateur relève de `allowMissingOrigin` |
769
+ | `enabled: false` posé « pour désactiver », et la garde d'origine ne s'applique plus | le service devient inerte et ne pose plus les politiques sur le hub | retirer le module du manifeste plutôt que l'éteindre |
770
+ | Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts:218`) | vérifier le motif de refus `realtime:denied` côté client |
761
771
 
762
772
  ## 🧪 Tests & couverture
763
773
 
@@ -778,12 +788,12 @@ qui compte ici, c'est **ce que les suites prouvent sur la configuration**.
778
788
  > **Un run vert ne prouve pas le fan-out entre machines.** Le banc Redis est doublement conditionnel :
779
789
  > il n'est lancé que sur demande, et il se **saute** de lui-même si aucun Redis ne répond. Un test
780
790
  > 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.
791
+ > 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
792
  >
783
793
  > ```bash
784
794
  > 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
795
+ > npm test # unitaires et intégration
796
+ > NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm run test:cluster # IPC entre workers + fan-out entre machines
787
797
  > npm run coverage # couverture (vitest, v8)
788
798
  > ```
789
799
 
@@ -353,7 +353,7 @@ gaspillerait une socket par écran.
353
353
 
354
354
  ```ts
355
355
  // frontend/src/realtime.ts
356
- import { RealtimeClient } from "nodefony/client";
356
+ import { NodefonySocket } from "nodefony/client";
357
357
 
358
358
  // `wss` si la page est en HTTPS, `ws` sinon. Une application Nodefony sert en
359
359
  // HTTPS par défaut, y compris en développement : ce sera donc `wss` chez toi.
@@ -366,7 +366,7 @@ const scheme = window.location.protocol === "https:" ? "wss" : "ws";
366
366
  * La reconnexion automatique, le renvoi des abonnements après une coupure et le
367
367
  * heartbeat sont inclus — rien à écrire pour ça.
368
368
  */
369
- export const chatSocket = new RealtimeClient({
369
+ export const chatSocket = new NodefonySocket({
370
370
  url: `${scheme}://${window.location.host}/chat/realtime`,
371
371
  });
372
372
  ```
@@ -445,7 +445,9 @@ import { ChatRoom } from "./ChatRoom";
445
445
 
446
446
  // La connexion s'ouvre UNE fois, au démarrage de l'application. Les hooks ne
447
447
  // gèrent que les abonnements : le cycle de vie de la socket reste à toi.
448
- void chatSocket.connect();
448
+ // `connect()` rejette si cette première tentative échoue (serveur absent) ;
449
+ // la socket se reconnecte seule ensuite, et l'état se lit par les hooks.
450
+ chatSocket.connect().catch(() => {});
449
451
 
450
452
  createRoot(document.getElementById("app")!).render(
451
453
  <NodefonyProvider client={chatSocket}>
@@ -847,6 +849,7 @@ fonctionnent :
847
849
  // modules/chat/nodefony/controllers/ChatController.ts
848
850
  // → remplace `@RealtimeInbound("chat:send")` par une ACTION
849
851
  import { Body, CurrentUser, IsGranted } from "@nodefony/framework";
852
+ import type { IUser } from "@nodefony/user";
850
853
 
851
854
  /**
852
855
  * L'envoi devient une action, joignable en POST HTTP comme par la socket.
@@ -860,7 +863,7 @@ import { Body, CurrentUser, IsGranted } from "@nodefony/framework";
860
863
  @IsGranted(["ROLE_USER"])
861
864
  async send(
862
865
  @Body() payload: { text?: string },
863
- @CurrentUser() user?: { identifier?: string },
866
+ @CurrentUser() user: IUser,
864
867
  ) {
865
868
  const text = typeof payload?.text === "string" ? payload.text.trim() : "";
866
869
  if (text.length === 0 || text.length > 2000) {
@@ -869,7 +872,7 @@ async send(
869
872
 
870
873
  // L'auteur vient du SERVEUR. Le client ne peut plus mentir dessus.
871
874
  const message: ChatMessage = {
872
- author: user?.identifier ?? "inconnu",
875
+ author: user.identifier,
873
876
  text,
874
877
  sentAt: Date.now(),
875
878
  };
@@ -1218,6 +1221,7 @@ import {
1218
1221
  import type { RealtimePublish } from "@nodefony/realtime";
1219
1222
  import { HttpError } from "@nodefony/http";
1220
1223
  import type { ContextType, IUploadedFile } from "@nodefony/http";
1224
+ import type { IUser } from "@nodefony/user";
1221
1225
 
1222
1226
  /** Un message tel qu'il circule sur le canal du salon. */
1223
1227
  export interface ChatMessage {
@@ -1288,14 +1292,14 @@ class ChatController extends RealtimeController {
1288
1292
  @IsGranted(["ROLE_USER"])
1289
1293
  async send(
1290
1294
  @Body() payload: { text?: string },
1291
- @CurrentUser() user?: { identifier?: string },
1295
+ @CurrentUser() user: IUser,
1292
1296
  ): Promise<{ ok: true; sentAt: number }> {
1293
1297
  const text = typeof payload?.text === "string" ? payload.text.trim() : "";
1294
1298
  if (text.length === 0 || text.length > 2000) {
1295
1299
  throw new HttpError("message vide ou trop long", 400);
1296
1300
  }
1297
1301
  const message: ChatMessage = {
1298
- author: user?.identifier ?? "inconnu",
1302
+ author: user.identifier,
1299
1303
  text,
1300
1304
  sentAt: Date.now(),
1301
1305
  };
@@ -1312,13 +1316,13 @@ class ChatController extends RealtimeController {
1312
1316
  @IsGranted(["ROLE_USER"])
1313
1317
  async upload(
1314
1318
  @UploadedFile() file?: IUploadedFile,
1315
- @CurrentUser() user?: { identifier?: string },
1319
+ @CurrentUser() user: IUser,
1316
1320
  ): Promise<{ ok: true; filename: string; size: number }> {
1317
1321
  if (!file) {
1318
1322
  throw new HttpError("aucun fichier reçu", 400);
1319
1323
  }
1320
1324
  const announcement: ChatAttachment = {
1321
- author: user?.identifier ?? "inconnu",
1325
+ author: user.identifier,
1322
1326
  filename: file.filename,
1323
1327
  size: file.size,
1324
1328
  mimeType: file.mimeType,
@@ -1496,7 +1500,7 @@ pour lire ta propre sortie :
1496
1500
  | `backpressure.maxBufferedAmount` | La **pire** file d'envoi observée à l'instant de la mesure. C'est l'indicateur d'alerte. |
1497
1501
  | `backpressure.slowConsumers` | Combien de connexions dépassent le seuil de consommateur lent. |
1498
1502
  | `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. |
1503
+ | `backplane` | Toujours présent : carte d'identité du relais (driver, nature, `crossPod`) ; sans relais, descripteur `local` / `loopback`. |
1500
1504
 
1501
1505
  Deux lectures suffisent à diagnostiquer :
1502
1506
 
@@ -1546,12 +1550,14 @@ Ce qui est couvert, par nature de test :
1546
1550
  - **Unitaires** — le hub (abonnement, fan-out, cycle de vie des providers), les décorateurs et leurs
1547
1551
  métadonnées, le registre de drivers de backplane, le contrôleur, le service, le transport
1548
1552
  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).
1553
+ - **Tests d'attaque** (`*.attack.test.ts`) — quatre scénarios verrouillés en non-régression : le
1554
+ plafond de canaux par connexion, la révocation d'une session en cours de connexion, le cas
1555
+ d'une politique de canal déclarée **sans** gardien branché (la dégradation doit rester bruyante),
1556
+ et l'injection par le bus (un pair ne pousse pas sur un canal non diffusable, un message forgé sur
1557
+ un bus scellé est ignoré).
1552
1558
  - **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.
1559
+ chemins de contrôleur, l'autorisation de canal, le branchement du firewall. Le cluster IPC, avec
1560
+ de vrais processus enfants, se lance à part : `npm run test:cluster` avec `NF_RUN_CLUSTER_E2E=1`.
1555
1561
  - **Cluster Redis** — exercé de bout en bout, mais **conditionné à la présence d'un Redis**. Sans
1556
1562
  lui, ces cas sont ignorés, et un test ignoré compte comme réussi : ne conclus pas d'un « tout est
1557
1563
  vert » que le chemin Redis a été vérifié chez toi.
@@ -1597,23 +1603,23 @@ Pour qui veut lire l'implémentation derrière chaque étape :
1597
1603
  <!-- prettier-ignore -->
1598
1604
  | Ce que tu as utilisé | Où c'est écrit |
1599
1605
  | --- | --- |
1600
- | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:192`), `RealtimeInbound()` (`realtimeDecorators.ts:231`) |
1601
- | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:299`) |
1602
- | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:276`) |
1603
- | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:462`), `RealtimeController.startChannel()` (`RealtimeController.ts:815`) |
1604
- | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:623`) |
1605
- | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:668`) |
1606
- | 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:1143`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1117`) |
1608
- | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:279`) |
1609
- | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:417`), fermetures `origin not allowed` (`RealtimeController.ts:327`) et `unauthorized` (`RealtimeController.ts:397`) |
1610
- | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:826`) |
1611
- | 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:1000`) |
1606
+ | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:191`), `RealtimeChannel()` (`realtimeDecorators.ts:240`), `RealtimeInbound()` (`realtimeDecorators.ts:278`) |
1607
+ | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:344`) |
1608
+ | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:321`) |
1609
+ | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:464`), `RealtimeController.startChannel()` (`RealtimeController.ts:869`) |
1610
+ | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:610`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:629`) |
1611
+ | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:420`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:674`) |
1612
+ | Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:720`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
1613
+ | Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1124`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1098`) |
1614
+ | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:306`) |
1615
+ | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:449`), fermetures `origin not allowed` (`RealtimeController.ts:422`) et `unauthorized` (`RealtimeController.ts:444`) |
1616
+ | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:815`) |
1617
+ | Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:66`), fermeture `slow consumer` (`RealtimeHub.ts:64`) |
1618
+ | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:981`) |
1613
1619
  | L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
1614
- | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:865`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1620
+ | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:847`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1615
1621
  | Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
1616
- | 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:255`) |
1618
- | 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`) |
1622
+ | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:218`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
1623
+ | Les hooks React | `useNodefony()` (`client/react/index.ts:255`), `useNodefonyState()` (`client/react/index.ts:273`), `useNodefonyIdentity()` (`client/react/index.ts:292`), `useNodefonyChannel()` (`client/react/index.ts:308`), `useNodefonyChannelData()` (`client/react/index.ts:332`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts:367`) |
1624
+ | Le client navigateur | `NodefonySocket.connect()` (`NodefonySocket.ts:592`), `NodefonySocket.subscribe()` (`NodefonySocket.ts:739`), `NodefonySocket.request()` (`NodefonySocket.ts:936`), `NodefonySocket.onDenied()` (`NodefonySocket.ts:678`) |
1625
+ | L'upload | `UploadedFile` (`routerDecorators.ts:1311`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:215`) |