@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.
- package/README.md +2 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.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 +124 -41
- package/docs/architecture.md +93 -91
- package/docs/cadence-et-contre-pression.md +634 -0
- package/docs/configuration.md +66 -56
- package/docs/cookbook-chat.md +39 -33
- package/docs/index.md +47 -31
- package/docs/observabilite.md +21 -18
- package/docs/protocole.md +79 -65
- package/docs/securite.md +69 -65
- package/docs/vocabulaire.md +80 -79
- package/package.json +16 -12
- package/docs/couverture-bancs.md +0 -139
package/docs/configuration.md
CHANGED
|
@@ -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 **
|
|
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__<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,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:
|
|
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.
|
|
141
142
|
|
|
142
143
|
> [!IMPORTANT]
|
|
143
|
-
>
|
|
144
|
-
>
|
|
145
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
280
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
649
|
-
>
|
|
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
|
|
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. **`
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
|
699
|
-
> configuration
|
|
700
|
-
>
|
|
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:
|
|
707
|
-
|
|
708
|
-
|
|
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
|
|
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:
|
|
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
|
|
752
|
-
| ----------------------------------------------------------------------------------- |
|
|
753
|
-
| Un message n'arrive qu'à la moitié des clients, sans aucune erreur | plusieurs répliques avec `driver: "loopback"`
|
|
754
|
-
| Driver `redis` configuré, journal `RealtimeHub reste local` | module `@nodefony/redis` absent du manifeste, ou connexions `publish`/`subscribe` indisponibles
|
|
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
|
|
756
|
-
|
|
|
757
|
-
| Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde
|
|
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)
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
786
|
-
> NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm test # +
|
|
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
|
|
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,
|
|
@@ -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` |
|
|
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`) —
|
|
1550
|
-
plafond de canaux par connexion, la révocation d'une session en cours de connexion,
|
|
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
|
|
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:
|
|
1601
|
-
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:
|
|
1602
|
-
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:
|
|
1603
|
-
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:
|
|
1604
|
-
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:
|
|
1605
|
-
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:
|
|
1606
|
-
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:
|
|
1607
|
-
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:
|
|
1608
|
-
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:
|
|
1609
|
-
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:
|
|
1610
|
-
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:
|
|
1611
|
-
| Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:
|
|
1612
|
-
| 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`) |
|
|
1613
1619
|
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
|
|
1614
|
-
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:
|
|
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:
|
|
1617
|
-
| Les hooks React | `useNodefony()` (`client/react/index.ts:
|
|
1618
|
-
| Le client navigateur | `
|
|
1619
|
-
| 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`) |
|