@nodefony/realtime 10.0.0-alpha.8 → 10.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -126,7 +126,7 @@ Nodefony étiquette avec **JSON-RPC 2.0**, une norme publique plutôt qu'un form
126
126
  Le point remarquable : le **même** moteur de protocole tourne des deux côtés du fil. La
127
127
  classe `JsonRpcPeer` (`JsonRpcPeer.ts:271`) est du code isomorphe du cœur — le navigateur
128
128
  l'exécute dans `RealtimeClient`, le serveur l'instancie une fois par connexion dans
129
- `RealtimeController.onHandshake()` (`RealtimeController.ts:328`). Le serveur peut donc
129
+ `RealtimeController.onHandshake()` (`RealtimeController.ts:374`). Le serveur peut donc
130
130
  appeler le client, pas seulement l'inverse : c'est du vrai duplex, pas un aller-retour
131
131
  déguisé.
132
132
 
@@ -141,11 +141,11 @@ Trois partis pris distinguent cette pile d'un simple « serveur WebSocket ».
141
141
  et explicite : `ServerRealtimeSocket.request()` (`ServerRealtimeSocket.ts:131`) **rejette
142
142
  toujours** — un handle posé sur le hub n'a pas d'interlocuteur unique, puisque le hub est
143
143
  multi-clients. Pour un appel serveur→client ciblé, on passe par la connexion :
144
- `RealtimeController.requestClient()` (`RealtimeController.ts:279`).
144
+ `RealtimeController.requestClient()` (`RealtimeController.ts:329`).
145
145
 
146
146
  **Un provider par canal, pas un par client.** Si mille onglets s'abonnent au même canal de
147
147
  santé, le calcul ne doit tourner qu'une fois. Le hub crée le producteur au **premier**
148
- abonné et le détruit au **dernier** (`RealtimeHub.subscribe()`, `RealtimeHub.ts:388`) : le
148
+ abonné et le détruit au **dernier** (`RealtimeHub.subscribe()`, `RealtimeHub.ts:462`) : le
149
149
  coût suit le nombre de canaux, pas le nombre de clients.
150
150
 
151
151
  **Le cluster est un détail de configuration.** Ni ton contrôleur ni ton client ne savent
@@ -325,7 +325,7 @@ La pile se lit de haut en bas ; chaque étage ne connaît que son voisin du dess
325
325
  | Protocole | `JsonRpcPeer` (`JsonRpcPeer.ts:271`) | étiqueter, corréler, refuser une frame | par où passent les octets |
326
326
  | Transport | `WsConnectionTransport` (`WsConnectionTransport.ts:46`) | déplacer des octets, mesurer la file | ce que veut dire un message |
327
327
  | Hub | `RealtimeHub` (`RealtimeHub.ts:213`) | table des abonnés locaux + fan-out | qu'il existe d'autres process |
328
- | Backplane | `IBackplane` (`IBackplane.ts:75`) | porter un message aux autres process | ce qu'est un abonné |
328
+ | Backplane | `IBackplane` (`IBackplane.ts:107`) | porter un message aux autres process | ce qu'est un abonné |
329
329
 
330
330
  > [!NOTE]
331
331
  > Une part de la documentation historique décrit cette pile en « 5 étages » en comptant
@@ -338,7 +338,7 @@ La pile se lit de haut en bas ; chaque étage ne connaît que son voisin du dess
338
338
  dispatch des notifications, gestion des erreurs, `dispose()` propre. Deux points
339
339
  d'interception y sont posés :
340
340
 
341
- - `beforeDispatch` (`JsonRpcPeer.ts:172`) — un verrou **synchrone** appelé avant tout
341
+ - `beforeDispatch` (`JsonRpcPeer.ts:192`) — un verrou **synchrone** appelé avant tout
342
342
  traitement de frame. Il rend `true`/`false`. Un refus sur une requête produit
343
343
  `-32001 unauthorized` ; sur une notification, la frame est jetée
344
344
  (`JsonRpcPeer.ts:413`).
@@ -355,14 +355,14 @@ au handshake, puis lue en O(1). C'est le compromis assumé, documenté dans
355
355
  Particularité contre-intuitive : côté serveur, le transport **n'écoute pas** d'événement
