@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.
Files changed (81) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +207 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +218 -0
  6. package/dist/nodefony/config/config.js +63 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +60 -0
  8. package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
  9. package/dist/nodefony/interfaces/IBackplane.js +1 -0
  10. package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
  11. package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
  12. package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
  13. package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
  14. package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
  15. package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
  16. package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
  17. package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
  18. package/dist/nodefony/interfaces/index.js +1 -0
  19. package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
  20. package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
  21. package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
  22. package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
  23. package/dist/nodefony/src/backplane/envelope.js +109 -0
  24. package/dist/nodefony/src/backplane/originId.js +35 -0
  25. package/dist/nodefony/src/backplane/publishQueue.js +96 -0
  26. package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
  27. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
  28. package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
  29. package/dist/nodefony/src/server/RealtimeController.js +601 -0
  30. package/dist/nodefony/src/server/RealtimeHub.js +926 -0
  31. package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
  32. package/dist/nodefony/src/server/deniedDetail.js +29 -0
  33. package/dist/nodefony/src/server/syslogUplink.js +105 -0
  34. package/dist/nodefony/src/server/welcomeEnv.js +25 -0
  35. package/dist/nodefony/src/service/RealtimeService.js +240 -0
  36. package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
  37. package/dist/nodefony/testing/index.js +163 -0
  38. package/dist/types/index.d.ts +99 -0
  39. package/dist/types/nodefony/config/config.d.ts +43 -0
  40. package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
  41. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
  42. package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
  43. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
  44. package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
  45. package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
  46. package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
  47. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
  48. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
  49. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
  50. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +18 -0
  52. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
  53. package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
  54. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
  55. package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
  56. package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
  57. package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
  58. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
  59. package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
  60. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
  61. package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
  62. package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
  63. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
  64. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
  65. package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
  66. package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
  67. package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
  68. package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
  69. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
  70. package/dist/types/nodefony/testing/index.d.ts +169 -0
  71. package/docs/actions.md +573 -0
  72. package/docs/architecture.md +889 -0
  73. package/docs/configuration.md +803 -0
  74. package/docs/cookbook-chat.md +1619 -0
  75. package/docs/couverture-bancs.md +139 -0
  76. package/docs/index.md +372 -0
  77. package/docs/observabilite.md +592 -0
  78. package/docs/protocole.md +518 -0
  79. package/docs/securite.md +845 -0
  80. package/docs/vocabulaire.md +845 -0
  81. 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>