@nodefony/realtime 10.0.0-beta.2 → 10.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +17 -10
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/config/configSchema.js +19 -0
- package/dist/nodefony/config/defineModuleConfig.js +42 -15
- package/dist/nodefony/decorators/realtimeDecorators.js +68 -17
- package/dist/nodefony/src/backplane/RedisBackplane.js +21 -4
- package/dist/nodefony/src/backplane/bootNotices.js +43 -0
- package/dist/nodefony/src/backplane/publishQueue.js +1 -4
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +15 -4
- package/dist/nodefony/src/guards.js +41 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -4
- package/dist/nodefony/src/server/RealtimeController.js +120 -50
- package/dist/nodefony/src/server/RealtimeHub.js +29 -51
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +13 -5
- package/dist/nodefony/src/server/syslogUplink.js +4 -2
- package/dist/nodefony/src/service/RealtimeService.js +3 -2
- package/dist/nodefony/testing/index.js +41 -1
- package/dist/types/nodefony/config/configSchema.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
- package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
- package/dist/types/nodefony/src/guards.d.ts +32 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +24 -4
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
- package/dist/types/nodefony/testing/index.d.ts +2 -2
- package/docs/actions.md +111 -28
- package/docs/architecture.md +77 -75
- package/docs/cadence-et-contre-pression.md +634 -0
- package/docs/configuration.md +39 -34
- package/docs/cookbook-chat.md +31 -27
- package/docs/index.md +35 -21
- package/docs/observabilite.md +17 -15
- package/docs/protocole.md +74 -63
- package/docs/securite.md +49 -49
- package/docs/vocabulaire.md +76 -75
- package/package.json +16 -12
- package/docs/couverture-bancs.md +0 -139
package/docs/configuration.md
CHANGED
|
@@ -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__<CHEMIN>=valeur"]
|
|
61
|
-
BUILD["Builder<br/>defineRealtimeConfig() : parse Zod →
|
|
60
|
+
ENVGEN["Déploiement<br/>NF_REALTIME_DRIVER · NF__REALTIME__<CHEMIN>=valeur"]
|
|
61
|
+
BUILD["Builder<br/>defineRealtimeConfig() : parse Zod → freeze"]
|
|
62
62
|
HUB["RealtimeHub<br/>garde d'origine · plafond de canaux · seuil de sonde"]
|
|
63
63
|
BP["Driver de backplane<br/>résolu par nom dans le registre"]
|
|
64
64
|
SCHEMA --> APP --> ENVGEN --> BUILD
|
|
@@ -131,10 +131,11 @@ explication en `.describe()`. Les défauts effectifs sont **dérivés** du sché
|
|
|
131
131
|
d'usine serait écrite, donc pas de dérive silencieuse possible entre la doc du champ et son
|
|
132
132
|
comportement.
|
|
133
133
|
|
|
134
|
-
Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts:
|
|
135
|
-
|
|
136
|
-
`NF_REALTIME_BACKPLANE_NAMESPACE`, `NF_REALTIME_BACKPLANE_SECRET`
|
|
137
|
-
|
|
134
|
+
Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts:39`) reste **pur** : il analyse l'entrée
|
|
135
|
+
et gèle le résultat. Il ne lit aucune variable d'environnement. Les trois variables du module
|
|
136
|
+
(`NF_REALTIME_DRIVER`, `NF_REALTIME_BACKPLANE_NAMESPACE`, `NF_REALTIME_BACKPLANE_SECRET`) sont
|
|
137
|
+
déclarées sur leur champ dans le schéma (`.meta({ env })`, `config.ts:59`, `:76`, `:93`) et posées
|
|
138
|
+
par le Kernel **avant** l'analyse : le schéma les valide comme une valeur écrite dans le fichier.
|
|
138
139
|
|
|
139
140
|
Le module **augmente le registre de types** (`index.ts:167`) : `use("@nodefony/realtime", { … })`
|
|
140
141
|
propose ses clés en autocomplétion et **refuse** une clé inconnue à la compilation.
|
|
@@ -251,7 +252,7 @@ regarder quand un message ne traverse pas.
|
|
|
251
252
|
|
|
252
253
|
> [!TIP]
|
|
253
254
|
> Ces quatre lignes viennent du même endroit du code (`Realtime.#wireBackplane()`,
|
|
254
|
-
> `src/packages/@nodefony/realtime/index.ts:
|
|
255
|
+
> `src/packages/@nodefony/realtime/index.ts:292`) et de la carte d'identité que chaque driver publie.
|
|
255
256
|
> Aucun repli n'est silencieux : si le fan-out est dégradé, c'est écrit.
|
|
256
257
|
|
|
257
258
|
## ⚙️ Le schéma, clé par clé
|
|
@@ -288,7 +289,7 @@ contre-pression, eux, se règlent sur le serveur WebSocket de `@nodefony/http`
|
|
|
288
289
|
|
|
289
290
|
Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
|
|
290
291
|
enregistrée, pas de backplane câblé, pas de sonde de pod
|
|
291
|
-
(`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:
|
|
292
|
+
(`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:246`), et le service devient
|
|
292
293
|
inerte (`RealtimeService.init()`, `RealtimeService.ts:77`).
|
|
293
294
|
|
|
294
295
|
> [!WARNING]
|
|
@@ -329,7 +330,7 @@ Ne concerne que les drivers qui traversent les machines. Le canal de transport d
|
|
|
329
330
|
`nodefony:realtime:<namespace>` (`resolveRedisChannel()`, `RedisBackplane.ts:31`).
|
|
330
331
|
|
|
331
332
|
Le défaut est **dérivé du nom de l'application**
|
|
332
|
-
(`src/packages/@nodefony/realtime/index.ts:
|
|
333
|
+
(`src/packages/@nodefony/realtime/index.ts:139`) — ce qui suffit à séparer deux applications
|
|
333
334
|
distinctes sur un Redis mutualisé, et **ne suffit pas** à séparer deux déploiements de la **même**
|
|
334
335
|
application. Préproduction et production portent le même nom d'application, donc le même canal
|
|
335
336
|
dérivé, donc le même flux.
|
|
@@ -377,13 +378,13 @@ jamais instancié, donc zéro minuteur, zéro message IPC — et l'endpoint de s
|
|
|
377
378
|
de l'instance courante.
|
|
378
379
|
|
|
379
380
|
Deux leviers coupent la sonde, et **l'un ou l'autre suffit** : cette clé, ou la variable
|
|
380
|
-
`NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts:
|
|
381
|
+
`NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts:404`). C'est ce qui permet
|
|
381
382
|
d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
|
|
382
383
|
|
|
383
384
|
### `slowConsumer.bytes` — un compteur, pas un frein
|
|
384
385
|
|
|
385
386
|
Seuil de `bufferedAmount` — les octets en attente d'envoi sur une socket — au-delà duquel la sonde
|
|
386
|
-
compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:
|
|
387
|
+
compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:847`).
|
|
387
388
|
|
|
388
389
|
> [!IMPORTANT]
|
|
389
390
|
> **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
|
|
@@ -439,7 +440,7 @@ des canaux d'état, où seul le dernier instantané compte.
|
|
|
439
440
|
Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
|
|
440
441
|
côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
|
|
441
442
|
processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
|
|
442
|
-
`RealtimeController.ts:
|
|
443
|
+
`RealtimeController.ts:869`).
|
|
443
444
|
|
|
444
445
|
Quatre propriétés qui décident du bon réglage :
|
|
445
446
|
|
|
@@ -450,7 +451,7 @@ Quatre propriétés qui décident du bon réglage :
|
|
|
450
451
|
- Un réabonnement à un canal **déjà tenu** ne consomme pas de place — l'idempotence est vérifiée
|
|
451
452
|
avant le plafond. Un client qui redemande son canal n'est pas puni.
|
|
452
453
|
- La garde existe **même sans configuration** : le hub porte le même défaut de 256
|
|
453
|
-
(`RealtimeHub.ts:
|
|
454
|
+
(`RealtimeHub.ts:218`), il n'y a pas de fenêtre où elle serait absente.
|
|
454
455
|
|
|
455
456
|
`null` retire la borne. C'est un retrait explicite, à réserver aux déploiements dont tu maîtrises
|
|
456
457
|
les clients — un tableau de bord interne qui compose des dizaines de flux, par exemple.
|
|
@@ -462,7 +463,7 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
|
|
|
462
463
|
### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
|
|
463
464
|
|
|
464
465
|
Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
|
|
465
|
-
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:
|
|
466
|
+
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:967`). Une origine refusée ferme la socket avec le code
|
|
466
467
|
`4003`.
|
|
467
468
|
|
|
468
469
|
Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
|
|
@@ -527,7 +528,7 @@ use("@nodefony/realtime", { backplane: { driver: "cluster" } });
|
|
|
527
528
|
**Ce qui se passe.** Un worker Node ne peut parler qu'au maître. Il lui envoie ses publications, et le
|
|
528
529
|
maître les redistribue aux autres workers. Le driver ne s'active qu'en **rôle worker** et avec la
|
|
529
530
|
variable `NF_CLUSTER=1`, posée par la commande de cluster
|
|
530
|
-
(`src/packages/@nodefony/realtime/index.ts:
|
|
531
|
+
(`src/packages/@nodefony/realtime/index.ts:106`) ; ailleurs, il rend `null` et le hub reste local.
|
|
531
532
|
|
|
532
533
|
**Pourquoi c'est précieux.** C'est le banc d'essai qui stabilise l'architecture multi-processus avant
|
|
533
534
|
d'ajouter du réseau. Ton contrôleur, tes canaux diffusables et ton client voient un vrai fan-out
|
|
@@ -552,7 +553,7 @@ use("@nodefony/realtime", {
|
|
|
552
553
|
|
|
553
554
|
**Comment il se branche.** Le driver ne dépend pas de la bibliothèque `redis` : il consomme deux
|
|
554
555
|
connexions du module `@nodefony/redis` par un adaptateur purement structurel
|
|
555
|
-
(`createRedisServiceTransport()`, `RedisBackplane.ts:
|
|
556
|
+
(`createRedisServiceTransport()`, `RedisBackplane.ts:137`). Deux et non une, parce qu'un client Redis
|
|
556
557
|
abonné ne peut plus émettre de commandes ordinaires — le module fournit précisément des connexions
|
|
557
558
|
nommées `publish` et `subscribe` dans ses défauts.
|
|
558
559
|
|
|
@@ -562,7 +563,7 @@ coupure. C'est acceptable pour un salon de discussion — le message est en base
|
|
|
562
563
|
un bus d'événements critiques. Ne construis pas au-dessus une fiabilité que le support n'offre pas.
|
|
563
564
|
|
|
564
565
|
**Si Redis est injoignable.** Le démarrage du backplane est attendu **explicitement**, et **borné à
|
|
565
|
-
cinq secondes** (`src/packages/@nodefony/realtime/index.ts:
|
|
566
|
+
cinq secondes** (`src/packages/@nodefony/realtime/index.ts:90`). Au-delà, ou en cas d'échec, le
|
|
566
567
|
module renonce, ferme proprement ce qu'il avait ouvert, et laisse le hub local — le démarrage
|
|
567
568
|
continue. Une base de messages en panne ne doit pas empêcher tes serveurs HTTP de monter.
|
|
568
569
|
|
|
@@ -650,7 +651,7 @@ présente court-circuite la sélection par nom.
|
|
|
650
651
|
> Le builder accepte aussi une instance en second argument (`defineRealtimeConfig(config, {
|
|
651
652
|
backplane })`), et la documentation d'architecture présente cette voie. En pratique elle n'est pas
|
|
652
653
|
> atteignable depuis `nodefony.config.ts` : le module appelle le builder avec la seule configuration
|
|
653
|
-
> fusionnée (`src/packages/@nodefony/realtime/index.ts:
|
|
654
|
+
> fusionnée (`src/packages/@nodefony/realtime/index.ts:230`), et le schéma Zod est strict : une clé
|
|
654
655
|
> inconnue, dont une instance de classe posée dans `backplane`, fait échouer le boot. Pour brancher un objet déjà construit, utilise
|
|
655
656
|
> le service `realtimeBackplane`.
|
|
656
657
|
|
|
@@ -665,7 +666,7 @@ processus se raccorde — et c'est pour cela qu'elles ne vivent pas dans un fich
|
|
|
665
666
|
|
|
666
667
|
| Variable | Portée | Effet |
|
|
667
668
|
| --------------------------------- | ---------------------- | -------------------------------------------------------------------------- |
|
|
668
|
-
| `NF_REALTIME_DRIVER` | ce module | remplace `backplane.driver
|
|
669
|
+
| `NF_REALTIME_DRIVER` | ce module | remplace `backplane.driver` — nom libre, un pilote inconnu est signalé |
|
|
669
670
|
| `NF_REALTIME_BACKPLANE_NAMESPACE` | ce module | remplace `backplane.namespace` — sépare deux déploiements de la même app |
|
|
670
671
|
| `NF_REALTIME_BACKPLANE_SECRET` | ce module | remplace `backplane.secret` — scelle le transport partagé (≥ 32 car.) |
|
|
671
672
|
| `NF__REALTIME__<CHEMIN>` | mécanisme du cœur | remplace n'importe quelle clé, par son chemin. Ex. `NF__REALTIME__ENABLED` |
|
|
@@ -677,13 +678,15 @@ L'ordre de recouvrement, du plus faible au plus fort :
|
|
|
677
678
|
|
|
678
679
|
1. **Les valeurs d'usine du schéma** — ce que tu obtiens sans rien écrire.
|
|
679
680
|
2. **La configuration de l'application** — ton `use("@nodefony/realtime", { … })`.
|
|
680
|
-
3. **`
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
681
|
+
3. **`NF_REALTIME_DRIVER`**, **`NF_REALTIME_BACKPLANE_NAMESPACE`** et
|
|
682
|
+
**`NF_REALTIME_BACKPLANE_SECRET`** — posées par le Kernel après la fusion et **avant** la
|
|
683
|
+
validation Zod (`applyEnvShortcuts`, `Kernel.ts:1989`) : une valeur fausse **arrête le démarrage** en nommant la
|
|
684
|
+
variable (un secret de moins de 32 caractères, un espace de noms qui contient `:`). Chacune
|
|
685
|
+
accepte aussi sa forme `<NOM>_FILE` (secret monté par Docker ou Kubernetes).
|
|
686
|
+
4. **`NF__REALTIME__<CHEMIN>`** — même moment, même validation. Un chemin introuvable produit un
|
|
687
|
+
avertissement avec une suggestion, jamais une clé fantôme. Viser le **même** champ qu'une des
|
|
688
|
+
trois variables ci-dessus est une ambiguïté : refusée en production, et en développement
|
|
689
|
+
`NF__…` l'emporte avec un avertissement qui nomme les deux.
|
|
687
690
|
|
|
688
691
|
Le mécanisme générique mérite d'être connu : le double tiret bas sépare les niveaux, les segments
|
|
689
692
|
sont insensibles à la casse, et les valeurs sont converties (booléens, nombres, listes séparées par
|
|
@@ -700,17 +703,19 @@ NF_CLUSTER_PROBE=0 # couper la sonde de pod sur une réplique en incident
|
|
|
700
703
|
|
|
701
704
|
> [!TIP]
|
|
702
705
|
> `NF_REALTIME_DRIVER` existe **en plus** du mécanisme générique parce qu'il est la variable qu'on
|
|
703
|
-
> pose en premier sur un déploiement
|
|
704
|
-
> configuration
|
|
705
|
-
>
|
|
706
|
+
> pose en premier sur un déploiement : son nom ne dépend pas de la structure interne de la
|
|
707
|
+
> configuration. C'est le levier d'urgence : ramener un cluster à `loopback` pour isoler une panne
|
|
708
|
+
> de bus se fait par une variable, sans redéployer de code. `npx nodefony env` la liste avec sa
|
|
709
|
+
> valeur et sa provenance.
|
|
706
710
|
|
|
707
711
|
## 🧰 Le builder et le schéma exportable
|
|
708
712
|
|
|
709
713
|
Deux fonctions publiques, utiles surtout aux tests et aux outils.
|
|
710
714
|
|
|
711
|
-
`defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:
|
|
712
|
-
|
|
713
|
-
|
|
715
|
+
`defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:39`) analyse et **gèle**. Appelé
|
|
716
|
+
hors Kernel (un test, un outil), il ne voit pas les variables d'environnement : c'est le Kernel
|
|
717
|
+
qui les pose. Le module l'appelle lui-même à l'enregistrement
|
|
718
|
+
(`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:221`) : tu n'as pas à
|
|
714
719
|
l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
|
|
715
720
|
sans démarrer un serveur.
|
|
716
721
|
|
|
@@ -722,7 +727,7 @@ const cfg = defineRealtimeConfig({ backplane: { driver: "redis" } });
|
|
|
722
727
|
// Une valeur invalide ou une clé inconnue lève une BootConfigurationError, avec le chemin exact du champ fautif.
|
|
723
728
|
```
|
|
724
729
|
|
|
725
|
-
`realtimeConfigJsonSchema()` (`defineModuleConfig.ts:
|
|
730
|
+
`realtimeConfigJsonSchema()` (`defineModuleConfig.ts:99`) produit le schéma JSON du module, chaque
|
|
726
731
|
champ portant sa description. C'est ce que consomme la page module de Studio pour afficher la
|
|
727
732
|
configuration attendue. L'instance de backplane éventuelle en est absente : une classe n'a rien à
|
|
728
733
|
faire dans un schéma sérialisable.
|
|
@@ -762,7 +767,7 @@ Trois endroits, par ordre de fiabilité décroissante.
|
|
|
762
767
|
| Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde | régler `websocketSecure.maxBackpressure` / `.backpressureCloseAfterDrops` (@nodefony/http) |
|
|
763
768
|
| Toutes les connexions tombent en `4003` après activation du contrôle d'origine | `allowList` vide, ou origine non identique au caractère près (port, schéma, sous-domaine) | inscrire l'origine exacte ; un client non-navigateur relève de `allowMissingOrigin` |
|
|
764
769
|
| `enabled: false` posé « pour désactiver », et la garde d'origine ne s'applique plus | le service devient inerte et ne pose plus les politiques sur le hub | retirer le module du manifeste plutôt que l'éteindre |
|
|
765
|
-
| Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts:
|
|
770
|
+
| Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts:218`) | vérifier le motif de refus `realtime:denied` côté client |
|
|
766
771
|
|
|
767
772
|
## 🧪 Tests & couverture
|
|
768
773
|
|
package/docs/cookbook-chat.md
CHANGED
|
@@ -353,7 +353,7 @@ gaspillerait une socket par écran.
|
|
|
353
353
|
|
|
354
354
|
```ts
|
|
355
355
|
// frontend/src/realtime.ts
|
|
356
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1325
|
+
author: user.identifier,
|
|
1322
1326
|
filename: file.filename,
|
|
1323
1327
|
size: file.size,
|
|
1324
1328
|
mimeType: file.mimeType,
|
|
@@ -1599,23 +1603,23 @@ Pour qui veut lire l'implémentation derrière chaque étape :
|
|
|
1599
1603
|
<!-- prettier-ignore -->
|
|
1600
1604
|
| Ce que tu as utilisé | Où c'est écrit |
|
|
1601
1605
|
| --- | --- |
|
|
1602
|
-
| Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:
|
|
1603
|
-
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:
|
|
1604
|
-
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:
|
|
1605
|
-
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:
|
|
1606
|
-
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:
|
|
1607
|
-
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:
|
|
1608
|
-
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:
|
|
1609
|
-
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:
|
|
1610
|
-
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:
|
|
1611
|
-
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:
|
|
1612
|
-
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:
|
|
1613
|
-
| Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:
|
|
1614
|
-
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:
|
|
1606
|
+
| Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:191`), `RealtimeChannel()` (`realtimeDecorators.ts:240`), `RealtimeInbound()` (`realtimeDecorators.ts:278`) |
|
|
1607
|
+
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:344`) |
|
|
1608
|
+
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:321`) |
|
|
1609
|
+
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:464`), `RealtimeController.startChannel()` (`RealtimeController.ts:869`) |
|
|
1610
|
+
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:610`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:629`) |
|
|
1611
|
+
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:420`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:674`) |
|
|
1612
|
+
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:720`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
|
|
1613
|
+
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1124`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1098`) |
|
|
1614
|
+
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:306`) |
|
|
1615
|
+
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:449`), fermetures `origin not allowed` (`RealtimeController.ts:422`) et `unauthorized` (`RealtimeController.ts:444`) |
|
|
1616
|
+
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:815`) |
|
|
1617
|
+
| Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:66`), fermeture `slow consumer` (`RealtimeHub.ts:64`) |
|
|
1618
|
+
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:981`) |
|
|
1615
1619
|
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
|
|
1616
|
-
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:
|
|
1620
|
+
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:847`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
|
|
1617
1621
|
| Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
|
|
1618
|
-
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:
|
|
1619
|
-
| Les hooks React | `useNodefony()` (`client/react/index.ts:
|
|
1620
|
-
| Le client navigateur | `
|
|
1621
|
-
| L'upload | `UploadedFile` (`routerDecorators.ts:
|
|
1622
|
+
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:218`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
|
|
1623
|
+
| Les hooks React | `useNodefony()` (`client/react/index.ts:255`), `useNodefonyState()` (`client/react/index.ts:273`), `useNodefonyIdentity()` (`client/react/index.ts:292`), `useNodefonyChannel()` (`client/react/index.ts:308`), `useNodefonyChannelData()` (`client/react/index.ts:332`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts:367`) |
|
|
1624
|
+
| Le client navigateur | `NodefonySocket.connect()` (`NodefonySocket.ts:592`), `NodefonySocket.subscribe()` (`NodefonySocket.ts:739`), `NodefonySocket.request()` (`NodefonySocket.ts:936`), `NodefonySocket.onDenied()` (`NodefonySocket.ts:678`) |
|
|
1625
|
+
| L'upload | `UploadedFile` (`routerDecorators.ts:1311`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:215`) |
|
package/docs/index.md
CHANGED
|
@@ -37,7 +37,7 @@ coverageModule: realtime
|
|
|
37
37
|
|
|
38
38
|
## 🧭 Par où commencer
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Quatre parcours selon ce que tu viens faire. L'ordre compte : chaque étape suppose la précédente.
|
|
41
41
|
|
|
42
42
|
**Je découvre le temps réel Nodefony** — comprendre le modèle avant d'écrire quoi que ce soit.
|
|
43
43
|
|
|
@@ -69,20 +69,31 @@ abonné connecté à l'autre.
|
|
|
69
69
|
3. [`@nodefony/security`](../../security/docs/index.md) — le pare-feu applicatif, qui protège le
|
|
70
70
|
temps réel avec **le même** modèle de zones que le web.
|
|
71
71
|
|
|
72
|
+
**Un client ne suit plus** — l'écran prend du retard, la mémoire du serveur monte, une connexion
|
|
73
|
+
se ferme en `1013`.
|
|
74
|
+
|
|
75
|
+
1. [Cadence et contre-pression](./cadence-et-contre-pression.md) — les deux étages qui encaissent
|
|
76
|
+
un client lent : la cadence que le client ralentit lui-même, la file que le serveur refuse de
|
|
77
|
+
laisser grossir. **Commence par le schéma général**, puis les courbes.
|
|
78
|
+
2. [Observabilité](./observabilite.md) — lire les clients lents et les trames refusées dans la
|
|
79
|
+
sonde, avant que quelqu'un ne se plaigne.
|
|
80
|
+
3. [Configuration](./configuration.md) — serrer les seuils, sur les **deux** serveurs WebSocket.
|
|
81
|
+
|
|
72
82
|
## 🗂️ Les pages du module
|
|
73
83
|
|
|
74
84
|
Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce qu'on y trouve.
|
|
75
85
|
|
|
76
|
-
| Page
|
|
77
|
-
|
|
|
78
|
-
| [Vocabulaire](./vocabulaire.md)
|
|
79
|
-
| [Architecture](./architecture.md)
|
|
80
|
-
| [Configuration](./configuration.md)
|
|
81
|
-
| [Sécurité](./securite.md)
|
|
82
|
-
| [Protocole](./protocole.md)
|
|
83
|
-
| [Actions RPC](./actions.md)
|
|
84
|
-
| [Observabilité](./observabilite.md)
|
|
85
|
-
| [
|
|
86
|
+
| Page | Ce qu'elle résout | Tu en as besoin quand… |
|
|
87
|
+
| ------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------- |
|
|
88
|
+
| [Vocabulaire](./vocabulaire.md) | les mots du domaine, une bonne fois | tu lis la doc, ou tu discutes archi |
|
|
89
|
+
| [Architecture](./architecture.md) | le trajet d'une frame, étage par étage | tu veux comprendre plutôt que régler |
|
|
90
|
+
| [Configuration](./configuration.md) | drivers, cloisonnement, bornes, contrôle d'origine | tu déploies, ou tu changes de topologie |
|
|
91
|
+
| [Sécurité](./securite.md) | identité à la poignée de main, droits par canal | ta socket est joignable depuis un navigateur |
|
|
92
|
+
| [Protocole](./protocole.md) | la grammaire d'une frame, et les codes d'erreur | tu débogues le fil, ou tu écris un client |
|
|
93
|
+
| [Actions RPC](./actions.md) | appeler le serveur et attendre une réponse | tu veux savoir si l'appel a marché |
|
|
94
|
+
| [Observabilité](./observabilite.md) | la sonde, les canaux de santé, les écrans | tu te demandes si ta socket va bien |
|
|
95
|
+
| [Cadence et contre-pression](./cadence-et-contre-pression.md) | un client trop lent : ralentir, refuser, couper | un écran prend du retard, ou la mémoire monte |
|
|
96
|
+
| [Cookbook — un chat](./cookbook-chat.md) | l'exemple complet, client et serveur | tu veux du code qui marche tout de suite |
|
|
86
97
|
|
|
87
98
|
```nodefony-cards
|
|
88
99
|
[
|
|
@@ -107,6 +118,9 @@ Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce q
|
|
|
107
118
|
{ "icon": "📡", "title": "observabilite", "href": "./observabilite.md",
|
|
108
119
|
"desc": "Ce que la sonde du hub expose, les canaux de santé et leur cadence, ce que Studio en montre — et comment écrire la sienne sans faire fuir la mémoire ni empêcher le processus de s'arrêter.",
|
|
109
120
|
"meta": "tu te demandes si ta socket va bien" },
|
|
121
|
+
{ "icon": "🚦", "title": "cadence-et-contre-pression", "href": "./cadence-et-contre-pression.md",
|
|
122
|
+
"desc": "Deux étages qui ne se parlent pas et se rencontrent quand même : le client ralentit la cadence de ses canaux d'état (AIMD), le serveur refuse d'empiler au-delà de 4 Mio puis ferme en 1013. Avec les courbes calculées par le vrai code, et les sondes qui le montrent en production.",
|
|
123
|
+
"meta": "un écran prend du retard, ou la mémoire du serveur monte" },
|
|
110
124
|
{ "icon": "🍳", "title": "cookbook-chat", "href": "./cookbook-chat.md",
|
|
111
125
|
"desc": "Un salon de discussion complet : configuration, service métier, contrôleur, client navigateur, puis le passage en cluster.",
|
|
112
126
|
"meta": "du code qui tourne, à déformer vers ton besoin" }
|
|
@@ -119,18 +133,18 @@ Quatre propriétés, toutes vérifiables dans le code — c'est ce qui justifie
|
|
|
119
133
|
WebSocket brute.
|
|
120
134
|
|
|
121
135
|
**Un contrôleur temps réel est un contrôleur.** `RealtimeController`
|
|
122
|
-
(`RealtimeController.ts:
|
|
136
|
+
(`RealtimeController.ts:203`) étend le `Controller` du framework : il se déclare avec les mêmes
|
|
123
137
|
décorateurs de route, reçoit la même injection, passe par le même pare-feu. HTTP et WebSocket ne sont
|
|
124
138
|
pas deux applications à écrire deux fois, mais deux entrées du même pipeline.
|
|
125
139
|
|
|
126
140
|
**Une connexion, N canaux, dans les deux sens.** Le client s'abonne à autant de canaux qu'il veut sur
|
|
127
141
|
la même socket. Trois formes de trafic coexistent : le serveur diffuse (`@RealtimeChannel`,
|
|
128
|
-
`realtimeDecorators.ts:
|
|
129
|
-
`realtimeDecorators.ts:
|
|
130
|
-
`realtimeDecorators.ts:
|
|
142
|
+
`realtimeDecorators.ts:240`), le client appelle et attend une réponse (`@RealtimeAction`,
|
|
143
|
+
`realtimeDecorators.ts:191`), le client pousse sans attendre (`@RealtimeInbound`,
|
|
144
|
+
`realtimeDecorators.ts:278`). Rien n'est ouvert qui n'ait été déclaré.
|
|
131
145
|
|
|
132
146
|
**Le travail est fait une fois par processus, pas une fois par client.** Le `RealtimeHub`
|
|
133
|
-
(`RealtimeHub.ts:
|
|
147
|
+
(`RealtimeHub.ts:214`) tient **un seul producteur par canal** : le premier abonné le démarre, le
|
|
134
148
|
dernier départ l'arrête. Chaque connexion supplémentaire n'ajoute qu'une destination de diffusion.
|
|
135
149
|
Un salon suivi par mille personnes coûte un producteur, pas mille.
|
|
136
150
|
|
|
@@ -143,7 +157,7 @@ toucher au cœur.
|
|
|
143
157
|
> [!IMPORTANT]
|
|
144
158
|
> **Rien ne franchit la frontière du processus sans intention explicite.** Par défaut, un canal reste
|
|
145
159
|
> local. Il faut le déclarer diffusable (`@RealtimeBroadcast`,
|
|
146
|
-
> `realtimeDecorators.ts:
|
|
160
|
+
> `realtimeDecorators.ts:420`) pour qu'il emprunte le backplane. C'est volontaire : un canal
|
|
147
161
|
> d'observation qui décrit l'état d'**un** pod n'aurait aucun sens répliqué sur les autres.
|
|
148
162
|
|
|
149
163
|
Le module se déclare par ailleurs **non critique** (`Realtime.critical`, `index.ts:144`) et son
|
|
@@ -239,11 +253,11 @@ export default ChatController;
|
|
|
239
253
|
Le client est isomorphe : c'est le cœur `nodefony` lui-même, importable côté navigateur.
|
|
240
254
|
|
|
241
255
|
```ts
|
|
242
|
-
import {
|
|
256
|
+
import { NodefonySocket } from "nodefony/client";
|
|
243
257
|
|
|
244
258
|
// `shared` réutilise la connexion existante pour une même URL : dix composants
|
|
245
259
|
// qui écoutent dix canaux ouvrent UNE socket, pas dix.
|
|
246
|
-
const socket =
|
|
260
|
+
const socket = NodefonySocket.shared({
|
|
247
261
|
url: "wss://127.0.0.1:5152/chat/realtime",
|
|
248
262
|
});
|
|
249
263
|
|
|
@@ -295,7 +309,7 @@ Côté serveur : `RealtimeController` (la classe à étendre), les décorateurs
|
|
|
295
309
|
publier depuis n'importe quel service injecté, le `RealtimeHub` et sa sonde, les trois backplanes
|
|
296
310
|
natifs, et le registre de drivers pour brancher le tien.
|
|
297
311
|
|
|
298
|
-
Côté client : `
|
|
312
|
+
Côté client : `NodefonySocket` via le subpath `nodefony/client`, et les hooks React via
|
|
299
313
|
`nodefony/react`.
|
|
300
314
|
|
|
301
315
|
Les signatures exactes vivent dans le graphe généré — `jq '.symbols.RealtimeHub' .ai/symbols.json` —
|
|
@@ -326,7 +340,7 @@ vue de tous les workers d'un pod.
|
|
|
326
340
|
|
|
327
341
|
Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
|
|
328
342
|
diffusion, connexions, octets et frames, pression d'écriture. C'est la même donnée que celle rendue à
|
|
329
|
-
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:
|
|
343
|
+
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:847`).
|
|
330
344
|
|
|
331
345
|
## 🧪 Tests & couverture
|
|
332
346
|
|
package/docs/observabilite.md
CHANGED
|
@@ -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:
|
|
99
|
+
(`RealtimeHub.ts:847`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
|
|
100
100
|
Aucune entrée-sortie, aucune exception possible, aucun état de lecture conservé. Appeler la sonde
|
|
101
101
|
mille fois par seconde ne changerait rien à ce que le hub fait par ailleurs.
|
|
102
102
|
|
|
@@ -228,10 +228,10 @@ Côté navigateur, deux lignes suffisent à consommer le canal :
|
|
|
228
228
|
|
|
229
229
|
```ts
|
|
230
230
|
// frontend/src/shopHealth.ts
|
|
231
|
-
import {
|
|
231
|
+
import { NodefonySocket } from "nodefony/client";
|
|
232
232
|
|
|
233
233
|
const scheme = window.location.protocol === "https:" ? "wss" : "ws";
|
|
234
|
-
const socket = new
|
|
234
|
+
const socket = new NodefonySocket({
|
|
235
235
|
url: `${scheme}://${window.location.host}/shop/realtime`,
|
|
236
236
|
});
|
|
237
237
|
|
|
@@ -265,7 +265,7 @@ Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts:61`). Quatre familles, un di
|
|
|
265
265
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
266
266
|
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
|
|
267
267
|
| `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
|
|
268
|
-
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:
|
|
268
|
+
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:836`). |
|
|
269
269
|
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
|
|
270
270
|
|
|
271
271
|
Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
|
|
@@ -316,13 +316,15 @@ deux autres. Le débit en octets par seconde se dérive de deux photos.
|
|
|
316
316
|
|
|
317
317
|
La source par connexion est `IRealtimeConnProbe` (`IRealtimeProbe.ts:25`), implémentée par le
|
|
318
318
|
transport. Les seuils d'ACTION sont distincts du seuil de comptage, et ils sont
|
|
319
|
-
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:
|
|
319
|
+
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:1226`) : au-delà de
|
|
320
320
|
`websocket.maxBackpressure` (4 MiB par défaut) la politique `websocket.backpressurePolicy`
|
|
321
|
-
s'applique — `drop` par défaut, la frame est **jetée
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
321
|
+
s'applique — `drop` par défaut, la frame est **jetée**. Sur un canal d'état c'est sans
|
|
322
|
+
conséquence, le prochain instantané la remplace ; mais le rejet ne trie rien, et une réponse RPC
|
|
323
|
+
ou un événement jetés sont **perdus**. La connexion est **fermée** en `1013` (« réessaie plus
|
|
324
|
+
tard ») quand le **solde** de refus atteint `websocket.backpressureCloseAfterDrops` (1000 par
|
|
325
|
+
défaut) — +1 par frame jetée, −1 par frame envoyée, si bien qu'un pic passager redescend — ou
|
|
326
|
+
dès le premier dépassement si la politique est `close`. Le mécanisme complet, avec ses courbes :
|
|
327
|
+
[Cadence et contre-pression](./cadence-et-contre-pression.md).
|
|
326
328
|
|
|
327
329
|
> [!IMPORTANT]
|
|
328
330
|
> `drops` qui croît n'est **pas** une panne : c'est la protection qui fonctionne. Ce qui serait une
|
|
@@ -462,11 +464,11 @@ instantané est exact — et ne dit rien des trois autres. Pire : deux appels co
|
|
|
462
464
|
sur deux workers et sembler se contredire.
|
|
463
465
|
|
|
464
466
|
**La réponse de Nodefony est un modèle _push_.** Chaque worker remonte périodiquement sa santé au
|
|
465
|
-
maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts:
|
|
467
|
+
maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts:190`) ; le maître fusionne et
|
|
466
468
|
rediffuse ; chaque worker met le résultat en cache. **N'importe lequel** sert alors la vue pod en temps
|
|
467
|
-
constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:
|
|
469
|
+
constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:306`).
|
|
468
470
|
|
|
469
|
-
La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:
|
|
471
|
+
La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:60`), est une fonction pure — et sa règle
|
|
470
472
|
mérite d'être connue :
|
|
471
473
|
|
|
472
474
|
| Grandeur | Agrégation | Pourquoi |
|
|
@@ -483,7 +485,7 @@ garde le détail par worker pour le forage.
|
|
|
483
485
|
### On paie ce qu'on regarde
|
|
484
486
|
|
|
485
487
|
Les sondes coûteuses ne tournent pas en permanence. Le maître peut demander à un worker précis
|
|
486
|
-
d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts:
|
|
488
|
+
d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts:259`) — quand
|
|
487
489
|
un humain ouvre le forage de ce `pid`. La sonde riche (espaces mémoire, cycles de ramasse-miettes,
|
|
488
490
|
descripteurs actifs) est alors allouée ; à la fermeture de l'écran, elle est libérée et son observateur
|
|
489
491
|
détaché.
|
|
@@ -504,7 +506,7 @@ l'orchestrateur finit par le tuer de force à l'expiration du délai de grâce.
|
|
|
504
506
|
|
|
505
507
|
La règle est tenue partout dans le code, y compris là où on ne l'attendrait pas — le tick de
|
|
506
508
|
revalidation d'identité du hub, qui ne démarre qu'au premier inscrit, est lui aussi détaché
|
|
507
|
-
(`RealtimeHub.ts:
|
|
509
|
+
(`RealtimeHub.ts:443`), tout comme le report de la sonde cluster (`ClusterProbeClient.ts:170`) et le
|
|
508
510
|
minuteur générique des canaux (`providers.ts:465`).
|
|
509
511
|
|
|
510
512
|
```ts ignore
|