356
356
  socket. C'est le pipeline HTTP de `@nodefony/http` qui appelle la route WebSocket du
357
357
  contrôleur à chaque message reçu, laquelle pousse la charge dans
358
- `WsConnectionTransport.feed()` (`WsConnectionTransport.ts:146`). La fermeture arrive de la
358
+ `WsConnectionTransport.feed()` (`WsConnectionTransport.ts:150`). La fermeture arrive de la
359
359
  même façon, par le hook `onFinish` du contexte, qui déclenche `fireClose()`
360
- (`WsConnectionTransport.ts:151`).
360
+ (`WsConnectionTransport.ts:155`).
361
361
 
362
362
  Ce transport porte aussi la **back-pressure**. Elle n'est pas câblée en dur : les trois
363
363
  leviers sont des clés de configuration du serveur WebSocket, lues par le transport
364
364
  (`WsConnectionTransport.ts:63-75`) et documentées dans `@nodefony/http`
365
- (`http/nodefony/config/config.ts:625`).
365
+ (`http/nodefony/config/config.ts:1044`).
366
366
 
367
367
  | File non drainée (`bufferedAmount`) | Décision | Réglage (défaut) |
368
368
  | --------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- |
@@ -388,7 +388,7 @@ Le schéma ci-dessous rend ces étages vivants : active le temps réel et il res
388
388
 
389
389
  ## 🔌 Le cycle de vie d'une connexion
390
390
 
391
- Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:328`), appelé
391
+ Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:374`), appelé
392
392
  une seule fois par connexion, en chemin froid.
393
393
 
394
394
  ```mermaid
@@ -419,19 +419,19 @@ sequenceDiagram
419
419
 
420
420
  Les étapes, dans l'ordre exact du code :
421
421
 
422
- 1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:970`). Refus →
422
+ 1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:986`). Refus →
423
423
  fermeture `4003`. La politique vient de la configuration ; sans politique, tout passe.
424
424
  2. **Résolution de l'identité** — `RealtimeHub.resolveAuthenticator()`
425
- (`RealtimeHub.ts:945`) parcourt les authentificateurs enregistrés : **le premier motif
425
+ (`RealtimeHub.ts:961`) parcourt les authentificateurs enregistrés : **le premier motif
426
426
  qui correspond capture**. Aucun ne correspond ? Le jeton anonyme gelé est posé
427
427
  (`ANONYMOUS_REALTIME_TOKEN`) — la lecture d'identité ne rend donc jamais `null`. Un
428
428
  échec d'authentification ferme en `4001`.
429
429
  3. **Création du peer et du transport**, puis association `peer → jeton`
430
- (`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:1002`), stockée dans une `WeakMap` : le
430
+ (`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:1018`), stockée dans une `WeakMap` : le
431
431
  jeton disparaît avec le peer, sans fuite.
432
432
  4. **Enregistrement des actions** — celles des décorateurs `@RealtimeAction`, puis celles
433
433
  de la surcharge `realtimeActions()`, qui gagne en cas de conflit. Le pont API
434
- `api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts:219`)
434
+ `api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts:276`)
435
435
  rend `true`.
436
436
  5. **Inscription aux registres** : sonde de connexion, révocation périodique si le jeton
437
437
  est révocable, préfixes de canaux broadcast, canaux entrants.
@@ -439,7 +439,7 @@ Les étapes, dans l'ordre exact du code :
439
439
  l'identité résolue (type, authentifié ou non, rôles, portées). Le client sait **qui il
440
440
  est** sans appeler la moindre route.
441
441
 
442
- À la fermeture, un unique `onFinish` (`RealtimeController.ts:542`) fait le ménage complet :
442
+ À la fermeture, un unique `onFinish` (`RealtimeController.ts:715`) fait le ménage complet :
443
443
  désabonnement de chaque canal tenu, retrait des deux registres, `fireClose()` du transport,
444
444
  `dispose()` du peer. C'est ce qui garantit qu'aucun minuteur ni écouteur ne survit à une
445
445
  déconnexion.
@@ -459,26 +459,26 @@ déconnexion.
459
459
  La seconde mérite une explication. Le verrou de frame est synchrone et lit une identité
460
460
  figée au handshake : il ne peut donc pas voir une session qui meurt en cours de route (une
461
461
  déconnexion HTTP, par exemple). Un minuteur — démarré au premier inscrit, arrêté dès que le
462
- registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:778`) — relit
462
+ registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:794`) — relit
463
463
  périodiquement ces identités et coupe les sockets orphelines. Seules les identités
464
464
  **révocables** y entrent : un visiteur anonyme ne coûte rien.
465
465
 
466
466
  ## Le hub — canaux partagés et fan-out
467
467
 
468
- Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1435`). Il ne
468
+ Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1451`). Il ne
469
469
  dépend de rien : ce sont les fabriques fournies par les contrôleurs qui portent les
470
470
  dépendances.
471
471
 
472
472
  ### Le provider partagé — un ticker, pas mille
473
473
 
474
- `RealtimeHub.subscribe()` (`RealtimeHub.ts:388`) applique une mécanique en trois temps :
474
+ `RealtimeHub.subscribe()` (`RealtimeHub.ts:462`) applique une mécanique en trois temps :
475
475
 
476
476
  1. Le canal existe déjà ? On ajoute simplement le sink de cette connexion. Fin.
477
477
  2. Sinon, on inscrit le sink **avant** d'appeler la fabrique — de sorte que le tout premier
478
478
  paquet du producteur atteigne bien ce premier abonné.
479
479
  3. La fabrique du contrôleur rend `null` (canal inconnu de lui) ? Dernier recours : le
480
480
  registre des **canaux système** (`RealtimeHub.registerSystemChannel()`,
481
- `RealtimeHub.ts:1281`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
481
+ `RealtimeHub.ts:1297`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
482
482
  l'abonnement est refusé et rien n'est alloué.
483
483
 
484
484
  Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts:576`) appelle le
