@nodefony/realtime 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +207 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +218 -0
- package/dist/nodefony/config/config.js +63 -0
- package/dist/nodefony/config/defineModuleConfig.js +60 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
- package/dist/nodefony/interfaces/IBackplane.js +1 -0
- package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
- package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
- package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
- package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
- package/dist/nodefony/src/backplane/envelope.js +109 -0
- package/dist/nodefony/src/backplane/originId.js +35 -0
- package/dist/nodefony/src/backplane/publishQueue.js +96 -0
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
- package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
- package/dist/nodefony/src/server/RealtimeController.js +601 -0
- package/dist/nodefony/src/server/RealtimeHub.js +926 -0
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
- package/dist/nodefony/src/server/deniedDetail.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +105 -0
- package/dist/nodefony/src/server/welcomeEnv.js +25 -0
- package/dist/nodefony/src/service/RealtimeService.js +240 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
- package/dist/nodefony/testing/index.js +163 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/nodefony/config/config.d.ts +43 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
- package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
- package/dist/types/nodefony/interfaces/index.d.ts +18 -0
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
- package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
- package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
- package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
- package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
- package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
- package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +169 -0
- package/docs/actions.md +573 -0
- package/docs/architecture.md +889 -0
- package/docs/configuration.md +803 -0
- package/docs/cookbook-chat.md +1619 -0
- package/docs/couverture-bancs.md +139 -0
- package/docs/index.md +372 -0
- package/docs/observabilite.md +592 -0
- package/docs/protocole.md +518 -0
- package/docs/securite.md +845 -0
- package/docs/vocabulaire.md +845 -0
- package/package.json +83 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Ce que les bancs du temps réel n'éprouvent PAS"
|
|
3
|
+
navTitle: Couverture des bancs
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/realtime"
|
|
6
|
+
topic: realtime
|
|
7
|
+
coverageModule: realtime
|
|
8
|
+
section: "Realtime"
|
|
9
|
+
audience: [developer]
|
|
10
|
+
tags: [realtime, tests, couverture, bancs, trous, e2e, charge]
|
|
11
|
+
version: "doc"
|
|
12
|
+
status: stable
|
|
13
|
+
publish: false
|
|
14
|
+
source: "src/packages/@nodefony/realtime/docs/couverture-bancs.md"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Ce que les bancs du temps réel n'éprouvent PAS
|
|
18
|
+
|
|
19
|
+
Le temps réel est la clé de voûte du framework — HTTP et WebSocket dans le même
|
|
20
|
+
contexte de contrôleur est ce qui distingue Nodefony. Sa couverture de bancs a des
|
|
21
|
+
trous, et le vrai problème n'est pas qu'il en reste : c'est que **personne n'avait
|
|
22
|
+
établi la liste**. Tant qu'elle n'existe pas, chaque défaut se découvre par
|
|
23
|
+
accident — c'est arrivé deux fois en une semaine, dont une frame que le serveur
|
|
24
|
+
émettait et que le client jetait en silence.
|
|
25
|
+
|
|
26
|
+
Cette page est la liste. Elle se **remesure**, elle ne se relit pas de confiance.
|
|
27
|
+
|
|
28
|
+
## La mesure, et ce qu'elle vaut
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node scripts/realtime-coverage-map.mjs # le relevé lisible
|
|
32
|
+
node scripts/realtime-coverage-map.mjs --json # pour un autre outil
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Le critère est mécanique : **un fichier de test ATTEINT-il ce module ?** —
|
|
36
|
+
directement, ou à travers un module source lui-même testé. Il ne juge pas si le
|
|
37
|
+
test l'exerce vraiment : un import peut ne servir qu'à un décor.
|
|
38
|
+
Il balaie tout `src/`, plus les scripts de charge du skill `nodefony-load-test`,
|
|
39
|
+
et range chaque banc par son CHEMIN (`/tests/integration/`, `.e2e.`,
|
|
40
|
+
`/tests/websockets/` → jonction ; `.mjs` du skill → charge ; le reste → unitaire).
|
|
41
|
+
|
|
42
|
+
**Sa limite, à connaître avant de croire un verdict : atteint n'est pas
|
|
43
|
+
exercé.** Un module peut être importé pour un décor, ou traversé sans que sa
|
|
44
|
+
logique propre soit éprouvée — cela ne se voit qu'en lisant le banc. La colonne
|
|
45
|
+
« atteints à travers un module testé » est là pour ça : elle nomme le chemin,
|
|
46
|
+
elle ne délivre pas un quitus.
|
|
47
|
+
|
|
48
|
+
> ⚠️ Cet instrument a rendu **trois** verdicts faux avant d'être corrigé, et
|
|
49
|
+
> chacun accusait le code au lieu de lui-même. Une première version ne balayait
|
|
50
|
+
> que les tests du module `realtime` et accusait `BrowserWsTransport` : un
|
|
51
|
+
> périmètre trop étroit ne rend pas un verdict incomplet, il rend un verdict
|
|
52
|
+
> FAUX. La suivante ne voyait que l'import DIRECT et a déclaré `publishQueue`
|
|
53
|
+
> « éprouvé par personne » alors qu'un banc portant son nom
|
|
54
|
+
> (`backplanePublishQueue.test.ts`) l'exerce depuis des semaines à travers
|
|
55
|
+
> `RedisBackplane` — le relevé l'avait sous les yeux, dans sa propre colonne.
|
|
56
|
+
> Deux tickets ont été ouverts sur ces faux. Une carte de couverture se
|
|
57
|
+
> contrôle comme le reste : par le terrain, pas par sa sortie.
|
|
58
|
+
|
|
59
|
+
## Les trous, et ce qu'ils coûtent
|
|
60
|
+
|
|
61
|
+
### Aucun test ne les atteint
|
|
62
|
+
|
|
63
|
+
**Aucun.** Les trente modules source du temps réel sont atteints par au moins un
|
|
64
|
+
banc. Ce n'est pas un quitus — voir la section suivante, puis celle des modules
|
|
65
|
+
jamais éprouvés dans la jonction.
|
|
66
|
+
|
|
67
|
+
### Atteints seulement À TRAVERS un module testé
|
|
68
|
+
|
|
69
|
+
| Module | Chemin d'atteinte |
|
|
70
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
71
|
+
| `publishQueue` | via `RedisBackplane` — `backplanePublishQueue.test.ts` (11 cas : saturation, anti-famine, drainage, échec du bus, hystérésis de l'annonce, ordre du jeté, contrôle négatif synchrone) et le banc de contre-pression RÉELLE de `RedisBackplane.test.ts` contre un vrai bus. |
|
|
72
|
+
| `BrowserWsTransport` | via `RealtimeClient` — `client-isomorphe-e2e.test.ts` (`@nodefony/http`) monte un vrai serveur WSS et un vrai client. Sa logique propre (états, reconnexion, codes de fermeture) n'est éprouvée qu'à travers ce que le client en fait. |
|
|
73
|
+
|
|
74
|
+
### Éprouvés en unitaire, jamais dans la jonction
|
|
75
|
+
|
|
76
|
+
Ces modules ont des cas unitaires, mais **aucun banc ne les fait travailler avec
|
|
77
|
+
un serveur en face**. C'est la forme d'angle mort qui a laissé passer les deux
|
|
78
|
+
défauts de la semaine : la logique était juste des deux côtés, c'est le fil entre
|
|
79
|
+
les deux qui ne portait pas.
|
|
80
|
+
|
|
81
|
+
| Module | Éprouvé par | Ce que la jonction prouverait en plus |
|
|
82
|
+
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
83
|
+
| `AdaptiveRate` | `AdaptiveRate.test.ts` | La cadence est calculée côté client ; rien ne montre qu'elle ARRIVE au hub et qu'il l'applique. Le fil peut être coupé sans qu'un cas tombe. |
|
|
84
|
+
| `WsConnectionTransport` | `backpressureConfig.test.ts`, `WsConnectionTransport.test.ts` | Le seuil de consommateur lent est configuré et mesuré, jamais FRANCHI avec un vrai client au bout. |
|
|
85
|
+
| `LoopbackBackplane` | `channelOwnership.test.ts`, `RealtimeHub.test.ts` | Le fond de panier mono-processus est le défaut de toute application : c'est le seul chemin que personne ne prouve de bout en bout. |
|
|
86
|
+
| `originId` | `originId.test.ts`, `RealtimeHub.test.ts` | Deux pods qui se donnent le même identifiant d'origine (même nom d'hôte sous Kubernetes) laisseraient passer un double fan-out. |
|
|
87
|
+
| `deniedDetail`, `welcomeEnv` | leurs cas unitaires | Tous deux décident ce qu'un visiteur APPREND selon le mode d'exécution. Leur jonction est désormais couverte pour le welcome (`realtimeChannelAuth.e2e.test.ts`), pas pour le refus. |
|
|
88
|
+
| `notice`, `observe`, `localEvents`, `IRealtimeSocket` | cas unitaires du client | La surface que consomme une application (observables, avis) n'est jamais exercée contre un serveur réel. |
|
|
89
|
+
| `backplaneRegistry`, `ClusterProbeClient`, `RealtimeAdminApi`, `ClusterBackplane`, `channelRate`, `AnonymousRealtimeToken` | cf le relevé | Voir la carte complète ci-dessous. |
|
|
90
|
+
|
|
91
|
+
## La carte complète
|
|
92
|
+
|
|
93
|
+
| Module | Étages | Bancs (3 premiers) |
|
|
94
|
+
| ------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
95
|
+
| `AdaptiveRate` | unit · charge | `AdaptiveRate.test.ts`, `aimd-demo.mjs` |
|
|
96
|
+
| `AnonymousRealtimeToken` | unit | `RealtimeHubSecurity.test.ts`, `RealtimeController.test.ts`, `RealtimeService.test.ts` |
|
|
97
|
+
| `backplaneRegistry` | unit | `healthBackplaneDrivers.test.ts`, `backplaneRegistry.test.ts` |
|
|
98
|
+
| `channelRate` | unit | `platformChannels.test.ts`, `channelRate.test.ts` |
|
|
99
|
+
| `ClusterBackplane` | unit · charge | `ClusterBackplane.test.ts`, `backplaneRegistry.test.ts`, `cluster-ipc.mjs` |
|
|
100
|
+
| `ClusterProbeClient` | unit · charge | `ClusterProbeClient.test.ts`, `cluster-health-endpoint-e2e.mjs`, `cluster-orm-rich-e2e.mjs` |
|
|
101
|
+
| `deniedDetail` | unit | `deniedDetail.test.ts` |
|
|
102
|
+
| `envelope` | unit · e2e | `backplaneInjection.attack.test.ts`, `RedisBackplane.test.ts` |
|
|
103
|
+
| `IRealtimeSocket` | unit | `RealtimeSocket.test.ts`, `AdaptiveRate.test.ts` |
|
|
104
|
+
| `IRealtimeTransport` | unit · e2e | `clientAngular.test.ts`, `clientSvelte.test.ts`, `RealtimeSocket.test.ts` |
|
|
105
|
+
| `JsonRpcPeer` | unit · e2e | `JsonRpcPeer.types.test.ts`, `JsonRpcPeer.test.ts`, `realtimeControllerPaths.e2e.test.ts` |
|
|
106
|
+
| `localEvents` | unit | `clientObserve.test.ts` |
|
|
107
|
+
| `LoopbackBackplane` | unit | `channelOwnership.test.ts`, `RealtimeHub.test.ts`, `backplaneRegistry.test.ts` |
|
|
108
|
+
| `notice` | unit | `RealtimeNotice.test.ts`, `clientObserve.test.ts` |
|
|
109
|
+
| `observe` | unit · charge | `clientObserve.test.ts`, `db-backend-cost.mjs`, `aimd-demo.mjs` |
|
|
110
|
+
| `originId` | unit · charge | `originId.test.ts`, `RealtimeHub.test.ts`, `cluster-ipc.mjs` |
|
|
111
|
+
| `platformChannels` | unit · e2e | `platformChannels.test.ts`, `clientSyslogUplink.test.ts`, `clientObserve.test.ts` |
|
|
112
|
+
| `RealtimeAdminApi` | unit | `healthBackplaneDrivers.test.ts` |
|
|
113
|
+
| `RealtimeClient` | unit · e2e | `NodefonyProvider.test.ts`, `clientAngular.test.ts`, `clientSvelte.test.ts` |
|
|
114
|
+
| `RealtimeController` | unit · e2e | `realtimeChannelCap.attack.test.ts`, `realtimeUnknownChannel.test.ts`, `RealtimeController.test.ts` |
|
|
115
|
+
| `RealtimeEventMap` | unit · e2e | `JsonRpcPeer.types.test.ts`, `RealtimeClient.types.test.ts`, `JsonRpcPeer.test.ts` |
|
|
116
|
+
| `RealtimeHub` | unit · e2e · charge | `realtimeUnenforcedPolicy.attack.test.ts`, `realtimeRevocation.attack.test.ts`, `realtimeChannelCap.attack.test.ts` |
|
|
117
|
+
| `RealtimeService` | unit · e2e | `RealtimeService.test.ts`, `realtimeFirewallWiring.e2e.test.ts` |
|
|
118
|
+
| `RedisBackplane` | unit · e2e | `RedisBackplane.test.ts`, `backplaneInjection.attack.test.ts`, `backplanePublishQueue.test.ts` |
|
|
119
|
+
| `ServerRealtimeSocket` | unit · e2e | `channelOwnership.test.ts`, `realtimeLoopback.e2e.test.ts` |
|
|
120
|
+
| `syslogUplink` | unit · e2e | `syslogUplink.test.ts`, `syslogUplink.e2e.test.ts` |
|
|
121
|
+
| `welcomeEnv` | unit | `welcomeEnv.test.ts` |
|
|
122
|
+
| `WsConnectionTransport` | unit | `backpressureConfig.test.ts`, `WsConnectionTransport.test.ts` |
|
|
123
|
+
|
|
124
|
+
## Ce qui est éprouvé, et bien
|
|
125
|
+
|
|
126
|
+
- **La jonction client ↔ serveur** : `realtimeLoopback.e2e.test.ts` relie un VRAI
|
|
127
|
+
`RealtimeClient` à un VRAI `RealtimeController` par un câble en mémoire.
|
|
128
|
+
- **L'autorisation, identité par identité** : `realtimeChannelAuth.e2e.test.ts` —
|
|
129
|
+
matrice `identité × canal` pour `subscribe`, et désormais pour ce que le
|
|
130
|
+
`realtime:welcome` ANNONCE.
|
|
131
|
+
- **Le fond de panier Redis et l'IPC de cluster**, y compris cross-pod, derrière
|
|
132
|
+
leurs variables d'infrastructure (`NF_REDIS_TEST_URL`, `NF_RUN_CLUSTER_E2E`) —
|
|
133
|
+
**absentes, ces suites se sautent, et un banc sauté compte comme vert**.
|
|
134
|
+
|
|
135
|
+
## Ce que cette page ne couvre pas
|
|
136
|
+
|
|
137
|
+
Les scripts de charge sont comptés par la MENTION du nom, pas par un import :
|
|
138
|
+
ils parlent au serveur, ils ne l'importent pas. Leur colonne dit donc « un script
|
|
139
|
+
de charge parle de ce mécanisme », jamais « il le mesure ».
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "@nodefony/realtime — la socket Nodefony"
|
|
3
|
+
navTitle: "@nodefony/realtime"
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/realtime"
|
|
6
|
+
topic: realtime
|
|
7
|
+
section: "Temps réel"
|
|
8
|
+
audience: [developer, devops]
|
|
9
|
+
tags:
|
|
10
|
+
[
|
|
11
|
+
realtime,
|
|
12
|
+
websocket,
|
|
13
|
+
json-rpc,
|
|
14
|
+
canaux,
|
|
15
|
+
fan-out,
|
|
16
|
+
backplane,
|
|
17
|
+
cluster,
|
|
18
|
+
hub,
|
|
19
|
+
socket,
|
|
20
|
+
]
|
|
21
|
+
version: "doc"
|
|
22
|
+
status: stable
|
|
23
|
+
updated: 2026-07-19
|
|
24
|
+
source: "src/packages/@nodefony/realtime/docs/index.md"
|
|
25
|
+
coverageModule: realtime
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# @nodefony/realtime — la socket Nodefony
|
|
29
|
+
|
|
30
|
+
> Une seule connexion WebSocket, et **N canaux logiques** qui circulent dessus dans les deux sens.
|
|
31
|
+
> Ce module apporte le broker qui distribue ces canaux à l'intérieur d'un processus, et le
|
|
32
|
+
> **backplane** qui prolonge la distribution entre processus quand l'application passe à plusieurs
|
|
33
|
+
> répliques. Le contrôleur temps réel reste un contrôleur Nodefony ordinaire : même routage, même
|
|
34
|
+
> injection, mêmes gardes que côté web — c'est là que se joue le différenciateur du framework.
|
|
35
|
+
|
|
36
|
+
📍 [Documentation](../../../../../docs/index.md) › **@nodefony/realtime**
|
|
37
|
+
|
|
38
|
+
## 🧭 Par où commencer
|
|
39
|
+
|
|
40
|
+
Trois parcours selon ce que tu viens faire. L'ordre compte : chaque étape suppose la précédente.
|
|
41
|
+
|
|
42
|
+
**Je découvre le temps réel Nodefony** — comprendre le modèle avant d'écrire quoi que ce soit.
|
|
43
|
+
|
|
44
|
+
1. [Vocabulaire](./vocabulaire.md) — socket, hub, canal, frame, backplane. **Commence ici** : les
|
|
45
|
+
pages suivantes emploient ces mots sans les redéfinir, et la moitié des malentendus vient de là.
|
|
46
|
+
2. [Ce que le module apporte](#-ce-que-le-module-apporte) — pourquoi une couche de plus au-dessus de
|
|
47
|
+
la WebSocket, et ce qu'elle t'évite d'écrire.
|
|
48
|
+
3. [Architecture](./architecture.md) — le trajet complet d'une frame, du câble jusqu'à ta méthode.
|
|
49
|
+
4. [Cookbook — un chat](./cookbook-chat.md) — l'exemple intégrateur, de bout en bout.
|
|
50
|
+
|
|
51
|
+
**Je passe d'un processus à plusieurs** — le moment où le temps réel casse sans prévenir. En
|
|
52
|
+
mono-processus tout marchait ; à deux répliques, un message publié sur l'une n'atteint jamais un
|
|
53
|
+
abonné connecté à l'autre.
|
|
54
|
+
|
|
55
|
+
1. [Configuration](./configuration.md) — choisir le driver de backplane, et le déclarer. C'est la
|
|
56
|
+
seule chose qui change entre développement et production.
|
|
57
|
+
2. [Architecture](./architecture.md) — ce que le backplane transporte réellement, et ce qu'il ne
|
|
58
|
+
transporte pas : un canal ne franchit les processus que si tu l'as **déclaré diffusable**.
|
|
59
|
+
3. [Observabilité](./observabilite.md) — vérifier que le backplane est bien celui que tu crois :
|
|
60
|
+
la sonde annonce le driver **effectif**, pas celui que tu as écrit dans la configuration.
|
|
61
|
+
4. [`@nodefony/redis`](../../redis/docs/index.md) — le module qui fournit les connexions pub/sub
|
|
62
|
+
consommées par le driver `redis`.
|
|
63
|
+
|
|
64
|
+
**Je sécurise une socket ouverte sur Internet** — la passe qu'on regrette de ne pas avoir faite.
|
|
65
|
+
|
|
66
|
+
1. [Sécurité](./securite.md) — qui se connecte, ce qu'il a le droit d'écouter, ce qu'il peut pousser.
|
|
67
|
+
2. [Configuration](./configuration.md) — le contrôle d'`Origin` à la poignée de main et le plafond
|
|
68
|
+
de canaux par connexion : deux réglages, deux classes d'abus fermées.
|
|
69
|
+
3. [`@nodefony/security`](../../security/docs/index.md) — le pare-feu applicatif, qui protège le
|
|
70
|
+
temps réel avec **le même** modèle de zones que le web.
|
|
71
|
+
|
|
72
|
+
## 🗂️ Les pages du module
|
|
73
|
+
|
|
74
|
+
Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce qu'on y trouve.
|
|
75
|
+
|
|
76
|
+
| Page | Ce qu'elle résout | Tu en as besoin quand… |
|
|
77
|
+
| ---------------------------------------- | -------------------------------------------------- | -------------------------------------------- |
|
|
78
|
+
| [Vocabulaire](./vocabulaire.md) | les mots du domaine, une bonne fois | tu lis la doc, ou tu discutes archi |
|
|
79
|
+
| [Architecture](./architecture.md) | le trajet d'une frame, étage par étage | tu veux comprendre plutôt que régler |
|
|
80
|
+
| [Configuration](./configuration.md) | drivers, cloisonnement, bornes, contrôle d'origine | tu déploies, ou tu changes de topologie |
|
|
81
|
+
| [Sécurité](./securite.md) | identité à la poignée de main, droits par canal | ta socket est joignable depuis un navigateur |
|
|
82
|
+
| [Protocole](./protocole.md) | la grammaire d'une frame, et les codes d'erreur | tu débogues le fil, ou tu écris un client |
|
|
83
|
+
| [Actions RPC](./actions.md) | appeler le serveur et attendre une réponse | tu veux savoir si l'appel a marché |
|
|
84
|
+
| [Observabilité](./observabilite.md) | la sonde, les canaux de santé, les écrans | tu te demandes si ta socket va bien |
|
|
85
|
+
| [Cookbook — un chat](./cookbook-chat.md) | l'exemple complet, client et serveur | tu veux du code qui marche tout de suite |
|
|
86
|
+
|
|
87
|
+
```nodefony-cards
|
|
88
|
+
[
|
|
89
|
+
{ "icon": "📖", "title": "vocabulaire", "href": "./vocabulaire.md",
|
|
90
|
+
"desc": "Socket, hub, peer, canal, frame, fan-out, backplane, sonde : chaque terme part d'une analogie concrète, puis donne le symbole réel du code.",
|
|
91
|
+
"meta": "à lire en premier — la plus courte, la plus rentable" },
|
|
92
|
+
{ "icon": "🏗️", "title": "architecture", "href": "./architecture.md",
|
|
93
|
+
"desc": "La pile complète, du transport WebSocket jusqu'à la méthode de ton contrôleur — puis le chemin inverse quand le serveur publie.",
|
|
94
|
+
"meta": "comprendre plutôt que régler" },
|
|
95
|
+
{ "icon": "⚙️", "title": "configuration", "href": "./configuration.md",
|
|
96
|
+
"desc": "Le schéma du module, ses valeurs d'usine réelles, et surtout le choix du driver de backplane. Avec le cloisonnement par espace de nommage et les bornes anti-abus.",
|
|
97
|
+
"meta": "avant la mise en production" },
|
|
98
|
+
{ "icon": "🛡️", "title": "securite", "href": "./securite.md",
|
|
99
|
+
"desc": "Une WebSocket s'authentifie une fois, à l'ouverture, puis reste ouverte des heures : ce n'est pas le modèle du web, et ça change tout. Les points de greffe où @nodefony/security pose l'identité, autorise les canaux et journalise les refus.",
|
|
100
|
+
"meta": "ta socket est joignable depuis un navigateur" },
|
|
101
|
+
{ "icon": "📜", "title": "protocole", "href": "./protocole.md",
|
|
102
|
+
"desc": "La socket parle JSON-RPC 2.0, et une frame n'a que trois formes : une notification, une requête qui attend une réponse, une réponse. Grammaire champ par champ, méthodes nominales, et codes d'erreur réellement émis.",
|
|
103
|
+
"meta": "déboguer le fil, ou écrire un client" },
|
|
104
|
+
{ "icon": "↔️", "title": "actions", "href": "./actions.md",
|
|
105
|
+
"desc": "Publier ne dit jamais si le message est arrivé ; une action RPC, si. Le critère de choix entre les deux, l'abandon et le rejeu — et pourquoi un flux se diffuse sur un canal plutôt que de tenir dans une réponse.",
|
|
106
|
+
"meta": "savoir si l'appel a marché" },
|
|
107
|
+
{ "icon": "📡", "title": "observabilite", "href": "./observabilite.md",
|
|
108
|
+
"desc": "Ce que la sonde du hub expose, les canaux de santé et leur cadence, ce que Studio en montre — et comment écrire la sienne sans faire fuir la mémoire ni empêcher le processus de s'arrêter.",
|
|
109
|
+
"meta": "tu te demandes si ta socket va bien" },
|
|
110
|
+
{ "icon": "🍳", "title": "cookbook-chat", "href": "./cookbook-chat.md",
|
|
111
|
+
"desc": "Un salon de discussion complet : configuration, service métier, contrôleur, client navigateur, puis le passage en cluster.",
|
|
112
|
+
"meta": "du code qui tourne, à déformer vers ton besoin" }
|
|
113
|
+
]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 🧩 Ce que le module apporte
|
|
117
|
+
|
|
118
|
+
Quatre propriétés, toutes vérifiables dans le code — c'est ce qui justifie une couche au-dessus de la
|
|
119
|
+
WebSocket brute.
|
|
120
|
+
|
|
121
|
+
**Un contrôleur temps réel est un contrôleur.** `RealtimeController`
|
|
122
|
+
(`RealtimeController.ts:144`) étend le `Controller` du framework : il se déclare avec les mêmes
|
|
123
|
+
décorateurs de route, reçoit la même injection, passe par le même pare-feu. HTTP et WebSocket ne sont
|
|
124
|
+
pas deux applications à écrire deux fois, mais deux entrées du même pipeline.
|
|
125
|
+
|
|
126
|
+
**Une connexion, N canaux, dans les deux sens.** Le client s'abonne à autant de canaux qu'il veut sur
|
|
127
|
+
la même socket. Trois formes de trafic coexistent : le serveur diffuse (`@RealtimeChannel`,
|
|
128
|
+
`realtimeDecorators.ts:142`), le client appelle et attend une réponse (`@RealtimeAction`,
|
|
129
|
+
`realtimeDecorators.ts:30`), le client pousse sans attendre (`@RealtimeInbound`,
|
|
130
|
+
`realtimeDecorators.ts:30`). Rien n'est ouvert qui n'ait été déclaré.
|
|
131
|
+
|
|
132
|
+
**Le travail est fait une fois par processus, pas une fois par client.** Le `RealtimeHub`
|
|
133
|
+
(`RealtimeHub.ts:213`) tient **un seul producteur par canal** : le premier abonné le démarre, le
|
|
134
|
+
dernier départ l'arrête. Chaque connexion supplémentaire n'ajoute qu'une destination de diffusion.
|
|
135
|
+
Un salon suivi par mille personnes coûte un producteur, pas mille.
|
|
136
|
+
|
|
137
|
+
**Le backplane est un transport, jamais un magasin.** Il relaie les publications d'un processus vers
|
|
138
|
+
les autres, et rien de plus : aucun historique, aucune reprise. Les drivers vivent dans un registre
|
|
139
|
+
(`registerBackplaneDriver`, `backplaneRegistry.ts:55`) — `loopback` (un seul processus), `cluster`
|
|
140
|
+
(échanges entre workers d'un même pod), `redis` (entre machines) — et tu peux y inscrire le tien sans
|
|
141
|
+
toucher au cœur.
|
|
142
|
+
|
|
143
|
+
> [!IMPORTANT]
|
|
144
|
+
> **Rien ne franchit la frontière du processus sans intention explicite.** Par défaut, un canal reste
|
|
145
|
+
> local. Il faut le déclarer diffusable (`@RealtimeBroadcast`,
|
|
146
|
+
> `realtimeDecorators.ts:342`) pour qu'il emprunte le backplane. C'est volontaire : un canal
|
|
147
|
+
> d'observation qui décrit l'état d'**un** pod n'aurait aucun sens répliqué sur les autres.
|
|
148
|
+
|
|
149
|
+
Le module se déclare par ailleurs **non critique** (`Realtime.critical`, `index.ts:144`) et son
|
|
150
|
+
démarrage de backplane est borné dans le temps : un Redis injoignable n'empêche pas l'application de
|
|
151
|
+
monter, le hub reste local, et la dégradation est annoncée dans les journaux plutôt que subie.
|
|
152
|
+
|
|
153
|
+
## 🚀 Démarrage rapide
|
|
154
|
+
|
|
155
|
+
Vu depuis une application créée par `nodefony create app`. Un salon de discussion minimal, en trois
|
|
156
|
+
fichiers.
|
|
157
|
+
|
|
158
|
+
### 1. Déclarer le module
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// nodefony.config.ts — l'orchestrateur de l'application
|
|
162
|
+
export default defineConfig(() => ({
|
|
163
|
+
modules: [
|
|
164
|
+
"@nodefony/http",
|
|
165
|
+
"@nodefony/framework",
|
|
166
|
+
use("@nodefony/realtime", {
|
|
167
|
+
// Un seul processus en développement : le hub distribue localement,
|
|
168
|
+
// aucun transport réseau n'est ouvert. Passer à "redis" en production.
|
|
169
|
+
backplane: { driver: "loopback" },
|
|
170
|
+
// Défense CSRF à la poignée de main (RFC 6455 §10.2) : sans elle, un site
|
|
171
|
+
// tiers peut ouvrir une socket avec les cookies de ton utilisateur.
|
|
172
|
+
csrf: {
|
|
173
|
+
checkOrigin: {
|
|
174
|
+
enabled: true,
|
|
175
|
+
allowList: ["https://127.0.0.1:5152"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
}));
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 2. Écrire le contrôleur temps réel
|
|
184
|
+
|
|
185
|
+
Un contrôleur ordinaire : une route qui déclare le transport `WEBSOCKET`, puis des méthodes décorées.
|
|
186
|
+
La classe de base porte tout le protocole — poignée de main, abonnements, nettoyage à la fermeture.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
// nodefony/controllers/chat.ts
|
|
190
|
+
import { route, controller } from "@nodefony/framework";
|
|
191
|
+
import {
|
|
192
|
+
RealtimeController,
|
|
193
|
+
RealtimeChannel,
|
|
194
|
+
RealtimeBroadcast,
|
|
195
|
+
} from "@nodefony/realtime";
|
|
196
|
+
import type { RealtimePublish } from "@nodefony/realtime";
|
|
197
|
+
import type { RpcActionHandler } from "nodefony";
|
|
198
|
+
import type { ContextType } from "@nodefony/http";
|
|
199
|
+
|
|
200
|
+
// Déclare le préfixe diffusable dès le chargement de la classe : sans lui, le
|
|
201
|
+
// canal resterait confiné à ce processus, même en cluster.
|
|
202
|
+
@RealtimeBroadcast("chat:")
|
|
203
|
+
@controller("/chat")
|
|
204
|
+
class ChatController extends RealtimeController {
|
|
205
|
+
constructor(context: ContextType) {
|
|
206
|
+
super("chat", context);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// La socket du salon. Une seule route, un seul port : le WebSocket se greffe
|
|
210
|
+
// sur le serveur HTTP existant.
|
|
211
|
+
@route("chat-realtime", {
|
|
212
|
+
path: "/realtime",
|
|
213
|
+
requirements: { methods: ["WEBSOCKET"] },
|
|
214
|
+
})
|
|
215
|
+
async realtime(message: string | Buffer | null): Promise<void> {
|
|
216
|
+
this.handleRealtime(message);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Appel client → réponse serveur, comme un appel de fonction distante.
|
|
220
|
+
protected override realtimeActions(): Record<string, RpcActionHandler> {
|
|
221
|
+
return { "chat:ping": () => ({ pong: true, ts: Date.now() }) };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Producteur du canal : démarré au premier abonné, arrêté au dernier départ.
|
|
225
|
+
// Le retour EST la fonction d'arrêt — c'est ce qui garantit zéro fuite.
|
|
226
|
+
@RealtimeChannel("chat:room-42")
|
|
227
|
+
room42(channel: string, publish: RealtimePublish): () => void {
|
|
228
|
+
const timer = setInterval(() => publish(channel, { ts: Date.now() }), 1000);
|
|
229
|
+
timer.unref();
|
|
230
|
+
return () => clearInterval(timer);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default ChatController;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### 3. S'y brancher depuis le navigateur
|
|
238
|
+
|
|
239
|
+
Le client est isomorphe : c'est le cœur `nodefony` lui-même, importable côté navigateur.
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import { RealtimeClient } from "nodefony/client";
|
|
243
|
+
|
|
244
|
+
// `shared` réutilise la connexion existante pour une même URL : dix composants
|
|
245
|
+
// qui écoutent dix canaux ouvrent UNE socket, pas dix.
|
|
246
|
+
const socket = RealtimeClient.shared({
|
|
247
|
+
url: "wss://127.0.0.1:5152/chat/realtime",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// S'abonner AVANT de connecter est sûr : les abonnements sont rejoués à
|
|
251
|
+
// l'ouverture, et après chaque reconnexion.
|
|
252
|
+
socket.on("chat:room-42", (message: unknown) => {
|
|
253
|
+
console.log("reçu", message);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await socket.connect();
|
|
257
|
+
socket.subscribe("chat:room-42");
|
|
258
|
+
|
|
259
|
+
// Appel aller-retour sur la même connexion, sans requête HTTP.
|
|
260
|
+
const pong = await socket.request("chat:ping", {});
|
|
261
|
+
console.log(pong);
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
> [!TIP]
|
|
265
|
+
> En React, ne câble pas le client à la main : les hooks du subpath `nodefony/react`
|
|
266
|
+
> (`useNodefony`, `useNodefonyChannel`, `useNodefonyIdentity`) gèrent l'abonnement, le
|
|
267
|
+
> désabonnement au démontage et le comptage de références.
|
|
268
|
+
|
|
269
|
+
## 🏛️ Place dans le framework
|
|
270
|
+
|
|
271
|
+
```mermaid
|
|
272
|
+
flowchart TD
|
|
273
|
+
HTTP["@nodefony/http<br/>serveurs ws · wss, une connexion par client"]
|
|
274
|
+
FRW["@nodefony/framework<br/>routage · injection · gardes"]
|
|
275
|
+
APP["Ton contrôleur<br/>@RealtimeChannel · @RealtimeAction · @RealtimeInbound"]
|
|
276
|
+
HUB["RealtimeHub<br/>un producteur par canal, par processus"]
|
|
277
|
+
BP["IBackplane<br/>loopback · cluster · redis"]
|
|
278
|
+
OUT["Autres processus<br/>workers, répliques"]
|
|
279
|
+
SEC["@nodefony/security<br/>identité et droits"]
|
|
280
|
+
HTTP --> FRW --> APP --> HUB
|
|
281
|
+
HUB -->|canaux déclarés diffusables| BP
|
|
282
|
+
BP <--> OUT
|
|
283
|
+
SEC -.->|poignée de main, autorisation| HUB
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Le module s'appuie sur `@nodefony/http` pour le transport et sur `@nodefony/framework` pour le
|
|
287
|
+
routage ; il n'impose aucune base de données. Le driver `redis` consomme les connexions publiées par
|
|
288
|
+
[`@nodefony/redis`](../../redis/docs/index.md), sans en dépendre : si le module est absent, le hub
|
|
289
|
+
reste local et le dit.
|
|
290
|
+
|
|
291
|
+
## 🧰 Surface publique
|
|
292
|
+
|
|
293
|
+
Côté serveur : `RealtimeController` (la classe à étendre), les décorateurs `RealtimeChannel`,
|
|
294
|
+
`RealtimeAction` et `RealtimeInbound`, le service `RealtimeService` (`RealtimeService.ts:50`) pour
|
|
295
|
+
publier depuis n'importe quel service injecté, le `RealtimeHub` et sa sonde, les trois backplanes
|
|
296
|
+
natifs, et le registre de drivers pour brancher le tien.
|
|
297
|
+
|
|
298
|
+
Côté client : `RealtimeClient` via le subpath `nodefony/client`, et les hooks React via
|
|
299
|
+
`nodefony/react`.
|
|
300
|
+
|
|
301
|
+
Les signatures exactes vivent dans le graphe généré — `jq '.symbols.RealtimeHub' .ai/symbols.json` —
|
|
302
|
+
jamais recopiées ici : elles divergeraient en silence.
|
|
303
|
+
|
|
304
|
+
## ⚙️ Configuration
|
|
305
|
+
|
|
306
|
+
Un seul point d'entrée : `use("@nodefony/realtime", { … })` dans `nodefony.config.ts`, validé au boot
|
|
307
|
+
contre le schéma du module (`realtimeConfigSchema`, `config.ts:205`). Cinq blocs :
|
|
308
|
+
|
|
309
|
+
- `backplane` — le driver de fan-out et son espace de nommage. Ce cloisonnement devient
|
|
310
|
+
indispensable dès que **deux déploiements partagent le même Redis** : sans lui, leurs publications
|
|
311
|
+
se mélangent.
|
|
312
|
+
- `csrf.checkOrigin` — le contrôle d'origine à l'ouverture de la socket.
|
|
313
|
+
- `limits` — le plafond de canaux par connexion, garde anti-saturation mémoire.
|
|
314
|
+
- `slowConsumer` — le seuil à partir duquel un client trop lent est signalé par la sonde.
|
|
315
|
+
- `cluster.probe` — la sonde agrégée du pod, en mode multi-workers.
|
|
316
|
+
|
|
317
|
+
Chaque bloc, ses valeurs d'usine et les situations qui justifient d'en changer sont détaillés dans
|
|
318
|
+
[Configuration](./configuration.md).
|
|
319
|
+
|
|
320
|
+
## 📡 Observabilité — Studio
|
|
321
|
+
|
|
322
|
+
Deux écrans dédiés : la **console temps réel** (`/nodefony/hub`) montre les canaux vivants, leurs
|
|
323
|
+
abonnés, le volume diffusé et les connexions en retard ; **Cluster** (`/nodefony/cluster`) agrège la
|
|
324
|
+
vue de tous les workers d'un pod.
|
|
325
|
+
|
|
326
|
+
Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
|
|
327
|
+
diffusion, connexions, octets et frames, pression d'écriture. C'est la même donnée que celle rendue à
|
|
328
|
+
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:775`).
|
|
329
|
+
|
|
330
|
+
## 🧪 Tests & couverture
|
|
331
|
+
|
|
332
|
+
Les compteurs sont régénérés depuis vitest, jamais figés dans cette prose. Ce qui mérite d'être dit
|
|
333
|
+
ici, c'est **ce que les suites prouvent** — et ce qu'elles ne prouvent qu'à condition d'avoir
|
|
334
|
+
l'infrastructure sous la main.
|
|
335
|
+
|
|
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 aucune infrastructure externe |
|
|
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 |
|
|
343
|
+
|
|
344
|
+
> [!WARNING]
|
|
345
|
+
> **Une suite verte ne prouve rien sur le fan-out cross-machine.** Le banc Redis est doublement
|
|
346
|
+
> conditionnel : il ne s'exécute que si tu le demandes (`NF_RUN_CLUSTER_E2E=1`), et il se **saute** de
|
|
347
|
+
> lui-même si aucun Redis n'est joignable. Or un test sauté compte comme un succès — on peut donc
|
|
348
|
+
> 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, lui, ne demande aucune infrastructure et tourne toujours.
|
|
350
|
+
>
|
|
351
|
+
> ```bash
|
|
352
|
+
> # 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
|
|
354
|
+
> ```
|
|
355
|
+
|
|
356
|
+
## 🔗 Pour aller plus loin
|
|
357
|
+
|
|
358
|
+
- ⬆️ **Remonter** : [Toute la documentation](../../../../../docs/index.md)
|
|
359
|
+
- 📄 **Les pages du module** : [Vocabulaire](./vocabulaire.md) · [Architecture](./architecture.md) ·
|
|
360
|
+
[Protocole](./protocole.md) · [Actions RPC](./actions.md) · [Configuration](./configuration.md) ·
|
|
361
|
+
[Sécurité](./securite.md) · [Observabilité](./observabilite.md) ·
|
|
362
|
+
[Cookbook — un chat](./cookbook-chat.md)
|
|
363
|
+
- 🧭 **Modules voisins** : [`@nodefony/http`](../../http/docs/index.md) (la connexion et son
|
|
364
|
+
contexte) · [`@nodefony/framework`](../../framework/docs/index.md) (routage et décorateurs) ·
|
|
365
|
+
[`@nodefony/security`](../../security/docs/index.md) (identité et droits) ·
|
|
366
|
+
[`@nodefony/redis`](../../redis/docs/index.md) (le transport cross-machine)
|
|
367
|
+
- 🏛️ **Transverse** : [vue d'ensemble du framework](../../../../../docs/architecture/vue-ensemble.md) ·
|
|
368
|
+
[le pipeline d'une requête](../../../../../docs/architecture/pipeline-requete.md)
|
|
369
|
+
- 📖 [Lexique général](../../../../../docs/lexique.md) du framework.
|
|
370
|
+
</content>
|
|
371
|
+
|
|
372
|
+
</invoke>
|