@nodefony/realtime 10.0.0-beta.1 → 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/docs/actions.md +13 -13
- package/docs/architecture.md +17 -17
- package/docs/configuration.md +29 -24
- package/docs/cookbook-chat.md +9 -7
- package/docs/index.md +12 -10
- package/docs/observabilite.md +4 -3
- package/docs/protocole.md +7 -4
- package/docs/securite.md +21 -17
- package/docs/vocabulaire.md +5 -5
- package/package.json +9 -9
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.152.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,8 @@ import { ClusterBackplane, processIpcTransport } from "./nodefony/src/backplane/
|
|
|
15
15
|
import { getBackplaneDriver, listBackplaneDrivers, registerBackplaneDriver } from "./nodefony/src/backplane/backplaneRegistry.js";
|
|
16
16
|
import { ClusterProbeClient, clusterProbeHealth, clusterProbeInstance, clusterProbeRequestEnrich, mergeClusterHealth, processProbeTransport, setClusterProbeClient } from "./nodefony/src/cluster/ClusterProbeClient.js";
|
|
17
17
|
import { buildOwnHealth, buildRealtimeHealth, createRealtimeAdminApi } from "./nodefony/src/server/RealtimeAdminApi.js";
|
|
18
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
19
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
18
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
|
|
19
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
|
|
20
20
|
import { Kernel, Module, services, withTimeout } from "nodefony";
|
|
21
21
|
//#region index.ts
|
|
22
22
|
/**
|
package/docs/actions.md
CHANGED
|
@@ -485,7 +485,7 @@ prudente est donc de **s'abonner avant** de lancer l'appel.
|
|
|
485
485
|
|
|
486
486
|
Nodefony a choisi la garantie côté serveur plutôt que la discipline côté client : le producteur du
|
|
487
487
|
canal **rejoue son historique** au nouvel abonné, de sorte qu'un arrivant tardif voit tout depuis
|
|
488
|
-
le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:
|
|
488
|
+
le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:456`). C'est ce qui autorise le front
|
|
489
489
|
à faire l'appel d'abord et à s'abonner ensuite, sans rien perdre.
|
|
490
490
|
|
|
491
491
|
> [!TIP]
|
|
@@ -524,18 +524,18 @@ passe par le pont pour tout ce qui est déjà une route. Le détail du pont vit
|
|
|
524
524
|
|
|
525
525
|
## ⚠️ Pièges
|
|
526
526
|
|
|
527
|
-
| Symptôme
|
|
528
|
-
|
|
|
529
|
-
| `RPC timeout: <méthode>` alors que le serveur a bien répondu
|
|
530
|
-
| Le client attend indéfiniment, aucune erreur
|
|
531
|
-
| `-32601 method not found`
|
|
532
|
-
| `-32603 internal error` sans détail
|
|
533
|
-
| `-32001 unauthorized` sur une action légitime
|
|
534
|
-
| Une action
|
|
535
|
-
| Un travail relancé crée deux jobs
|
|
536
|
-
| `abort()` introuvable sur la socket
|
|
537
|
-
| Les premières lignes d'un job manquent
|
|
538
|
-
| Toutes les `Promise` rejettent d'un coup
|
|
527
|
+
| Symptôme | Cause | Correction |
|
|
528
|
+
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
529
|
+
| `RPC timeout: <méthode>` alors que le serveur a bien répondu | le handler dure plus que 30 s, le défaut | passer le délai en **3ᵉ argument** : `request(m, p, 120_000)` |
|
|
530
|
+
| Le client attend indéfiniment, aucune erreur | le handler ne rend jamais (il publie au lieu de retourner) — pas de frame `result` | toujours `return` une valeur ; publier **en plus**, jamais **à la place** |
|
|
531
|
+
| `-32601 method not found` | nom mal orthographié, ou action déclarée sur un **autre** endpoint | vérifier `socket.serverMethods` — c'est la liste réelle de CETTE connexion |
|
|
532
|
+
| `-32603 internal error` sans détail | un `throw` ordinaire est rendu opaque au client (Zero Trust) | lever une `RpcError` avec un code et un message publiables |
|
|
533
|
+
| `-32001 unauthorized` sur une action légitime | le nom commence par `nodefony:`, le namespace réservé à la plateforme | renommer hors de `nodefony:`, ou obtenir `ROLE_NODEFONY_ADMIN` |
|
|
534
|
+
| Une action est refusée à un anonyme alors qu'on la croyait publique | une action est fermée par défaut (`{ authenticated: true }`) | déclarer `{ authenticated: false }` pour l'ouvrir ; un rôle se pose par une règle de préfixe |
|
|
535
|
+
| Un travail relancé crée deux jobs | action non idempotente rejouée après une reconnexion | action compagnon d'annulation, ou identifiant fourni par l'appelant + mémorisation |
|
|
536
|
+
| `abort()` introuvable sur la socket | il n'existe pas — et n'arrêterait pas le serveur de toute façon | exposer une action d'annulation qui prend l'identifiant du travail |
|
|
537
|
+
| Les premières lignes d'un job manquent | abonnement au canal après le début de la production, sans historique rejoué | s'abonner **avant** de lancer, ou faire rejouer l'historique par le producteur |
|
|
538
|
+
| Toutes les `Promise` rejettent d'un coup | la connexion s'est fermée : `dispose()` vide la table des appels en attente | attendu ; relancer après reconnexion, l'appel n'a pas survécu |
|
|
539
539
|
|
|
540
540
|
## 🧪 Tests & couverture
|
|
541
541
|
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
@@ -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,10 +273,16 @@ 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
|
|
|
@@ -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
|
|
@@ -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
|
@@ -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.
|
|
@@ -1616,4 +1618,4 @@ Pour qui veut lire l'implémentation derrière chaque étape :
|
|
|
1616
1618
|
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:213`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
|
|
1617
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:1286`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.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).
|
|
@@ -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
|
package/docs/observabilite.md
CHANGED
|
@@ -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
|
package/docs/protocole.md
CHANGED
|
@@ -348,8 +348,9 @@ ouvertes à l'application. Colonne `id` : présent = requête (réponse due), ab
|
|
|
348
348
|
| `<action>` | client→server | **oui** | Toute action déclarée par `@RealtimeAction` | `realtimeDecorators.ts:101` |
|
|
349
349
|
|
|
350
350
|
> [!TIP]
|
|
351
|
-
> **L'accueil est ta carte du territoire.** `
|
|
352
|
-
>
|
|
351
|
+
> **L'accueil est ta carte du territoire.** `channels` ne liste que les canaux que ce visiteur
|
|
352
|
+
> pourrait obtenir (filtrés par le même verrou que `subscribe`), `methods` les actions de l'endpoint
|
|
353
|
+
> (`RealtimeController.ts:710`) : un client peut activer ou griser
|
|
353
354
|
> ses commandes sans rien coder en dur. Côté navigateur, ils se lisent en `socket.serverMethods` et
|
|
354
355
|
> `socket.serverChannels`.
|
|
355
356
|
|
|
@@ -437,8 +438,10 @@ radicalement :
|
|
|
437
438
|
|
|
438
439
|
`IRealtimeDenied` (`RealtimeEventMap.ts:269`) porte `channel` et `reason` — plus un `detail` optionnel, posé hors production seulement — et le motif
|
|
439
440
|
est **générique**. Jamais « il te manque `ROLE_ADMIN` » : ce serait un oracle d'autorisation, un
|
|
440
|
-
attaquant y lirait la carte des droits.
|
|
441
|
-
`limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts:811`)
|
|
441
|
+
attaquant y lirait la carte des droits. Trois motifs circulent : `forbidden` (le verrou a dit non),
|
|
442
|
+
`limit` (le plafond de canaux de la connexion est atteint, `RealtimeController.ts:811`) et `unknown`
|
|
443
|
+
(aucun producteur ne sert ce nom — jamais un oracle : un canal gardé rend `forbidden`, qu'il existe ou
|
|
444
|
+
non). Côté client,
|
|
442
445
|
`onDenied()` (`RealtimeClient.ts:469`) branche un handler dessus.
|
|
443
446
|
|
|
444
447
|
> [!CAUTION]
|
package/docs/securite.md
CHANGED
|
@@ -93,16 +93,16 @@ Les étapes 1 et 2 appartiennent à `@nodefony/http` et `@nodefony/security` : l
|
|
|
93
93
|
Une WebSocket authentifiée est une cible de choix : elle porte une identité, vit longtemps et
|
|
94
94
|
diffuse en continu.
|
|
95
95
|
|
|
96
|
-
| Attaque | Ce qui la bloque
|
|
97
|
-
| ------------------------------------------------------------------------------------------------------ |
|
|
98
|
-
| **CSWSH** — `evil.com` fait `new WebSocket("wss://app.exemple.com/rt")`, le navigateur joint le cookie | Contrôle d'`Origin` same-origin par défaut, puis allowlist stricte
|
|
99
|
-
| **Écoute des flux internes** — un visiteur s'abonne à `nodefony:syslog` et lit les logs du pod | Plancher des namespaces réservés (authentifié + `ROLE_NODEFONY_ADMIN`)
|
|
100
|
-
| **Élévation par canal métier** — un `ROLE_USER` s'abonne au canal admin d'un autre module | Policy déclarée sur le canal, évaluée avec la hiérarchie de rôles
|
|
101
|
-
| **Pont API plus permissif que REST** — `api.request {path}` pour contourner un 401 HTTP | Re-match de la MÊME zone firewall que `GET {path}`
|
|
102
|
-
| **Socket zombie** — un admin se déconnecte, sa socket continue de diffuser | Re-validation périodique de l'identité, fermeture `4001`
|
|
103
|
-
| **DoS mémoire par abonnements** — une connexion ouvre des milliers de canaux | Plafond de canaux par connexion (256 par défaut)
|
|
104
|
-
| **DoS mémoire par lenteur** — un client ne lit pas, la file d'envoi enfle jusqu'à l'OOM | Jet de frames
|
|
105
|
-
| **Oracle d'autorisation** — sonder les canaux pour cartographier les droits | Motif de refus **générique** (`forbidden`), jamais le détail
|
|
96
|
+
| Attaque | Ce qui la bloque | Où |
|
|
97
|
+
| ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
98
|
+
| **CSWSH** — `evil.com` fait `new WebSocket("wss://app.exemple.com/rt")`, le navigateur joint le cookie | Contrôle d'`Origin` same-origin par défaut, puis allowlist stricte | `@nodefony/http` transport, puis `csrf.checkOrigin` realtime |
|
|
99
|
+
| **Écoute des flux internes** — un visiteur s'abonne à `nodefony:syslog` et lit les logs du pod | Plancher des namespaces réservés (authentifié + `ROLE_NODEFONY_ADMIN`) | Verrou de frame `@nodefony/security` |
|
|
100
|
+
| **Élévation par canal métier** — un `ROLE_USER` s'abonne au canal admin d'un autre module | Policy déclarée sur le canal, évaluée avec la hiérarchie de rôles | `@RealtimeChannel(name, { roles })` + verrou de frame |
|
|
101
|
+
| **Pont API plus permissif que REST** — `api.request {path}` pour contourner un 401 HTTP | Re-match de la MÊME zone firewall que `GET {path}` | Verrou de frame, surface `api.request` |
|
|
102
|
+
| **Socket zombie** — un admin se déconnecte, sa socket continue de diffuser | Re-validation périodique de l'identité, fermeture `4001` | Tick de révocation du hub |
|
|
103
|
+
| **DoS mémoire par abonnements** — une connexion ouvre des milliers de canaux | Plafond de canaux par connexion (256 par défaut) | `limits.maxChannelsPerConnection` |
|
|
104
|
+
| **DoS mémoire par lenteur** — un client ne lit pas, la file d'envoi enfle jusqu'à l'OOM | Jet de frames au-delà de 4 MiB, fermeture `1013` après 1000 jets d'affilée | Back-pressure du transport WS |
|
|
105
|
+
| **Oracle d'autorisation** — sonder les canaux pour cartographier les droits | Motif de refus **générique** (`forbidden`), jamais le détail | `realtime:denied` |
|
|
106
106
|
|
|
107
107
|
> [!IMPORTANT]
|
|
108
108
|
> Les lignes 2 à 5 supposent `@nodefony/security` chargé **avec au moins une zone protégée**. Sans
|
|
@@ -517,12 +517,16 @@ politique déclarée, alors que ce module en déclare pour d'autres canaux. […
|
|
|
517
517
|
|
|
518
518
|
Deux façons de fermer, selon l'intention :
|
|
519
519
|
|
|
520
|
-
- **
|
|
521
|
-
|
|
520
|
+
- **la famille doit être gardée** → déclarer un **motif** : `@RealtimeChannel("chat:room:*", { roles })`
|
|
521
|
+
garde tous les canaux de la famille ; l'avertissement propose le motif à écrire et se tait dès
|
|
522
|
+
qu'un motif couvre le canal ;
|
|
523
|
+
- **le canal doit être gardé seul** → l'exposer sous un nom déclaré (un canal par nom exact), ou
|
|
524
|
+
porter la vérification **dans la fabrique** elle-même, qui reçoit le nom demandé et peut refuser
|
|
525
|
+
(`null`) ;
|
|
522
526
|
- **le canal est public** → rien à faire, l'avertissement dit seulement qu'il n'est pas gardé.
|
|
523
527
|
|
|
524
|
-
>
|
|
525
|
-
>
|
|
528
|
+
> Le nom exact l'emporte sur un motif ; entre deux motifs, le plus spécifique gagne. Un `*` dans un
|
|
529
|
+
> nom souscrit est refusé : il désigne un motif de politique, jamais un canal.
|
|
526
530
|
|
|
527
531
|
### ⚠️ La condition d'activation — le point critique
|
|
528
532
|
|
|
@@ -676,7 +680,7 @@ canaux). `WsConnectionTransport.send()` (`WsConnectionTransport.ts:82`) applique
|
|
|
676
680
|
|
|
677
681
|
| `bufferedAmount` | Action |
|
|
678
682
|
| ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
|
679
|
-
| > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:
|
|
683
|
+
| > `websocket.maxBackpressure` — 4 MiB par défaut (`http`, `http/nodefony/config/config.ts:1048`) | politique `websocket.backpressurePolicy` : `drop` (défaut) jette la frame — canaux d'état, le prochain snapshot la remplace |
|
|
680
684
|
| `websocket.backpressureCloseAfterDrops` drops CONSÉCUTIFS — 1000 par défaut | `close(1013)` « Try Again Later » ; le client se reconnecte et resynchronise |
|
|
681
685
|
|
|
682
686
|
### Taille des messages entrants
|
|
@@ -693,7 +697,7 @@ au défaut de la librairie `ws`.
|
|
|
693
697
|
| Limite de **fréquence** des frames entrantes | Un client authentifié peut inonder le peer ; seul le coût CPU le freine |
|
|
694
698
|
| Limite de **connexions par IP ou par utilisateur** | Rien n'empêche N sockets par client au niveau du module |
|
|
695
699
|
| Plafond **global** de canaux du process | Le plafond est par connexion ; M connexions × 256 canaux reste possible |
|
|
696
|
-
| Seuils de back-pressure
|
|
700
|
+
| Seuils de back-pressure dans ce module | aucun réglage dans `@nodefony/realtime` : ils se règlent sur le serveur WebSocket de `@nodefony/http` (`websocket.maxBackpressure`, `.backpressureCloseAfterDrops`) |
|
|
697
701
|
|
|
698
702
|
## ⚙️ Configuration de sécurité
|
|
699
703
|
|
|
@@ -795,7 +799,7 @@ n'est pas appliquée.
|
|
|
795
799
|
| Un admin déconnecté garde ses flux pendant une dizaine de secondes | Le tick de révocation est périodique (30 s), pas immédiat | Comportement attendu ; pour une coupure immédiate, fermer la socket côté serveur |
|
|
796
800
|
| Une session révoquée ne ferme **jamais** la socket | La session n'était pas lisible au handshake → revalidateur `null`, `isValid()` répond toujours `true` | Vérifier que le handshake traverse bien la zone (session chargée avant le controller realtime) |
|
|
797
801
|
| Le client se croit abonné, ne reçoit rien | Refus d'autorisation, plafond atteint, **ou nom de canal sans producteur** (pas de réponse RPC) | Écouter `realtime:denied` : `forbidden` (droits), `limit` (borne), `unknown` (le nom ne désigne rien) |
|
|
798
|
-
| `slowConsumer.bytes` augmenté, les frames sont toujours jetées à
|
|
802
|
+
| `slowConsumer.bytes` augmenté, les frames sont toujours jetées à 4 MiB | Cette clé pilote le **comptage** de la sonde, pas les seuils de drop/close du transport | Régler `websocket.maxBackpressure` / `.backpressureCloseAfterDrops` dans `@nodefony/http` |
|
|
799
803
|
| Deux déploiements se parlent en cross-talk | Pas de `backplane.namespace` sur un Redis mutualisé (la base Redis ne cloisonne pas le pub/sub) | Poser un `namespace` explicite par déploiement |
|
|
800
804
|
| Le fan-out cross-pod s'arrête après avoir posé un secret | `backplane.secret` différent d'un pod à l'autre : les messages sont scellés, aucun ne se vérifie | Le **même** secret sur tous les pods (`NF_REALTIME_BACKPLANE_SECRET`) ; suivre `ingressRejectedTotal` |
|
|
801
805
|
| Un canal broadcast ne reçoit rien des autres pods | Le préfixe n'est pas déclaré côté receveur → l'entrée refuse le canal (compté) | Déclarer le préfixe (`broadcast` du controller) sur **tous** les pods, pas seulement l'émetteur |
|
package/docs/vocabulaire.md
CHANGED
|
@@ -31,7 +31,7 @@ source: "src/packages/@nodefony/realtime/docs/vocabulaire.md"
|
|
|
31
31
|
# Vocabulaire — le lexique de la socket Nodefony
|
|
32
32
|
|
|
33
33
|
> Cette page est le **dictionnaire** du temps réel Nodefony : chaque mot y a une définition, une
|
|
34
|
-
> raison d'être ce mot-là, et le symbole du code qui l'incarne. Les
|
|
34
|
+
> raison d'être ce mot-là, et le symbole du code qui l'incarne. Les huit autres pages du module
|
|
35
35
|
> l'emploient sans le redéfinir. Règle de lecture : quand une discussion realtime s'embrouille,
|
|
36
36
|
> demande-toi **de quoi on parle — de la prise, de l'autocom, ou du fond de panier ?** Neuf fois
|
|
37
37
|
> sur dix, la confusion tenait à un seul mot.
|
|
@@ -302,7 +302,7 @@ front**. Une différence assumée : `request()` n'y est pas supporté — au-des
|
|
|
302
302
|
### `accueil` (welcome) — la première frame
|
|
303
303
|
|
|
304
304
|
La notification `realtime:welcome`, poussée par le serveur juste après le handshake. Elle annonce le
|
|
305
|
-
protocole, les canaux
|
|
305
|
+
protocole, les canaux que ce visiteur peut obtenir, les actions de l'endpoint, et l'identité résolue. Un client
|
|
306
306
|
attend cette frame avant de pousser quoi que ce soit.
|
|
307
307
|
|
|
308
308
|
`IRealtimeWelcome` (`RealtimeEventMap.ts:231`).
|
|
@@ -762,7 +762,7 @@ n° 1 : il monte quand le client n'absorbe plus. Deux seuils distincts existent
|
|
|
762
762
|
Seuil de comptage `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:63`) — une constante, car il ne
|
|
763
763
|
décide de rien. Les seuils d'ACTION, eux, sont des réglages : `websocket.maxBackpressure`,
|
|
764
764
|
`websocket.backpressurePolicy` et `websocket.backpressureCloseAfterDrops` (`http`,
|
|
765
|
-
`http/nodefony/config/config.ts:
|
|
765
|
+
`http/nodefony/config/config.ts:1048`).
|
|
766
766
|
|
|
767
767
|
### `consommateur lent` — la connexion qui décroche
|
|
768
768
|
|
|
@@ -829,8 +829,8 @@ l'aperçu, régénérée depuis vitest et jamais figée ici :
|
|
|
829
829
|
- **intégration** : le driver Redis contre un serveur réel ;
|
|
830
830
|
- **bout en bout** : boucle locale, chemins de contrôleur, autorisation de canal, câblage du
|
|
831
831
|
pare-feu, cluster par communication inter-process, cluster Redis ;
|
|
832
|
-
- **attaque** : plafond de canaux, révocation, politique non appliquée
|
|
833
|
-
du lexique ont chacun leur banc hostile.
|
|
832
|
+
- **attaque** : plafond de canaux, révocation, politique non appliquée, injection par le bus — les
|
|
833
|
+
pièges de sécurité du lexique ont chacun leur banc hostile.
|
|
834
834
|
|
|
835
835
|
Couverture : `npm run coverage` dans `@nodefony/realtime`.
|
|
836
836
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/realtime",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.2",
|
|
4
4
|
"description": "Couche temps réel serveur de Nodefony : canaux, appels bidirectionnels JSON-RPC 2.0, contre-pression, et diffusion entre plusieurs instances (boucle locale, IPC de grappe, Redis)",
|
|
5
5
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
6
6
|
"type": "module",
|
|
@@ -41,20 +41,20 @@
|
|
|
41
41
|
"esm"
|
|
42
42
|
],
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
45
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
46
|
-
"nodefony": "^10.0.0-beta.
|
|
44
|
+
"@nodefony/framework": "^10.0.0-beta.2",
|
|
45
|
+
"@nodefony/http": "^10.0.0-beta.2",
|
|
46
|
+
"nodefony": "^10.0.0-beta.2",
|
|
47
47
|
"reflect-metadata": "0.2.2",
|
|
48
48
|
"zod": "^4.6.5"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
52
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
51
|
+
"@nodefony/framework": "^10.0.0-beta.2",
|
|
52
|
+
"@nodefony/http": "^10.0.0-beta.2",
|
|
53
53
|
"@types/node": "26.6.3",
|
|
54
|
-
"@vitest/coverage-v8": "5.0.
|
|
55
|
-
"nodefony": "^10.0.0-beta.
|
|
54
|
+
"@vitest/coverage-v8": "5.0.3",
|
|
55
|
+
"nodefony": "^10.0.0-beta.2",
|
|
56
56
|
"reflect-metadata": "0.2.2",
|
|
57
|
-
"vitest": "5.0.
|
|
57
|
+
"vitest": "5.0.3"
|
|
58
58
|
},
|
|
59
59
|
"private": false,
|
|
60
60
|
"engines": {
|