@@ -511,7 +511,7 @@ une connexion fautive n'interrompt pas la diffusion aux autres.
511
511
 
512
512
  Par défaut, **aucun canal ne traverse le backplane**. Il faut déclarer un préfixe, via
513
513
  `@RealtimeBroadcast` sur ton contrôleur (`realtimeDecorators.ts:342`) ou
514
- directement `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:594`).
514
+ directement `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:668`).
515
515
 
516
516
  Trois raisons à ce choix, qui prend à contre-pied la plupart des bibliothèques temps réel :
517
517
 
@@ -547,17 +547,17 @@ coûte chaque abonné supplémentaire.
547
547
 
548
548
  ### Le contrat, en cinq méthodes
549
549
 
550
- `IBackplane` (`IBackplane.ts:75`) est délibérément minuscule : le backplane ne connaît ni
550
+ `IBackplane` (`IBackplane.ts:107`) est délibérément minuscule : le backplane ne connaît ni
551
551
  les abonnés ni les canaux logiques. Tout l'état vit dans le hub.
552
552
 
553
- | Membre | Rôle |
554
- | ------------------------------------------------ | --------------------------------------------------------------- |
555
- | `originId` (`IBackplane.ts:77`) | l'étiquette de CE process, lue par l'anti-écho |
556
- | `start()` (`IBackplane.ts:115`) | ouvrir le transport. Idempotent, synchrone ou asynchrone |
557
- | `publish(channel, payload)` (`IBackplane.ts:89`) | propager aux **autres** pairs. Ne refait pas le fan-out local |
558
- | `onMessage(handler)` (`IBackplane.ts:127`) | recevoir des pairs. Un seul gestionnaire à la fois |
559
- | `stop()` (`IBackplane.ts:130`) | libérer connexions et écouteurs. Idempotent |
560
- | `describe()` (`IBackplane.ts:136`) | la carte d'identité : driver, nature, origine, cross-pod, canal |
553
+ | Membre | Rôle |
554
+ | ------------------------------------------------- | --------------------------------------------------------------- |
555
+ | `originId` (`IBackplane.ts:109`) | l'étiquette de CE process, lue par l'anti-écho |
556
+ | `start()` (`IBackplane.ts:115`) | ouvrir le transport. Idempotent, synchrone ou asynchrone |
557
+ | `publish(channel, payload)` (`IBackplane.ts:121`) | propager aux **autres** pairs. Ne refait pas le fan-out local |
558
+ | `onMessage(handler)` (`IBackplane.ts:127`) | recevoir des pairs. Un seul gestionnaire à la fois |
559
+ | `stop()` (`IBackplane.ts:130`) | libérer connexions et écouteurs. Idempotent |
560
+ | `describe()` (`IBackplane.ts:136`) | la carte d'identité : driver, nature, origine, cross-pod, canal |
561
561
 
