@nodefony/realtime 10.0.0-alpha.9 → 10.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/src/backplane/envelope.js +2 -1
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -1
- package/dist/nodefony/src/server/RealtimeController.js +52 -11
- package/dist/nodefony/src/server/RealtimeHub.js +14 -12
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +9 -7
- package/dist/nodefony/src/service/RealtimeService.js +7 -6
- package/dist/nodefony/testing/index.js +4 -2
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +3 -3
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +27 -0
- package/dist/types/nodefony/testing/index.d.ts +2 -2
- package/docs/actions.md +19 -19
- package/docs/architecture.md +35 -35
- package/docs/configuration.md +35 -30
- package/docs/cookbook-chat.md +19 -17
- package/docs/index.md +13 -14
- package/docs/observabilite.md +6 -8
- package/docs/protocole.md +24 -19
- package/docs/securite.md +43 -39
- package/docs/vocabulaire.md +23 -23
- package/package.json +10 -10
package/docs/architecture.md
CHANGED
|
@@ -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:
|
|
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,7 +141,7 @@ 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:
|
|
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**
|
|
@@ -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:
|
|
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
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,13 +459,13 @@ 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:
|
|
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:
|
|
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
|
|
|
@@ -478,7 +478,7 @@ dépendances.
|
|
|
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:
|
|
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
|
|
@@ -547,12 +547,12 @@ coûte chaque abonné supplémentaire.
|
|
|
547
547
|
|
|
548
548
|
### Le contrat, en cinq méthodes
|
|
549
549
|
|
|
550
|
-
`IBackplane` (`IBackplane.ts:
|
|
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
553
|
| Membre | Rôle |
|
|
554
554
|
| ------------------------------------------------- | --------------------------------------------------------------- |
|
|
555
|
-
| `originId` (`IBackplane.ts:
|
|
555
|
+
| `originId` (`IBackplane.ts:109`) | l'étiquette de CE process, lue par l'anti-écho |
|
|
556
556
|
| `start()` (`IBackplane.ts:115`) | ouvrir le transport. Idempotent, synchrone ou asynchrone |
|
|
557
557
|
| `publish(channel, payload)` (`IBackplane.ts:121`) | propager aux **autres** pairs. Ne refait pas le fan-out local |
|
|
558
558
|
| `onMessage(handler)` (`IBackplane.ts:127`) | recevoir des pairs. Un seul gestionnaire à la fois |
|
|
@@ -718,8 +718,8 @@ code qui **appelle le builder lui-même** (un `Module` sur mesure, une intégrat
|
|
|
718
718
|
programmatique). Une application qui configure via `use("@nodefony/realtime", …)` **ne
|
|
719
719
|
l'atteint pas** : elle ne construit pas la config (le module appelle
|
|
720
720
|
`defineRealtimeConfig(this.options)` sans second argument, `index.ts:216`), et une
|
|
721
|
-
instance posée dans l'objet de config est
|
|
722
|
-
|
|
721
|
+
instance posée dans l'objet de config est refusée par la validation Zod (clé inconnue du
|
|
722
|
+
schéma strict). Pour une app, la voie « instance » est l'option 2 ci-dessus.
|
|
723
723
|
|
|
724
724
|
Le registre s'introspecte : `listBackplaneDrivers()` (`backplaneRegistry.ts:68`) rend les
|
|
725
725
|
noms disponibles — c'est ce que le message d'erreur affiche quand un driver déclaré est
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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.
|
|
@@ -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:
|
|
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
|
|
|
@@ -824,25 +824,25 @@ désabonné.
|
|
|
824
824
|
|
|
825
825
|
**Le vrai point de vigilance est la back-pressure**, pas le processeur : une file d'envoi
|
|
826
826
|
non bornée multipliée par le nombre de clients, c'est la panne mémoire — et le multiplexage
|
|
827
|
-
concentre le risque. D'où les
|
|
827
|
+
concentre le risque. D'où les réglages de contre-pression du transport, le comptage des consommateurs lents
|
|
828
828
|
dans la sonde, et le plafond de canaux par connexion.
|
|
829
829
|
|
|
830
830
|
## ⚠️ Pièges
|
|
831
831
|
|
|
832
|
-
| Symptôme | Cause
|
|
833
|
-
| -------------------------------------------------------------- |
|
|
834
|
-
| Le chat marche en local, plus rien en cluster | le canal n'est pas déclaré broadcast (défaut : instance-local)
|
|
835
|
-
| Le client ne reçoit rien alors qu'il a un handler `on(...)` | `on` reçoit, `subscribe` demande — il faut les **deux**
|
|
836
|
-
| Chaque message arrive **en double** en cluster | `publishLocal` court-circuité, ou `originId` non unique entre pods
|
|
837
|
-
| Deux applications se parlent sur un Redis mutualisé | même canal dérivé (le numéro de base Redis ne cloisonne pas le pub/sub)
|
|
838
|
-
| Producteur planté après le départ du premier abonné | la fabrique a capturé le contexte de la connexion créatrice
|
|
839
|
-
| Abonnement refusé avec `realtime:denied` motif `limit` | plafond de canaux par connexion atteint (256 par défaut)
|
|
840
|
-
| Fermeture `1013` sur un client lent | file
|
|
841
|
-
| Fermeture `4001` en cours de session | identité révoquée, détectée par le tick de re-validation
|
|
842
|
-
| Frames envoyées juste après `connect()` perdues | le transport n'est pas branché tant que le handshake n'est pas fini
|
|
843
|
-
| Avertissement « channel policies … NOT enforced » au démarrage | des canaux déclarent une politique sans décideur câblé
|
|
844
|
-
| `ServerRealtimeSocket.request()` rejette systématiquement | un handle posé sur le hub n'a pas d'interlocuteur unique
|
|
845
|
-
| `TS2550: RegExp.escape does not exist` à la compilation | `lib` du tsconfig sous ES2025 (les paquets exposent leurs types en source)
|
|
832
|
+
| Symptôme | Cause | Correction |
|
|
833
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
834
|
+
| Le chat marche en local, plus rien en cluster | le canal n'est pas déclaré broadcast (défaut : instance-local) | déclarer le préfixe avec `@RealtimeBroadcast` |
|
|
835
|
+
| Le client ne reçoit rien alors qu'il a un handler `on(...)` | `on` reçoit, `subscribe` demande — il faut les **deux** | appeler `socket.subscribe(canal)` |
|
|
836
|
+
| Chaque message arrive **en double** en cluster | `publishLocal` court-circuité, ou `originId` non unique entre pods | ne jamais republier une arrivée backplane ; vérifier `NF_POD_NAME`/hostname |
|
|
837
|
+
| Deux applications se parlent sur un Redis mutualisé | même canal dérivé (le numéro de base Redis ne cloisonne pas le pub/sub) | poser un `backplane.namespace` explicite et distinct |
|
|
838
|
+
| Producteur planté après le départ du premier abonné | la fabrique a capturé le contexte de la connexion créatrice | ne capturer que des valeurs à longue durée de vie |
|
|
839
|
+
| Abonnement refusé avec `realtime:denied` motif `limit` | plafond de canaux par connexion atteint (256 par défaut) | regrouper les canaux, ou relever le plafond en connaissance de cause |
|
|
840
|
+
| Fermeture `1013` sur un client lent | file au-delà de `websocket.maxBackpressure` (4 MiB) pendant 1000 frames jetées d'affilée | attendu ; le client se reconnecte et se resynchronise |
|
|
841
|
+
| Fermeture `4001` en cours de session | identité révoquée, détectée par le tick de re-validation | se réauthentifier ; le comportement est voulu |
|
|
842
|
+
| Frames envoyées juste après `connect()` perdues | le transport n'est pas branché tant que le handshake n'est pas fini | attendre `realtime:welcome` (`RealtimeClient` le fait déjà) |
|
|
843
|
+
| Avertissement « channel policies … NOT enforced » au démarrage | des canaux déclarent une politique sans décideur câblé | charger `@nodefony/security` avec une zone realtime |
|
|
844
|
+
| `ServerRealtimeSocket.request()` rejette systématiquement | un handle posé sur le hub n'a pas d'interlocuteur unique | utiliser `RealtimeController.requestClient()` pour un appel ciblé |
|
|
845
|
+
| `TS2550: RegExp.escape does not exist` à la compilation | `lib` du tsconfig sous ES2025 (les paquets exposent leurs types en source) | `"lib": ["ESNext", …]`, comme le tsconfig généré par `nodefony create app` |
|
|
846
846
|
|
|
847
847
|
## 🧪 Tests & couverture
|
|
848
848
|
|
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
|
|
@@ -140,10 +140,9 @@ Le module **augmente le registre de types** (`index.ts:167`) : `use("@nodefony/r
|
|
|
140
140
|
propose ses clés en autocomplétion et **refuse** une clé inconnue à la compilation.
|
|
141
141
|
|
|
142
142
|
> [!IMPORTANT]
|
|
143
|
-
>
|
|
144
|
-
>
|
|
145
|
-
|
|
146
|
-
> faute ne compile pas.
|
|
143
|
+
> Ce filet attrape la faute dès l'écriture. Le schéma, lui, est strict : une clé inconnue arrête le
|
|
144
|
+
> démarrage (`BootConfigurationError`, avec le chemin complet de la clé). `slowConsummer: { bytes:
|
|
145
|
+
4096 }` est donc refusé au boot ; avec l'augmentation, la même faute ne compile même pas.
|
|
147
146
|
|
|
148
147
|
## 🚀 Démarrage rapide
|
|
149
148
|
|
|
@@ -257,7 +256,7 @@ regarder quand un message ne traverse pas.
|
|
|
257
256
|
|
|
258
257
|
## ⚙️ Le schéma, clé par clé
|
|
259
258
|
|
|
260
|
-
|
|
259
|
+
Sept blocs, seize clés au total. Le tableau donne la vérité complète ; les sections qui suivent
|
|
261
260
|
expliquent quand et pourquoi en changer.
|
|
262
261
|
|
|
263
262
|
<!-- prettier-ignore -->
|
|
@@ -274,16 +273,22 @@ expliquent quand et pourquoi en changer.
|
|
|
274
273
|
| `csrf.checkOrigin.enabled` | `boolean` | `false` | active le contrôle d'`Origin` à l'ouverture de la socket |
|
|
275
274
|
| `csrf.checkOrigin.allowList` | `string[]` | `[]` | origines acceptées, **comparaison exacte**. Vide + activé = tout est refusé |
|
|
276
275
|
| `csrf.checkOrigin.allowMissingOrigin` | `boolean` | `false` | accepter une ouverture sans en-tête `Origin` (clients non-navigateur) |
|
|
276
|
+
| `clientLogs.enabled` | `boolean` | `false` | accepte les journaux du navigateur sur le canal montant `nodefony:syslog:uplink`. Surface d'écriture, fermée par défaut ; une connexion anonyme ne pousse rien |
|
|
277
|
+
| `clientLogs.maxEntriesPerBatch` | `number` | `50` | entrées acceptées par lot |
|
|
278
|
+
| `clientLogs.maxEntriesPerWindow` | `number` | `300` | entrées acceptées par fenêtre de débit |
|
|
279
|
+
| `clientLogs.windowMs` | `number` | `10000` | durée de la fenêtre de débit, en ms |
|
|
280
|
+
| `clientLogs.maxStringLength` | `number` | `4096` | longueur maximale d'une chaîne reçue |
|
|
277
281
|
|
|
278
282
|
C'est **tout**. Il n'existe ni réglage de ping, ni de cadence, ni de fréquence d'échantillonnage de
|
|
279
|
-
la sonde
|
|
280
|
-
|
|
283
|
+
la sonde : ces comportements existent, mais leurs valeurs sont des constantes du code. Les seuils de
|
|
284
|
+
contre-pression, eux, se règlent sur le serveur WebSocket de `@nodefony/http`
|
|
285
|
+
(`websocket.maxBackpressure`, `.backpressurePolicy`, `.backpressureCloseAfterDrops`).
|
|
281
286
|
|
|
282
287
|
### `enabled` — le module au repos
|
|
283
288
|
|
|
284
289
|
Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
|
|
285
290
|
enregistrée, pas de backplane câblé, pas de sonde de pod
|
|
286
|
-
(`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:
|
|
291
|
+
(`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:239`), et le service devient
|
|
287
292
|
inerte (`RealtimeService.init()`, `RealtimeService.ts:77`).
|
|
288
293
|
|
|
289
294
|
> [!WARNING]
|
|
@@ -344,7 +349,7 @@ canal exotique.
|
|
|
344
349
|
Ne concerne, là aussi, que les transports **partagés**. Un bus pub/sub ne dit pas _qui_ a publié :
|
|
345
350
|
sans secret, tout ce qui atterrit sur le canal — venu d'un pod ou d'un tiers qui sait écrire dans ce
|
|
346
351
|
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:
|
|
352
|
+
HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts:112`) : un message non
|
|
348
353
|
scellé, altéré ou repointé vers un autre canal est **ignoré**, sans exception possible.
|
|
349
354
|
|
|
350
355
|
Le secret doit être **identique sur tous les pods** — c'est lui qui les reconnaît entre eux. En
|
|
@@ -378,7 +383,7 @@ d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
|
|
|
378
383
|
### `slowConsumer.bytes` — un compteur, pas un frein
|
|
379
384
|
|
|
380
385
|
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:
|
|
386
|
+
compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
|
|
382
387
|
|
|
383
388
|
> [!IMPORTANT]
|
|
384
389
|
> **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
|
|
@@ -434,7 +439,7 @@ des canaux d'état, où seul le dernier instantané compte.
|
|
|
434
439
|
Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
|
|
435
440
|
côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
|
|
436
441
|
processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
|
|
437
|
-
`RealtimeController.ts:
|
|
442
|
+
`RealtimeController.ts:815`).
|
|
438
443
|
|
|
439
444
|
Quatre propriétés qui décident du bon réglage :
|
|
440
445
|
|
|
@@ -457,7 +462,7 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
|
|
|
457
462
|
### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
|
|
458
463
|
|
|
459
464
|
Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
|
|
460
|
-
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:
|
|
465
|
+
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:986`). Une origine refusée ferme la socket avec le code
|
|
461
466
|
`4003`.
|
|
462
467
|
|
|
463
468
|
Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
|
|
@@ -645,8 +650,8 @@ présente court-circuite la sélection par nom.
|
|
|
645
650
|
> Le builder accepte aussi une instance en second argument (`defineRealtimeConfig(config, {
|
|
646
651
|
backplane })`), et la documentation d'architecture présente cette voie. En pratique elle n'est pas
|
|
647
652
|
> atteignable depuis `nodefony.config.ts` : le module appelle le builder avec la seule configuration
|
|
648
|
-
> fusionnée (`src/packages/@nodefony/realtime/index.ts:216`), et le schéma Zod
|
|
649
|
-
>
|
|
653
|
+
> fusionnée (`src/packages/@nodefony/realtime/index.ts:216`), et le schéma Zod est strict : une clé
|
|
654
|
+
> inconnue, dont une instance de classe posée dans `backplane`, fait échouer le boot. Pour brancher un objet déjà construit, utilise
|
|
650
655
|
> le service `realtimeBackplane`.
|
|
651
656
|
|
|
652
657
|
Le détail du contrat, de l'anti-écho et du cycle de vie d'un driver est dans
|
|
@@ -705,7 +710,7 @@ Deux fonctions publiques, utiles surtout aux tests et aux outils.
|
|
|
705
710
|
|
|
706
711
|
`defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:35`) analyse, applique la variable
|
|
707
712
|
de driver et **gèle**. Le module l'appelle lui-même à l'enregistrement
|
|
708
|
-
(`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:
|
|
713
|
+
(`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:213`) : tu n'as pas à
|
|
709
714
|
l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
|
|
710
715
|
sans démarrer un serveur.
|
|
711
716
|
|
|
@@ -714,7 +719,7 @@ import { defineRealtimeConfig } from "@nodefony/realtime";
|
|
|
714
719
|
|
|
715
720
|
const cfg = defineRealtimeConfig({ backplane: { driver: "redis" } });
|
|
716
721
|
// cfg.limits.maxChannelsPerConnection === 256 — les sections omises gardent leurs défauts.
|
|
717
|
-
// Une valeur invalide lève une
|
|
722
|
+
// Une valeur invalide ou une clé inconnue lève une BootConfigurationError, avec le chemin exact du champ fautif.
|
|
718
723
|
```
|
|
719
724
|
|
|
720
725
|
`realtimeConfigJsonSchema()` (`defineModuleConfig.ts:69`) produit le schéma JSON du module, chaque
|
|
@@ -748,16 +753,16 @@ Trois endroits, par ordre de fiabilité décroissante.
|
|
|
748
753
|
|
|
749
754
|
## ⚠️ Pièges
|
|
750
755
|
|
|
751
|
-
| Symptôme | Cause
|
|
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:217`)
|
|
756
|
+
| Symptôme | Cause | Correction |
|
|
757
|
+
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
758
|
+
| Un message n'arrive qu'à la moitié des clients, sans aucune erreur | plusieurs répliques avec `driver: "loopback"` | passer à `redis` et déclarer `@nodefony/redis` dans le manifeste |
|
|
759
|
+
| Driver `redis` configuré, journal `RealtimeHub reste local` | module `@nodefony/redis` absent du manifeste, ou connexions `publish`/`subscribe` indisponibles | déclarer le module ; vérifier que Redis répond |
|
|
760
|
+
| Le fan-out traverse, mais des messages d'un autre environnement arrivent | `backplane.namespace` non posé : deux déploiements de la même application dérivent le même canal | poser un namespace explicite et distinct par environnement |
|
|
761
|
+
| Le démarrage échoue avec `clé inconnue : …` | clé absente du schéma strict (faute de frappe, ou clé retirée) | corriger ou retirer la clé que nomme le message |
|
|
762
|
+
| Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde | régler `websocketSecure.maxBackpressure` / `.backpressureCloseAfterDrops` (@nodefony/http) |
|
|
763
|
+
| Toutes les connexions tombent en `4003` après activation du contrôle d'origine | `allowList` vide, ou origine non identique au caractère près (port, schéma, sous-domaine) | inscrire l'origine exacte ; un client non-navigateur relève de `allowMissingOrigin` |
|
|
764
|
+
| `enabled: false` posé « pour désactiver », et la garde d'origine ne s'applique plus | le service devient inerte et ne pose plus les politiques sur le hub | retirer le module du manifeste plutôt que l'éteindre |
|
|
765
|
+
| Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts:217`) | vérifier le motif de refus `realtime:denied` côté client |
|
|
761
766
|
|
|
762
767
|
## 🧪 Tests & couverture
|
|
763
768
|
|
|
@@ -778,12 +783,12 @@ qui compte ici, c'est **ce que les suites prouvent sur la configuration**.
|
|
|
778
783
|
> **Un run vert ne prouve pas le fan-out entre machines.** Le banc Redis est doublement conditionnel :
|
|
779
784
|
> il n'est lancé que sur demande, et il se **saute** de lui-même si aucun Redis ne répond. Un test
|
|
780
785
|
> sauté compte comme un succès — on peut donc lire « tout est vert » sur une suite qui n'a jamais
|
|
781
|
-
> ouvert une connexion. Le fan-out entre workers
|
|
786
|
+
> ouvert une connexion. Le fan-out entre workers ne demande aucune infrastructure externe, mais ne tourne que dans `npm run test:cluster` (avec `NF_RUN_CLUSTER_E2E=1`).
|
|
782
787
|
>
|
|
783
788
|
> ```bash
|
|
784
789
|
> cd src/packages/@nodefony/realtime
|
|
785
|
-
> npm test # unitaires
|
|
786
|
-
> NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm test # +
|
|
790
|
+
> npm test # unitaires et intégration
|
|
791
|
+
> NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm run test:cluster # IPC entre workers + fan-out entre machines
|
|
787
792
|
> npm run coverage # couverture (vitest, v8)
|
|
788
793
|
> ```
|
|
789
794
|
|
package/docs/cookbook-chat.md
CHANGED
|
@@ -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
|
|
@@ -1496,7 +1496,7 @@ pour lire ta propre sortie :
|
|
|
1496
1496
|
| `backpressure.maxBufferedAmount` | La **pire** file d'envoi observée à l'instant de la mesure. C'est l'indicateur d'alerte. |
|
|
1497
1497
|
| `backpressure.slowConsumers` | Combien de connexions dépassent le seuil de consommateur lent. |
|
|
1498
1498
|
| `backpressure.drops` | Messages abandonnés faute de pouvoir être livrés. |
|
|
1499
|
-
| `backplane` |
|
|
1499
|
+
| `backplane` | Toujours présent : carte d'identité du relais (driver, nature, `crossPod`) ; sans relais, descripteur `local` / `loopback`. |
|
|
1500
1500
|
|
|
1501
1501
|
Deux lectures suffisent à diagnostiquer :
|
|
1502
1502
|
|
|
@@ -1546,12 +1546,14 @@ Ce qui est couvert, par nature de test :
|
|
|
1546
1546
|
- **Unitaires** — le hub (abonnement, fan-out, cycle de vie des providers), les décorateurs et leurs
|
|
1547
1547
|
métadonnées, le registre de drivers de backplane, le contrôleur, le service, le transport
|
|
1548
1548
|
WebSocket, la validation de configuration, la dérivation de l'identifiant d'origine.
|
|
1549
|
-
- **Tests d'attaque** (`*.attack.test.ts`) —
|
|
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)
|
|
1549
|
+
- **Tests d'attaque** (`*.attack.test.ts`) — quatre scénarios verrouillés en non-régression : le
|
|
1550
|
+
plafond de canaux par connexion, la révocation d'une session en cours de connexion, le cas
|
|
1551
|
+
d'une politique de canal déclarée **sans** gardien branché (la dégradation doit rester bruyante),
|
|
1552
|
+
et l'injection par le bus (un pair ne pousse pas sur un canal non diffusable, un message forgé sur
|
|
1553
|
+
un bus scellé est ignoré).
|
|
1552
1554
|
- **Intégration de bout en bout** (`*.e2e.test.ts`) — le cheminement complet sur boucle locale, les
|
|
1553
|
-
chemins de contrôleur, l'autorisation de canal, le branchement du firewall
|
|
1554
|
-
de vrais processus enfants
|
|
1555
|
+
chemins de contrôleur, l'autorisation de canal, le branchement du firewall. Le cluster IPC, avec
|
|
1556
|
+
de vrais processus enfants, se lance à part : `npm run test:cluster` avec `NF_RUN_CLUSTER_E2E=1`.
|
|
1555
1557
|
- **Cluster Redis** — exercé de bout en bout, mais **conditionné à la présence d'un Redis**. Sans
|
|
1556
1558
|
lui, ces cas sont ignorés, et un test ignoré compte comme réussi : ne conclus pas d'un « tout est
|
|
1557
1559
|
vert » que le chemin Redis a été vérifié chez toi.
|
|
@@ -1598,22 +1600,22 @@ Pour qui veut lire l'implémentation derrière chaque étape :
|
|
|
1598
1600
|
| Ce que tu as utilisé | Où c'est écrit |
|
|
1599
1601
|
| --- | --- |
|
|
1600
1602
|
| Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:192`), `RealtimeInbound()` (`realtimeDecorators.ts:231`) |
|
|
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:462`), `RealtimeController.startChannel()` (`RealtimeController.ts:
|
|
1603
|
+
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:299`) |
|
|
1604
|
+
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:276`) |
|
|
1605
|
+
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:462`), `RealtimeController.startChannel()` (`RealtimeController.ts:815`) |
|
|
1604
1606
|
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:623`) |
|
|
1605
1607
|
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:668`) |
|
|
1606
1608
|
| 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:
|
|
1609
|
+
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1143`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1117`) |
|
|
1608
1610
|
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:279`) |
|
|
1609
|
-
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:
|
|
1610
|
-
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:
|
|
1611
|
+
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:417`), fermetures `origin not allowed` (`RealtimeController.ts:327`) et `unauthorized` (`RealtimeController.ts:397`) |
|
|
1612
|
+
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:826`) |
|
|
1611
1613
|
| 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:
|
|
1614
|
+
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:1000`) |
|
|
1613
1615
|
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
|
|
1614
|
-
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:
|
|
1616
|
+
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:865`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
|
|
1615
1617
|
| Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
|
|
1616
1618
|
| 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:
|
|
1619
|
+
| Les hooks React | `useNodefony()` (`client/react/index.ts:145`), `useNodefonyState()` (`client/react/index.ts:163`), `useNodefonyIdentity()` (`client/react/index.ts:182`), `useNodefonyChannel()` (`client/react/index.ts:198`), `useNodefonyChannelData()` (`client/react/index.ts:221`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts:255`) |
|
|
1618
1620
|
| Le client navigateur | `RealtimeClient.connect()` (`RealtimeClient.ts:389`), `RealtimeClient.subscribe()` (`RealtimeClient.ts:530`), `RealtimeClient.request()` (`RealtimeClient.ts:729`), `RealtimeClient.onDenied()` (`RealtimeClient.ts:471`) |
|
|
1619
|
-
| L'upload | `UploadedFile` (`routerDecorators.ts:
|
|
1621
|
+
| L'upload | `UploadedFile` (`routerDecorators.ts:1286`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:226`) |
|
package/docs/index.md
CHANGED
|
@@ -304,7 +304,7 @@ jamais recopiées ici : elles divergeraient en silence.
|
|
|
304
304
|
## ⚙️ Configuration
|
|
305
305
|
|
|
306
306
|
Un seul point d'entrée : `use("@nodefony/realtime", { … })` dans `nodefony.config.ts`, validé au boot
|
|
307
|
-
contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`).
|
|
307
|
+
contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Six blocs :
|
|
308
308
|
|
|
309
309
|
- `backplane` — le driver de fan-out et son espace de nommage. Ce cloisonnement devient
|
|
310
310
|
indispensable dès que **deux déploiements partagent le même Redis** : sans lui, leurs publications
|
|
@@ -313,6 +313,7 @@ contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Cinq bloc
|
|
|
313
313
|
- `limits` — le plafond de canaux par connexion, garde anti-saturation mémoire.
|
|
314
314
|
- `slowConsumer` — le seuil à partir duquel un client trop lent est signalé par la sonde.
|
|
315
315
|
- `cluster.probe` — la sonde agrégée du pod, en mode multi-workers.
|
|
316
|
+
- `clientLogs` — la réception des journaux du navigateur (canal montant), fermée par défaut.
|
|
316
317
|
|
|
317
318
|
Chaque bloc, ses valeurs d'usine et les situations qui justifient d'en changer sont détaillés dans
|
|
318
319
|
[Configuration](./configuration.md).
|
|
@@ -325,7 +326,7 @@ vue de tous les workers d'un pod.
|
|
|
325
326
|
|
|
326
327
|
Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
|
|
327
328
|
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:
|
|
329
|
+
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
|
|
329
330
|
|
|
330
331
|
## 🧪 Tests & couverture
|
|
331
332
|
|
|
@@ -333,24 +334,25 @@ Les compteurs sont régénérés depuis vitest, jamais figés dans cette prose.
|
|
|
333
334
|
ici, c'est **ce que les suites prouvent** — et ce qu'elles ne prouvent qu'à condition d'avoir
|
|
334
335
|
l'infrastructure sous la main.
|
|
335
336
|
|
|
336
|
-
| Type | Où | Ce qui est prouvé
|
|
337
|
-
| --------------- | ------------------------------- |
|
|
338
|
-
| Unitaire | `nodefony/tests/unit/**` | hub, registre de drivers, schéma de configuration, décorateurs
|
|
339
|
-
| Intégration | `nodefony/tests/integration/**` | socket réelle : abonnements, chemins de contrôleur, autorisation
|
|
340
|
-
| Multi-processus | `clusterIpc.e2e.test.ts` | fan-out entre workers, sans
|
|
341
|
-
| Cross-machine | `redisCluster.e2e.test.ts` | fan-out entre pods via pub/sub Redis
|
|
342
|
-
| Tests d'attaque | `*.attack.test.ts` | plafond de canaux, révocation d'identité, politique non appliquée |
|
|
337
|
+
| Type | Où | Ce qui est prouvé |
|
|
338
|
+
| --------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
|
|
339
|
+
| Unitaire | `nodefony/tests/unit/**` | hub, registre de drivers, schéma de configuration, décorateurs |
|
|
340
|
+
| Intégration | `nodefony/tests/integration/**` | socket réelle : abonnements, chemins de contrôleur, autorisation |
|
|
341
|
+
| Multi-processus | `clusterIpc.e2e.test.ts` | fan-out entre workers, sans infrastructure externe (`npm run test:cluster`) |
|
|
342
|
+
| Cross-machine | `redisCluster.e2e.test.ts` | fan-out entre pods via pub/sub Redis |
|
|
343
|
+
| Tests d'attaque | `*.attack.test.ts` | plafond de canaux, révocation d'identité, politique non appliquée, injection sur le bus |
|
|
343
344
|
|
|
344
345
|
> [!WARNING]
|
|
345
346
|
> **Une suite verte ne prouve rien sur le fan-out cross-machine.** Le banc Redis est doublement
|
|
346
347
|
> conditionnel : il ne s'exécute que si tu le demandes (`NF_RUN_CLUSTER_E2E=1`), et il se **saute** de
|
|
347
348
|
> lui-même si aucun Redis n'est joignable. Or un test sauté compte comme un succès — on peut donc
|
|
348
349
|
> lire « tout est vert » sur une suite qui n'a jamais ouvert une seule connexion. Le fan-out entre
|
|
349
|
-
> workers d'un même pod
|
|
350
|
+
> workers d'un même pod ne demande aucune infrastructure externe, mais ne tourne que dans
|
|
351
|
+
> `npm run test:cluster` avec `NF_RUN_CLUSTER_E2E=1`.
|
|
350
352
|
>
|
|
351
353
|
> ```bash
|
|
352
354
|
> # Le seul run qui prouve réellement le fan-out entre machines.
|
|
353
|
-
> NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm test
|
|
355
|
+
> NF_RUN_CLUSTER_E2E=1 NF_REDIS_PASSWORD=nodefony-dev npm run test:cluster
|
|
354
356
|
> ```
|
|
355
357
|
|
|
356
358
|
## 🔗 Pour aller plus loin
|
|
@@ -367,6 +369,3 @@ l'infrastructure sous la main.
|
|
|
367
369
|
- 🏛️ **Transverse** : [vue d'ensemble du framework](../../../../../docs/architecture/vue-ensemble.md) ·
|
|
368
370
|
[le pipeline d'une requête](../../../../../docs/architecture/pipeline-requete.md)
|
|
369
371
|
- 📖 [Lexique général](../../../../../docs/lexique.md) du framework.
|
|
370
|
-
</content>
|
|
371
|
-
|
|
372
|
-
</invoke>
|
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: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:
|
|
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
|
|
@@ -447,7 +447,8 @@ après fusion et validation — la seule vue qui dit ce qui s'applique, plutôt
|
|
|
447
447
|
|
|
448
448
|
> [!CAUTION]
|
|
449
449
|
> `slowConsumer.bytes` ne pilote **que la métrique**. Les seuils qui jettent une frame ou ferment une
|
|
450
|
-
> connexion
|
|
450
|
+
> connexion se règlent côté `@nodefony/http` (`websocket.maxBackpressure`, `.backpressurePolicy`,
|
|
451
|
+
> `.backpressureCloseAfterDrops`). Baisser cette clé rend le signalement plus précoce ; ça
|
|
451
452
|
> ne change rien au comportement de la socket.
|
|
452
453
|
|
|
453
454
|
Couper la sonde cluster est un **contournement total**, pas une mise en sourdine : sans elle, il n'y a
|
|
@@ -552,7 +553,7 @@ aux sondes qui instrumentent chaque opération et se paient donc à l'usage.
|
|
|
552
553
|
| `publishTotal` grimpe, `fanoutTotal` stagne | Tu publies dans le vide : personne n'est abonné au canal visé | Comparer le nom publié et le nom souscrit — presque toujours une faute de frappe |
|
|
553
554
|
| Deux appels consécutifs à `/api/health` donnent des chiffres contradictoires | En cluster, la requête tombe sur un worker au hasard et rend une vue per-instance | Lire le champ `cluster` ; si absent, la sonde pod est coupée ou en démarrage à froid |
|
|
554
555
|
| La mémoire du processus grimpe sans fuite apparente | Une sonde alloue à chaque tick, ou un client lent fait grossir la file d'envoi | Lire `backpressure.totalBufferedAmount` ; simplifier le corps du tick |
|
|
555
|
-
| Baisser `slowConsumer.bytes` ne ferme pas les clients lents | Cette clé pilote le **comptage** de la sonde ; les seuils d'action sont
|
|
556
|
+
| Baisser `slowConsumer.bytes` ne ferme pas les clients lents | Cette clé pilote le **comptage** de la sonde ; les seuils d'action sont ceux du serveur WebSocket | Régler `maxBackpressure` / `backpressureCloseAfterDrops` dans `@nodefony/http` |
|
|
556
557
|
| La cadence demandée n'est pas respectée | Elle a été **ramenée dans les bornes** du canal, silencieusement | Lire les bornes du canal ; vérifier le nom réellement souscrit dans `channels[]` |
|
|
557
558
|
| Deux abonnés au même canal, deux minuteurs | Ils ont demandé des cadences différentes — un canal cadencé est distinct par cadence | Laisser la cadence par défaut (aucun suffixe) quand elle convient : les abonnés partagent un minuteur |
|
|
558
559
|
| Rien n'est visible alors que le trafic est réel | La sonde n'observe **jamais** ce à quoi personne n'est abonné : un canal sans abonné n'existe pas | Ouvrir l'écran ou s'abonner ; c'est le comportement voulu, pas une panne |
|
|
@@ -566,7 +567,7 @@ infrastructure, puisque la sonde est une lecture pure.
|
|
|
566
567
|
| Suite | Ce qui est prouvé |
|
|
567
568
|
| --- | --- |
|
|
568
569
|
| `RealtimeHub.test.ts` | Hub vide → instantané à zéro ; abonnés et publications par canal ; `publishTotal` vs `fanoutTotal` ; comptage des consommateurs lents et `maxBufferedAmount` ; sortie du registre à la déconnexion ; frames entrantes ; descripteur `local` du backplane |
|
|
569
|
-
| `WsConnectionTransport.test.ts` | Les
|
|
570
|
+
| `WsConnectionTransport.test.ts` | Les seuils de contre-pression : abandon « le dernier gagne », puis fermeture `1013` |
|
|
570
571
|
| `ClusterProbeClient.test.ts` | Fusion pod (sommes vs maximum), report périodique et cache du snapshot, forage `enrich`/`rich`, repli per-instance quand la sonde est absente |
|
|
571
572
|
|
|
572
573
|
**Ce qui manque, et qu'il faut savoir** : il n'existe pas de banc de charge dédié à la sonde
|
|
@@ -588,6 +589,3 @@ npm run coverage # rapport vitest — jamais de pourcentage figé dans cette p
|
|
|
588
589
|
- [Sécurité](securite.md) — qui a le droit de s'abonner à un canal de santé, et pourquoi ça compte.
|
|
589
590
|
- [Cookbook — un chat](cookbook-chat.md) — le patron appliqué de bout en bout sur une vraie fonctionnalité.
|
|
590
591
|
- [Vocabulaire](vocabulaire.md) — les douze mots de la socket.
|
|
591
|
-
</content>
|
|
592
|
-
|
|
593
|
-
</invoke>
|