562
562
  Deux absences volontaires. Il n'y a **pas** de `subscribe`/`unsubscribe` par canal : le
563
563
  transport porte **un seul** canal physique, et les canaux logiques voyagent **dans**
@@ -620,7 +620,7 @@ et non une :
620
620
 
621
621
  1. **Côté hub** : l'arrivée passe par `publishLocal`, jamais par `publish`. Rien ne repart.
622
622
  2. **Côté backplane** : à la réception, on compare l'`originId` de l'enveloppe au sien et
623
- on jette si c'est le même (`RedisBackplane.ts:220`, `ClusterBackplane.ts:134`).
623
+ on jette si c'est le même (`RedisBackplane.ts:209`, `ClusterBackplane.ts:134`).
624
624
 
625
625
  L'étiquette elle-même est calculée par `resolveBackplaneOriginId()` (`originId.ts:24`), et
626
626
  sa recette mérite qu'on s'y arrête :
@@ -661,7 +661,7 @@ est ignoré, pas propagé (`RedisBackplane.ts:212`).
661
661
  ### Le démarrage, et ce qui se passe quand ça rate
662
662
 
663
663
  Le module valide sa configuration par Zod à l'enregistrement
664
- (`src/packages/@nodefony/realtime/index.ts:181`), puis câble tout au démarrage du noyau
664
+ (`src/packages/@nodefony/realtime/index.ts:219`), puis câble tout au démarrage du noyau
665
665
  (`src/packages/@nodefony/realtime/index.ts:213`) — après l'initialisation des services (le
666
666
  service Redis a donc ouvert ses connexions) et avant le trafic.
667
667
 
@@ -743,19 +743,19 @@ Deux propriétés architecturales méritent d'être notées ici, parce qu'elles
743
743
  choix de conception visibles partout dans le module :
744
744
 
745
745
  **Coût nul quand la sécurité est absente.** Le verrou de frame n'est branché sur le peer que
746
- si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:1042`, testé une
746
+ si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:1058`, testé une
747
747
  fois au handshake). Sans module de sécurité, `beforeDispatch` reste indéfini et le chemin
748
748
  chaud ne paie **rien** du tout.
749
749
 
750
750
  **Échec bruyant plutôt que faux sentiment de sécurité.** Si des canaux déclarent une
751
751
  politique sans qu'aucun décideur ne soit câblé, `hasUnenforcedChannelPolicies()`
752
- (`RealtimeHub.ts:1101`) le détecte et un avertissement est émis une fois par process
752
+ (`RealtimeHub.ts:1117`) le détecte et un avertissement est émis une fois par process
753
753
  (`RealtimeController.ts:487`). Un canal qui **se croit** gardé alors qu'il est ouvert est
754
754
  bien plus dangereux qu'un canal ouvertement public.
755
755
 
756
756
  ## 📡 Observabilité — la sonde et Studio
757
757
 
758
- `RealtimeHub.probe()` (`RealtimeHub.ts:775`) rend un instantané en **lecture pure** : aucune
758
+ `RealtimeHub.probe()` (`RealtimeHub.ts:865`) rend un instantané en **lecture pure** : aucune
759
759
  allocation sur le chemin chaud, jamais d'exception. Les compteurs sont des primitives
760
760
  incrémentées en O(1) ; ils sont **monotones**, ce qui laisse au lecteur le soin de dériver
761
761
  un débit.
@@ -774,7 +774,7 @@ process, plus — si elles sont disponibles — les sondes ORM et les compteurs
774
774
  chemins de lecture, un seul producteur :
775
775
 
776
776
  - **HTTP** : `GET /nodefony/realtime/api/health`, exposé par `createRealtimeAdminApi()`
777
- (`RealtimeAdminApi.ts:91`) ;
777
+ (`RealtimeAdminApi.ts:98`) ;
778
778
  - **Canal temps réel** : `nodefony:socket`, poussé par un ticker.
779
779
 
780
780
  ### En cluster : la vue « pod » plutôt que « worker »
@@ -808,7 +808,7 @@ révocables, authentificateurs, politiques de canal, association peer→jeton :
808
808
  `null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts:141` et suivants).
809
809
 
810
810
  **Aucun minuteur au repos.** Le tick de révocation démarre au premier inscrit et s'arrête
811
- dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:793`).
811
+ dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:809`).
812
812
  Les minuteurs sont détachés de la boucle d'événements : ils ne retiennent jamais l'arrêt du
813
813
  process.
814
814
 
@@ -125,7 +125,7 @@ fichiers, jamais trois**.
125
125
  | `nodefony/config/config.ts` | **le quoi** | le schéma Zod commenté, source **unique** des valeurs d'usine, et les défauts matérialisés |
126
126
  | `nodefony/config/defineModuleConfig.ts` | **le comment** | le builder pur : analyse, surcharge d'environnement, gel — il ne retape jamais une valeur d'usine |
127
127
 
128
- Le schéma `realtimeConfigSchema` (`config.ts:205`) porte chaque valeur en `.default()` et chaque
128
+ Le schéma `realtimeConfigSchema` (`config.ts:259`) porte chaque valeur en `.default()` et chaque
129
129
  explication en `.describe()`. Les défauts effectifs sont **dérivés** du schéma lui-même
130
130
  (`realtimeConfigSchema.parse({})`, `config.ts:231`) : il n'existe pas de second endroit où une valeur
131
131
  d'usine serait écrite, donc pas de dérive silencieuse possible entre la doc du champ et son
@@ -252,7 +252,7 @@ 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:253`) et de la carte d'identité que chaque driver publie.
255
+ > `src/packages/@nodefony/realtime/index.ts:279`) 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é
@@ -283,7 +283,7 @@ valeurs sont des constantes du code, pas des clés.
283
283
 
284
284
  Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
285
285
  enregistrée, pas de backplane câblé, pas de sonde de pod
286
- (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:213`), et le service devient
286
+ (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:239`), et le service devient
287
287
  inerte (`RealtimeService.init()`, `RealtimeService.ts:77`).
288
288
 
289
289
  > [!WARNING]
@@ -344,7 +344,7 @@ canal exotique.
344
344
  Ne concerne, là aussi, que les transports **partagés**. Un bus pub/sub ne dit pas _qui_ a publié :
345
345
  sans secret, tout ce qui atterrit sur le canal — venu d'un pod ou d'un tiers qui sait écrire dans ce
346
346
  Redis — est traité comme un message légitime. Poser un secret fait porter à chaque message un sceau
347
- HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts:96`) : un message non
347
+ HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts:112`) : un message non
348
348
  scellé, altéré ou repointé vers un autre canal est **ignoré**, sans exception possible.
349
349
 
350
350
  Le secret doit être **identique sur tous les pods** — c'est lui qui les reconnaît entre eux. En
@@ -378,7 +378,7 @@ d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
378
378
  ### `slowConsumer.bytes` — un compteur, pas un frein
379
379
 
380
380
  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:775`).
381
+ compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
382
382
 
383
383
  > [!IMPORTANT]
384
384
  > **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
@@ -434,7 +434,7 @@ des canaux d'état, où seul le dernier instantané compte.
434
434
  Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
435
435
  côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
436
436
  processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
437
- `RealtimeController.ts:706`).
437
+ `RealtimeController.ts:815`).
438
438
 
439
439
  Quatre propriétés qui décident du bon réglage :
440
440
 
@@ -457,7 +457,7 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
457
457
  ### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
458
458
 
459
459
  Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
460
- (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:970`). Une origine refusée ferme la socket avec le code
460
+ (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:986`). Une origine refusée ferme la socket avec le code
461
461
  `4003`.
462
462
 
463
463
  Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
@@ -705,7 +705,7 @@ Deux fonctions publiques, utiles surtout aux tests et aux outils.
705
705
 
706
706
  `defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:35`) analyse, applique la variable
707
707
  de driver et **gèle**. Le module l'appelle lui-même à l'enregistrement
708
- (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:219`) : tu n'as pas à
708
+ (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:213`) : tu n'as pas à
709
709
  l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
710
710
  sans démarrer un serveur.
711
711
 
@@ -722,9 +722,10 @@ champ portant sa description. C'est ce que consomme la page module de Studio pou
722
722
  configuration attendue. L'instance de backplane éventuelle en est absente : une classe n'a rien à
723
723
  faire dans un schéma sérialisable.
724
724
 
725
- Un message d'erreur de validation est reformaté par le module avant d'être levé : chaque problème
726
- apparaît sous la forme `chemin: message`, séparés par des points médians
727
- (`src/packages/@nodefony/realtime/index.ts:219`). Tu lis quel champ est fautif, pas une trace Zod
725
+ Un message d'erreur de validation est reformaté par le cœur avant d'être levé (`parseModuleConfig`,
726
+ appelé par `defineRealtimeConfig`) : chaque problème apparaît sous la forme `chemin : message`
727
+ (`src/nodefony/src/kernel/moduleConfig.ts:42`), séparés par des points médians
728
+ (`src/nodefony/src/kernel/moduleConfig.ts:58`). Tu lis quel champ est fautif, pas une trace Zod
728
729
  brute.
729
730
 
730
731
  ## 📡 Observabilité — relire ce qui s'applique vraiment
@@ -134,7 +134,7 @@ le moment venu.
134
134
 
135
135
  ```bash
136
136
  # 1. Créer l'application (profil complet : http, framework, realtime, sécurité, frontend)
137
- npx nodefony create app mon-chat --complete
137
+ npx nodefony create app mon-chat --preset complete
138
138
  cd mon-chat
139
139
 
140
140
  # 2. Créer le module qui portera le salon
@@ -1597,23 +1597,23 @@ Pour qui veut lire l'implémentation derrière chaque étape :
1597
1597
  <!-- prettier-ignore -->
1598
1598
  | Ce que tu as utilisé | Où c'est écrit |
1599
1599
  | --- | --- |
1600
- | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:142`), `RealtimeInbound()` (`realtimeDecorators.ts:182`) |
1601
- | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:249`) |
1602
- | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:219`) |
1603
- | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:388`), `RealtimeController.startChannel()` (`RealtimeController.ts:746`) |
1604
- | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:549`) |
1605
- | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:594`) |
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
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:1037`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1037`) |
1608
- | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:253`) |
1609
- | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:228`), erreur `unauthorized` (`JsonRpcPeer.ts:400`), fermetures `origin not allowed` (`RealtimeController.ts:327`) et `unauthorized` (`RealtimeController.ts:349`) |
1610
- | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:810`) |
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
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:984`) |
1612
+ | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:1000`) |
1613
1613
  | L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
1614
- | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:775`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1615
- | Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:91`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
1616
- | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:200`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
1617
- | Les hooks React | `useNodefony()` (`client/react/index.ts:67`), `useNodefonyState()` (`client/react/index.ts:87`), `useNodefonyIdentity()` (`client/react/index.ts:104`), `useNodefonyChannel()` (`client/react/index.ts:198`), `useNodefonyChannelData()` (`client/react/index.ts:148`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts:182`) |
1618
- | Le client navigateur | `RealtimeClient.connect()` (`RealtimeClient.ts:311`), `RealtimeClient.subscribe()` (`RealtimeClient.ts:530`), `RealtimeClient.request()` (`RealtimeClient.ts:594`), `RealtimeClient.onDenied()` (`RealtimeClient.ts:386`) |
1619
- | L'upload | `UploadedFile` (`routerDecorators.ts:1239`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:185`) |
1614
+ | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:865`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1615
+ | 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`) |
package/docs/index.md CHANGED
@@ -125,9 +125,9 @@ pas deux applications à écrire deux fois, mais deux entrées du même pipeline
125
125
 
126
126
  **Une connexion, N canaux, dans les deux sens.** Le client s'abonne à autant de canaux qu'il veut sur
127
127
  la même socket. Trois formes de trafic coexistent : le serveur diffuse (`@RealtimeChannel`,
128
- `realtimeDecorators.ts:142`), le client appelle et attend une réponse (`@RealtimeAction`,
129
- `realtimeDecorators.ts:30`), le client pousse sans attendre (`@RealtimeInbound`,
130
- `realtimeDecorators.ts:30`). Rien n'est ouvert qui n'ait été déclaré.
128
+ `realtimeDecorators.ts:192`), le client appelle et attend une réponse (`@RealtimeAction`,
129
+ `realtimeDecorators.ts:142`), le client pousse sans attendre (`@RealtimeInbound`,
130
+ `realtimeDecorators.ts:231`). Rien n'est ouvert qui n'ait été déclaré.
131
131
 
132
132
  **Le travail est fait une fois par processus, pas une fois par client.** Le `RealtimeHub`
133
133
  (`RealtimeHub.ts:213`) tient **un seul producteur par canal** : le premier abonné le démarre, le
@@ -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:205`). Cinq blocs :
307
+ contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Cinq 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
@@ -325,7 +325,7 @@ vue de tous les workers d'un pod.
325
325
 
326
326
  Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
327
327
  diffusion, connexions, octets et frames, pression d'écriture. C'est la même donnée que celle rendue à
328
- l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:775`).
328
+ l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
329
329
 
330
330
  ## 🧪 Tests & couverture
331
331
 
@@ -367,6 +367,3 @@ l'infrastructure sous la main.
367
367
  - 🏛️ **Transverse** : [vue d'ensemble du framework](../../../../../docs/architecture/vue-ensemble.md) ·
368
368
  [le pipeline d'une requête](../../../../../docs/architecture/pipeline-requete.md)
369
369
  - 📖 [Lexique général](../../../../../docs/lexique.md) du framework.
370
- </content>
371
-
372
- </invoke>
@@ -96,7 +96,7 @@ temps de réponse — elle mesure **ce qui n'est pas encore parti**.
96
96
  Trois partis pris, tous lisibles dans le code.
97
97
 
98
98
  **1. La sonde est une lecture pure, jamais un collecteur.** `RealtimeHub.probe()`
99
- (`RealtimeHub.ts:775`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
99
+ (`RealtimeHub.ts:865`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
100
100
  Aucune entrée-sortie, aucune exception possible, aucun état de lecture conservé. Appeler la sonde
101
101
  mille fois par seconde ne changerait rien à ce que le hub fait par ailleurs.
102
102
 
@@ -265,7 +265,7 @@ Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts:61`). Quatre familles, un di
265
265
  | ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
266
266
  | `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
267
267
  | `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
268
- | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:838`). |
268
+ | `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:854`). |
269
269
  | `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
270
270
 
271
271
  Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
@@ -316,7 +316,7 @@ deux autres. Le débit en octets par seconde se dérive de deux photos.
316
316
 
317
317
  La source par connexion est `IRealtimeConnProbe` (`IRealtimeProbe.ts:25`), implémentée par le
318
318
  transport. Les seuils d'ACTION sont distincts du seuil de comptage, et ils sont
319
- **configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:625`) : au-delà de
319
+ **configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:1044`) : au-delà de
320
320
  `websocket.maxBackpressure` (4 MiB par défaut) la politique `websocket.backpressurePolicy`
321
321
  s'applique — `drop` par défaut, la frame est **jetée** puisque les canaux d'état sont « le
322
322
  dernier gagne » et que le prochain instantané la remplacera. La connexion est **fermée** en
@@ -356,7 +356,7 @@ Trois champs n'apparaissent que lorsqu'ils ont un sens :
356
356
  ### La couche d'identité, au-dessus
357
357
 
358
358
  `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) enrichit ce snapshot pur pour produire
359
- `IRealtimeHealth` (`IRealtimeProbe.ts:106`) : `instanceId`, la santé **process** du worker
359
+ `IRealtimeHealth` (`IRealtimeProbe.ts:121`) : `instanceId`, la santé **process** du worker
360
360
  (CPU, mémoire, boucle d'événements), et — si elles ont été branchées — la santé ORM et les compteurs
361
361
  d'erreurs du journal. Ces trois derniers champs sont **additifs et optionnels** : un consommateur qui
362
362
  ne les connaît pas les ignore.
@@ -368,14 +368,14 @@ resterait vide jusqu'au premier tick.
368
368
 
369
369
  ### L'instantané HTTP
370
370
 
371
- `GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:91`)
371
+ `GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`)
372
372
  sous le namespace `realtime` du data plane admin. Sert le premier affichage, les sondes de liveness,
373
373
  et tout script qui ne veut pas ouvrir une socket.
374
374
 
375
375
  ### Le canal temps réel
376
376
 
377
377
  `nodefony:socket`. Le producteur est un minuteur qui rappelle **le même endpoint** à travers le
378
- courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts:182`,
378
+ courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts:212`,
379
379
  via `createBrokerTicker()` (`providers.ts:465`)). Rien n'est dupliqué : le canal est un endpoint
380
380
  rejoué.
381
381
 
@@ -406,8 +406,9 @@ refusée.
406
406
  | `nodefony:syslog` | `createSyslogBridge()` (`providers.ts:145`) | **événementiel** | — (coalescé) |
407
407
 
408
408
  > [!WARNING]
409
- > **`nodefony:dashboard` n'existe pas.** Le canal de supervision s'appelle `nodefony:supervision` ; ce
410
- > nom n'apparaît plus que comme donnée de test. S'abonner à un canal inconnu ne lève **aucune
409
+ > **`nodefony:dashboard` n'est servi par aucun fournisseur.** Le canal de supervision s'appelle
410
+ > `nodefony:supervision`. Le nom survit pourtant comme constante exportée, dépréciée
411
+ > (`PLATFORM_CHANNELS.dashboard`, `platformChannels.ts:58`, retrait en 11.0.0) — c'est ce qui le rend piégeux. S'abonner à un canal inconnu ne lève **aucune
411
412
  > erreur** — le provider rend `null` et rien n'arrive jamais. C'est le piège numéro un de la page.
412
413
 
413
414
  `nodefony:syslog` est le seul de la liste à ne pas être cadencé : il **relaie** au lieu de sonder. Comme
@@ -462,7 +463,7 @@ sur deux workers et sembler se contredire.
462
463
  **La réponse de Nodefony est un modèle _push_.** Chaque worker remonte périodiquement sa santé au
463
464
  maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts:170`) ; le maître fusionne et
464
465
  rediffuse ; chaque worker met le résultat en cache. **N'importe lequel** sert alors la vue pod en temps
465
- constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:285`).
466
+ constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:291`).
466
467
 
467
468
  La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:46`), est une fonction pure — et sa règle
468
469
  mérite d'être connue :
@@ -474,7 +475,7 @@ mérite d'être connue :
474
475
  | `backpressure.maxBufferedAmount` | **maximum** | La santé d'une flotte se juge sur son **pire** membre, pas sur sa moyenne. |
475
476
  | `slowConsumers`, `drops` | somme | Ce sont des dénombrements. |
476
477
 
477
- Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`IRealtimeProbe.ts:155`) :
478
+ Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`IRealtimeProbe.ts:178`) :
478
479
  un consommateur sait immédiatement s'il lit une vue pod ou une vue per-instance, et `instances[]`
479
480
  garde le détail par worker pour le forage.
480
481
 
@@ -587,6 +588,3 @@ npm run coverage # rapport vitest — jamais de pourcentage figé dans cette p
587
588
  - [Sécurité](securite.md) — qui a le droit de s'abonner à un canal de santé, et pourquoi ça compte.
588
589
  - [Cookbook — un chat](cookbook-chat.md) — le patron appliqué de bout en bout sur une vraie fonctionnalité.
589
590
  - [Vocabulaire](vocabulaire.md) — les douze mots de la socket.
590
- </content>
591
-
592
- </invoke>