@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,845 @@
1
+ ---
2
+ title: "Vocabulaire — le lexique de la socket Nodefony"
3
+ navTitle: Vocabulaire
4
+ lang: fr
5
+ module: "@nodefony/realtime"
6
+ topic: realtime
7
+ coverageModule: realtime
8
+ section: "Realtime"
9
+ audience: [developer]
10
+ tags:
11
+ [
12
+ vocabulaire,
13
+ lexique,
14
+ socket,
15
+ canal,
16
+ hub,
17
+ fan-out,
18
+ backplane,
19
+ driver,
20
+ sonde,
21
+ frame,
22
+ seam,
23
+ cadence,
24
+ ]
25
+ version: "doc"
26
+ status: stable
27
+ updated: 2026-07-19
28
+ source: "src/packages/@nodefony/realtime/docs/vocabulaire.md"
29
+ ---
30
+
31
+ # Vocabulaire — le lexique de la socket Nodefony
32
+
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 cinq autres pages du module
35
+ > l'emploient sans le redéfinir. Règle de lecture : quand une discussion realtime s'embrouille,
36
+ > demande-toi **de quoi on parle — de la prise, de l'autocom, ou du fond de panier ?** Neuf fois
37
+ > sur dix, la confusion tenait à un seul mot.
38
+
39
+ 📍 [Documentation](../../../../../docs/index.md) › [Realtime](index.md) › **Vocabulaire**
40
+
41
+ ## 🧠 Schéma général — où vit chaque mot
42
+
43
+ Le vocabulaire suit la matière : ce que ton code **tient**, ce qui passe **sur le fil**, ce qui
44
+ **aiguille** dans le process, ce qui **franchit** la frontière du process.
45
+
46
+ ```mermaid
47
+ flowchart TB
48
+ subgraph CLIENT["Ce que ton code tient (page, mobile, autre serveur, service back)"]
49
+ SOCK["socket — la prise<br/>subscribe · on · publish · request"]
50
+ CANAL["canal — un sous-flux nommé, duplex"]
51
+ end
52
+ subgraph WIRE["Ce qui passe sur le fil"]
53
+ PEER["pair — JsonRpcPeer<br/>frame JSON-RPC 2.0"]
54
+ TR["transport — les octets (WebSocket)"]
55
+ end
56
+ subgraph POD["Un pod = un process = un hub"]
57
+ HUB["hub — le broker local<br/>fan-out vers les abonnés"]
58
+ PROBE["sonde — l'état de santé"]
59
+ end
60
+ subgraph OUT["Les autres pods du cluster"]
61
+ BP["backplane — le fond de panier<br/>driver loopback, cluster, redis"]
62
+ end
63
+ SOCK --> CANAL --> PEER --> TR --> HUB
64
+ HUB --> BP
65
+ BP --> HUB
66
+ HUB -.- PROBE
67
+ ```
68
+
69
+ ## 📖 Lexique — les douze mots à retenir
70
+
71
+ Les douze mots qui suffisent à lire toute la documentation du module. Les familles plus bas
72
+ détaillent le reste du vocabulaire.
73
+
74
+ | Mot | Analogie | En une ligne | Le symbole | La carte |
75
+ | ------------- | --------------------- | --------------------------------------------------------------- | ----------------------- | ------------------------------------------------ |
76
+ | **socket** | la prise murale | le handle unique que tient ton code, client **ou** serveur | `IRealtimeSocket` | [voir](#socket--la-prise-que-tient-ton-code) |
77
+ | **canal** | la conférence | un sous-flux **nommé** et **duplex** multiplexé sur la socket | nom de chaîne | [voir](#canal-channel--un-sous-flux-nommé) |
78
+ | **pair** | le combiné | le moteur de protocole, **identique** des deux côtés | `JsonRpcPeer` | [voir](#pair-peer--le-moteur-de-protocole) |
79
+ | **frame** | l'enveloppe | l'unité atomique qui passe sur le fil | JSON-RPC 2.0 | [voir](#frame--lunité-qui-passe-sur-le-fil) |
80
+ | **transport** | le câble | la techno qui porte les octets, interchangeable | `IRealtimeTransport` | [voir](#transport--la-couche-octets) |
81
+ | **hub** | l'autocom | le broker **du process** : table des abonnés + fan-out | `RealtimeHub` | [voir](#hub--le-broker-du-process) |
82
+ | **fan-out** | le ventilateur | une publication, N livraisons aux abonnés | `RealtimeHub.publish()` | [voir](#fan-out--une-publication-n-livraisons) |
83
+ | **backplane** | le fond de panier | le **driver** qui fait franchir la frontière du process | `IBackplane` | [voir](#backplane--le-fond-de-panier-du-cluster) |
84
+ | **driver** | la carte enfichée | l'implémentation choisie du backplane, par son **nom** | registre de drivers | [voir](#driver--limplémentation-du-transport) |
85
+ | **sonde** | l'oscilloscope | l'état de santé instantané, en JSON | `RealtimeHub.probe()` | [voir](#sonde-probe--létat-de-santé-en-json) |
86
+ | **seam** | la prise préparée | le point de greffe qu'une couche haute utilise sans rien casser | hooks du peer et du hub | [voir](#seam--le-point-de-greffe) |
87
+ | **cadence** | le débit d'un robinet | la fréquence d'un canal d'état, portée par son **nom** | `rateChannel()` | [voir](#cadence--la-fréquence-portée-par-le-nom) |
88
+
89
+ ## Qu'est-ce qu'un vocabulaire figé, et pourquoi une page entière ?
90
+
91
+ Le temps réel est le domaine où les mots glissent le plus : « socket » désigne tour à tour la prise
92
+ applicative, la connexion TCP, l'objet `WebSocket` du navigateur et le protocole. Chaque glissement
93
+ coûte un malentendu d'architecture.
94
+
95
+ Nodefony **fige** donc trois séparations, et tout le reste en découle :
96
+
97
+ 1. **La prise n'est pas le câble.** La `socket` est ce que ton code tient ; le `transport` est la
98
+ couche octets. Changer de transport ne change pas une ligne de ton code.
99
+ 2. **Le broker local n'est pas le bus inter-process.** Le `hub` aiguille dans **un** process ; le
100
+ `backplane` fait franchir la frontière du process. Deux mots, deux étages, deux pannes distinctes.
101
+ 3. **On nomme le rôle, jamais l'outil.** On dit « le backplane Redis », jamais « le Redis » : Redis
102
+ est un **choix de driver** interchangeable, pas un concept d'architecture.
103
+
104
+ > [!IMPORTANT]
105
+ > **`store` et `driver` ne sont pas synonymes.** Un **store** est l'endroit où vivent des **données**
106
+ > (une session, un jeton). Un **driver** est un **flux/transport**. Le backplane realtime est un
107
+ > **driver** — il ne conserve rien, il fait passer. Un canal n'a pas d'historique : qui n'était pas
108
+ > abonné n'a rien reçu.
109
+
110
+ ## La vision Nodefony — les trois mots qui portent la différence
111
+
112
+ Trois mots du lexique ne sont pas des synonymes de ce que proposent les autres bibliothèques
113
+ temps réel. Ce sont les différenciateurs du framework.
114
+
115
+ **`socket` est isomorphe.** Le même contrat `IRealtimeSocket` (`IRealtimeSocket.ts:122`) décrit la
116
+ prise **côté navigateur** (`RealtimeClient`, `RealtimeClient.ts:194`) **et côté serveur**
117
+ (`ServerRealtimeSocket`, `ServerRealtimeSocket.ts:44`). Un service back publie exactement comme une
118
+ page front : `publish("chat:room-42", payload)`. Il n'y a pas une API cliente et une API serveur à
119
+ apprendre, il y en a **une**.
120
+
121
+ **`canal` est duplex et multiplexé.** Une connexion porte **N canaux** simultanés, chacun
122
+ bidirectionnel, chacun avec son cycle de vie et son compteur d'abonnés. Le backing d'un canal est
123
+ **pluggable côté serveur** — pub/sub, encapsulation d'un autre protocole, pont vers TCP/UDP, relais
124
+ vers un autre service — et le consommateur ne voit jamais la différence
125
+ (`IRealtimeChannel`, `IRealtimeSocket.ts:87`).
126
+
127
+ **`contrôleur` est le même mot qu'en HTTP.** Un endpoint temps réel est un contrôleur qui étend
128
+ `RealtimeController` (`RealtimeController.ts:144`) et porte une route WebSocket : HTTP et WebSocket
129
+ vivent dans le **même contexte de contrôleur**. C'est ce qui permet au pont `api.request`
130
+ (`RealtimeController.realtimeApiRequest()`, `RealtimeController.ts:219`) de rejouer sur la socket
131
+ **la même action** que celle servie en REST.
132
+
133
+ ## 🚀 Démarrage rapide — les mots en situation
134
+
135
+ Un chat minimal. Chaque terme du lexique apparaît au moins une fois, commenté.
136
+
137
+ Côté serveur — un **contrôleur** qui déclare un **canal**, un **canal entrant** et une **action RPC** :
138
+
139
+ ```ts
140
+ import {
141
+ RealtimeController,
142
+ RealtimeAction,
143
+ RealtimeChannel,
144
+ RealtimeInbound,
145
+ serverSocket,
146
+ type RealtimePublish,
147
+ } from "@nodefony/realtime";
148
+
149
+ export class ChatController extends RealtimeController {
150
+ // Un CANAL : le provider est créé au 1ᵉʳ abonné, son dispose appelé au dernier.
151
+ @RealtimeChannel("chat:room-42", { authenticated: true })
152
+ room42(channel: string, publish: RealtimePublish): () => void {
153
+ const timer = setInterval(() => publish(channel, { ts: Date.now() }), 1000);
154
+ return () => clearInterval(timer);
155
+ }
156
+
157
+ // Un CANAL ENTRANT : le client pousse, le serveur répond sur le même canal.
158
+ @RealtimeInbound("chat:send")
159
+ send(params: unknown, reply: (payload: unknown) => void): void {
160
+ const text = (params as { text?: unknown }).text;
161
+ if (typeof text !== "string") return; // entrée client = jamais fiable
162
+ // FAÇADE SERVEUR : un service back publie comme une page front.
163
+ serverSocket().publish("chat:room-42", { text });
164
+ reply({ ok: true });
165
+ }
166
+
167
+ // Une ACTION RPC : requête corrélée, le retour devient le `result`.
168
+ @RealtimeAction("chat:ping")
169
+ ping(): { pong: true; ts: number } {
170
+ return { pong: true, ts: Date.now() };
171
+ }
172
+
173
+ // BROADCAST : ce préfixe traverse le backplane. Sans cette ligne, le canal
174
+ // reste INSTANCE-LOCAL (défaut sûr) et Bob, sur un autre pod, ne voit rien.
175
+ // (le préfixe diffusable est déclaré par `@RealtimeBroadcast` sur la classe)
176
+ }
177
+ ```
178
+
179
+ Côté client — la même **socket**, les quatre verbes :
180
+
181
+ ```ts
182
+ import { RealtimeClient } from "nodefony/client";
183
+
184
+ export async function joinChat(): Promise<void> {
185
+ const socket = new RealtimeClient({ url: "wss://127.0.0.1:5152/chat" });
186
+ await socket.connect();
187
+
188
+ socket.subscribe("chat:room-42"); // DEMANDE le flux au pair
189
+ socket.on("chat:room-42", (payload: unknown) => {
190
+ console.log("reçu", payload); // REÇOIT — `on` n'est pas `subscribe`
191
+ });
192
+
193
+ socket.publish("chat:send", { text: "salut" }); // ÉMET, sans réponse
194
+ console.log(await socket.request("chat:ping")); // RPC corrélé, avec réponse
195
+ }
196
+ ```
197
+
198
+ ## 🧰 Le lien — ce que ton code tient
199
+
200
+ | Terme | En une ligne |
201
+ | ----------------- | ------------------------------------------------------ |
202
+ | `socket` | le handle unique, isomorphe client/serveur |
203
+ | `canal` | un sous-flux nommé, duplex, multiplexé |
204
+ | `abonnement` | demander au pair de pousser un canal |
205
+ | `réception` | brancher un handler local — n'ouvre aucun flux |
206
+ | `publication` | émettre sur un canal, sans attendre de réponse |
207
+ | `action RPC` | appel corrélé qui rend une valeur |
208
+ | `canal entrant` | canal où le **client** a le droit de pousser |
209
+ | `handle de canal` | la vue objet d'un canal (nom, nature, cycle de vie) |
210
+ | `client` | l'implémentation navigateur de la socket |
211
+ | `façade serveur` | l'implémentation serveur de la même socket |
212
+ | `accueil` | la première frame reçue : protocole, canaux, identité |
213
+ | `identité` | qui est la connexion, telle que le serveur l'a résolue |
214
+
215
+ ### `socket` — la prise que tient ton code
216
+
217
+ Le **handle unique** manipulé par le code applicatif : quatre verbes (`subscribe`, `on`, `publish`,
218
+ `request`) et une vue par canal. Le mot vient de la prise murale : on branche, on ignore le câblage.
219
+
220
+ Contrat isomorphe `IRealtimeSocket` (`IRealtimeSocket.ts:122`), implémenté côté navigateur par
221
+ `RealtimeClient` (`RealtimeClient.ts:194`) et côté serveur par `ServerRealtimeSocket`
222
+ (`ServerRealtimeSocket.ts:44`). ⚠️ La socket **n'est pas** le [transport](#transport--la-couche-octets).
223
+
224
+ → [Architecture](./architecture.md) pour la pile complète.
225
+
226
+ ### `canal` (channel) — un sous-flux nommé
227
+
228
+ Un **tuyau bidirectionnel nommé**, multiplexé sur la connexion : `"chat:room-42"`,
229
+ `"nodefony:orm:health"`, `"nodefony:socket"`. Convention de nommage par `:` (espace de noms, puis
230
+ précision). Ce n'est pas une classe — c'est une **chaîne de caractères indexée par le hub**.
231
+
232
+ `RealtimeHub.subscribe()` (`RealtimeHub.ts:388`) l'indexe ; `IRealtimeSocket.subscribe()`
233
+ (`IRealtimeSocket.ts:132`) le demande. Plusieurs canaux cohabitent sur **une** connexion : c'est le
234
+ multiplexage.
235
+
236
+ ### `abonnement` (subscribe) — demander le flux
237
+
238
+ **Ref-compté** : la demande part vers le pair au **premier** consommateur seulement, et le flux se
239
+ coupe au **dernier** désabonnement. Réémise automatiquement à chaque reconnexion.
240
+
241
+ `IRealtimeSocket.subscribe()` (`IRealtimeSocket.ts:132`). Le plafond par connexion est réglable
242
+ (`limits.maxChannelsPerConnection`, `config.ts:142`) — au-delà, le canal n'est pas ouvert.
243
+
244
+ ### `réception` (on) — brancher un handler
245
+
246
+ `on(channel, handler)` **branche un écouteur local** et rend un `dispose`. C'est le faux-ami n°1 du
247
+ module : `on` ne demande rien au serveur, `subscribe` si. S'abonner sans écouter reçoit des frames
248
+ que personne ne lit ; écouter sans s'abonner n'en reçoit aucune.
249
+
250
+ `IRealtimeSocket.on(channel, handler)` (`IRealtimeSocket.ts:141`).
251
+
252
+ ### `publication` (publish) — émettre sans réponse
253
+
254
+ Émettre une charge sur un canal, sans corrélation ni accusé. **La cible dépend du côté** : depuis le
255
+ client, la charge part vers le serveur (un seul pair) ; depuis le serveur, elle part en
256
+ [fan-out](#fan-out--une-publication-n-livraisons) vers tous les abonnés.
257
+
258
+ `IRealtimeSocket.publish()` (`IRealtimeSocket.ts:160`), `RealtimeHub.publish()` (`RealtimeHub.ts:530`).
259
+
260
+ ### `action RPC` (request) — l'appel corrélé
261
+
262
+ L'appel qui **attend une réponse** : une `Promise` résolue avec le `result`, rejetée sur `error` ou
263
+ sur expiration. Déclarée côté serveur par `@RealtimeAction` (`realtimeDecorators.ts:101`), appelée
264
+ côté client par `request()` (`IRealtimeSocket.ts:166`).
265
+
266
+ Un handler qui lève une erreur rend un `-32603` **générique** ; seule une `RpcError`
267
+ (`JsonRpcPeer.ts:70`) choisit ce qu'elle expose au pair.
268
+
269
+ ### `canal entrant` (inbound) — le client pousse
270
+
271
+ Un canal où le **client a le droit d'émettre** vers le serveur. Défaut sûr : **aucun**. Un canal
272
+ n'accepte d'entrée que déclaré explicitement, par `@RealtimeInbound` (`realtimeDecorators.ts:30`) ou
273
+ par l'override `realtimeInbound()` (`RealtimeController.ts:205`).
274
+
275
+ Le handler reçoit `(params, reply)` — `params` vient du réseau, donc **jamais fiable** :
276
+ `RealtimeInboundHandler` (`IRealtimeController.ts:16`).
277
+
278
+ ### `handle de canal` — la vue objet d'un canal
279
+
280
+ `socket.channel(name)` rend un objet qui porte le **nom** du canal, sa **nature** (`kind` :
281
+ `pubsub`, `protocol`, `bridge`, `proxy`) et son cycle de vie (`open`/`close`/`send`/`on`). C'est le
282
+ point d'accroche des canaux à état — un appel SIP, une connexion pontée.
283
+
284
+ `IRealtimeChannel` (`IRealtimeSocket.ts:87`), `channel()` (`IRealtimeSocket.ts:178`).
285
+
286
+ ### `client` — la socket côté navigateur
287
+
288
+ `RealtimeClient` (`RealtimeClient.ts:161`) : reconnexion automatique, réémission des abonnements,
289
+ compteurs par canal, heartbeat. Publié dans le sous-chemin `nodefony/client` du cœur — donc
290
+ importable **sans** aucune dépendance serveur, ce qui est la condition de l'isomorphisme.
291
+
292
+ → Les crochets React (`useNodefony`, `useNodefonyChannel`) vivent dans `nodefony/react`.
293
+
294
+ ### `façade serveur` — la même socket, côté back
295
+
296
+ `ServerRealtimeSocket` (`ServerRealtimeSocket.ts:44`), obtenue par `serverSocket()`
297
+ (`ServerRealtimeSocket.ts:223`) : un service métier tient un handle et publie **comme une page
298
+ front**. Une différence assumée : `request()` n'y est pas supporté — au-dessus du hub il n'y a pas
299
+ **un** pair mais N clients. Pour un appel serveur → un client précis, c'est `requestClient()`
300
+ (`RealtimeController.ts:263`).
301
+
302
+ ### `accueil` (welcome) — la première frame
303
+
304
+ La notification `realtime:welcome`, poussée par le serveur juste après le handshake. Elle annonce le
305
+ protocole, les canaux et actions **découvrables** de l'endpoint, et l'identité résolue. Un client
306
+ attend cette frame avant de pousser quoi que ce soit.
307
+
308
+ `IRealtimeWelcome` (`RealtimeEventMap.ts:204`).
309
+
310
+ ### `identité` — qui est cette connexion
311
+
312
+ La vue « sur soi » d'une connexion : type de jeton, authentifié ou non, identifiant, rôles, scopes.
313
+ Aucun secret — seulement ce que le porteur sait déjà de lui-même. Elle est **résolue une fois** au
314
+ handshake, jamais renégociée par frame.
315
+
316
+ `RealtimeIdentity` (`RealtimeEventMap.ts:185`).
317
+
318
+ ## 🏗️ Le serveur — ce qui aiguille derrière la prise
319
+
320
+ | Terme | En une ligne |
321
+ | ----------------------- | -------------------------------------------------------------------- |
322
+ | `hub` | le broker du process : table des abonnés + fan-out |
323
+ | `fan-out` | une publication, N livraisons |
324
+ | `sink` | le point de livraison d'**une** connexion sur un canal |
325
+ | `provider` | ce qui **produit** les messages d'un canal, partagé par les abonnés |
326
+ | `canal partagé` | un provider par canal et par pod, pas un par connexion |
327
+ | `canal passif` | un canal que seuls des écouteurs tiennent — personne ne le produit |
328
+ | `canal système` | canal servi par la plateforme, sans qu'aucun contrôleur le connaisse |
329
+ | `canal broadcast` | canal déclaré pour traverser le backplane |
330
+ | `canal instance-local` | le défaut : le canal ne quitte jamais le process |
331
+ | `contrôleur temps réel` | l'endpoint WebSocket, écrit comme un contrôleur HTTP |
332
+ | `service realtime` | la façade d'injection du hub |
333
+ | `pont API` | rejouer une route HTTP sur la socket, même action, même garde |
334
+
335
+ ### `hub` — le broker du process
336
+
337
+ Le **standard téléphonique** du pod : il tient la table « canal → abonnés locaux » et diffuse. Un
338
+ process = un hub, obtenu par `getRealtimeHub()` (`RealtimeHub.ts:1255`). Il ne connaît **ni** les
339
+ contrôleurs, **ni** le métier : ce sont les providers qui portent les dépendances.
340
+
341
+ `RealtimeHub` (`RealtimeHub.ts:213`). ⚠️ « hub » désigne **toujours** le serveur ; ce que tient le
342
+ code applicatif s'appelle une [socket](#socket--la-prise-que-tient-ton-code).
343
+
344
+ ### `fan-out` — une publication, N livraisons
345
+
346
+ L'action de diffuser : une charge publiée sur un canal part vers **tous** les sinks abonnés
347
+ localement. Une connexion fautive ne casse pas la diffusion aux autres — chaque livraison est isolée.
348
+
349
+ `RealtimeHub.publish()` (`RealtimeHub.ts:530`) pour le chemin complet, `publishLocal()`
350
+ (`RealtimeHub.ts:549`) pour la diffusion **strictement locale** (voie d'entrée du backplane).
351
+
352
+ ### `sink` — le point de livraison d'une connexion
353
+
354
+ Un `sink` = **une** connexion sur **un** canal : la fonction qui pousse la charge vers son pair. Le
355
+ mot vient du couple source/puits : le provider est la source, le sink le puits.
356
+
357
+ `ChannelSink` (`RealtimeHub.ts:134`). Le nombre de sinks d'un canal est son nombre d'abonnés locaux.
358
+
359
+ ### `provider` — ce qui produit un canal
360
+
361
+ La fabrique appelée **au premier abonné** d'un canal, qui démarre ce qui produit les messages (un
362
+ ticker, un écouteur) et rend son `dispose`, appelé **au dernier désabonné**. Zéro abonné = zéro
363
+ timer.
364
+
365
+ `ChannelFactory` (`RealtimeHub.ts:149`). ⚠️ Un provider est **partagé** : il survit à la connexion qui
366
+ l'a créé — n'y capturer que des dépendances à longue vie, jamais le contexte d'une connexion.
367
+
368
+ ### `canal partagé` — un provider par canal et par pod
369
+
370
+ Le modèle de coût du module : là où une implémentation naïve crée un ticker **par connexion**,
371
+ Nodefony en crée **un par canal et par pod** et se contente d'ajouter un sink par abonné. Mille
372
+ spectateurs d'un tableau de bord coûtent un timer, pas mille.
373
+
374
+ `RealtimeHub.subscribe()` (`RealtimeHub.ts:388`).
375
+
376
+ ### `canal passif` — écouter n'est pas posséder
377
+
378
+ Un canal appartient à qui fournit son [provider](#provider--ce-qui-produit-un-canal). Un service du
379
+ serveur qui se contente d'**écouter** — `serverSocket().subscribe("x")` — n'en fournit aucun : le
380
+ canal qu'il ouvre est **passif**. Il reçoit ce qui passe, mais son existence n'engage personne.
381
+
382
+ La distinction se voit à l'arrivée d'une connexion cliente. Sur un canal passif, le hub **rejoue la
383
+ fabrique** du contrôleur : si elle reconnaît le canal, il devient un canal ordinaire et le client
384
+ reçoit du vrai producteur ; sinon l'abonnement est refusé, et l'écoute en place n'est pas touchée.
385
+ Sans cette distinction, le premier service qui prononce un nom de canal l'ouvrirait pour tout le
386
+ monde — et les clients suivants s'abonneraient à un flux que rien ne produit, sans jamais l'apprendre.
387
+
388
+ `RealtimeHub.listen()` (`RealtimeHub.ts:455`) pour l'écoute, `isChannelOwned()`
389
+ (`RealtimeHub.ts:470`) pour lire l'état. ⚠️ Un canal passif **n'est pas** un canal fermé : il n'a
390
+ simplement pas encore de propriétaire.
391
+
392
+ ### `canal système` — servi par la plateforme
393
+
394
+ Un canal dont la fabrique est déclarée par un module bas niveau (le journal d'audit, par exemple), et
395
+ qui devient servable par **n'importe quel** endpoint, présent ou futur, sans qu'aucun contrôleur ne
396
+ le connaisse. Consulté seulement quand la fabrique du contrôleur a dit « inconnu ».
397
+
398
+ `RealtimeHub.registerSystemChannel()` (`RealtimeHub.ts:1117`).
399
+
400
+ ### `canal broadcast` — celui qui traverse le backplane
401
+
402
+ Un canal **déclaré** comme franchissant la frontière du process : chat, présence, notifications. La
403
+ déclaration se fait par **préfixe**, ce qui couvre les variantes de cadence (`chat:` couvre
404
+ `chat:room-42:1000`).
405
+
406
+ `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:594`), déclaré côté contrôleur par
407
+ `@RealtimeBroadcast` (`realtimeDecorators.ts:342`).
408
+
409
+ ### `canal instance-local` — le défaut
410
+
411
+ Tout canal **non déclaré** broadcast reste dans son process. C'est voulu : les canaux
412
+ d'observabilité (journaux, sondes, état interne) décrivent **ce pod**, et les agréger silencieusement
413
+ serait à la fois faux et une fuite. Traverser le process est une capacité qu'un canal **demande**.
414
+
415
+ Politique de forward du hub — `#broadcastPrefixes` (`RealtimeHub.ts:334`).
416
+
417
+ ### `contrôleur temps réel` — l'endpoint WebSocket
418
+
419
+ La classe de base d'un endpoint : elle porte tout le protocole (handshake, accueil, discrimination
420
+ des frames, cycle de vie des canaux) et ne laisse au métier que ses canaux et ses actions.
421
+
422
+ `RealtimeController` (`RealtimeController.ts:144`), point d'entrée `handleRealtime()`
423
+ (`RealtimeController.ts:233`). C'est un **contrôleur** au sens habituel de Nodefony : la même classe
424
+ peut porter des routes HTTP.
425
+
426
+ ### `service realtime` — la façade d'injection
427
+
428
+ Le service exposé au conteneur sous le nom `realtimeService` : `publish`, `subscribe`, `probe`, la
429
+ config gelée. C'est l'API stable pour le code utilisateur, quand `getRealtimeHub()` est l'accès
430
+ interne.
431
+
432
+ `RealtimeService` (`RealtimeService.ts:48`).
433
+
434
+ ### `pont API` — la même action, sur la socket
435
+
436
+ L'option qui expose la méthode `api.request { path }` : la connexion rejoue **la même action de
437
+ contrôleur** que celle servie en REST, avec **la même garde**. Le pont n'atteint que les routes qui
438
+ déclarent explicitement le transport WebSocket — aucun contournement possible.
439
+
440
+ `realtimeApiRequest()` (`RealtimeController.ts:219`), mise en œuvre `invokeApiRequest()`
441
+ (`RealtimeController.ts:818`). Désactivé par défaut.
442
+
443
+ ## 🔌 Le protocole et le transport — ce qui passe sur le fil
444
+
445
+ | Terme | En une ligne |
446
+ | -------------- | ---------------------------------------------------------- |
447
+ | `frame` | l'unité atomique qui passe sur le fil |
448
+ | `pair` | le moteur de protocole, identique client et serveur |
449
+ | `requête` | frame avec `method` **et** `id` — appelle une réponse |
450
+ | `notification` | frame avec `method` seul — sans réponse |
451
+ | `réponse` | frame avec `id` seul — résout une requête en attente |
452
+ | `dispatch` | classer une frame entrante puis la router vers son handler |
453
+ | `transport` | la couche octets, interchangeable |
454
+ | `enveloppe` | joindre des métadonnées serveur à côté du résultat |
455
+ | `cadence` | la fréquence d'un canal d'état, portée par son nom |
456
+ | `AIMD` | l'auto-ajustement de cadence face à un client lent |
457
+
458
+ ### `frame` — l'unité qui passe sur le fil
459
+
460
+ Une enveloppe JSON-RPC 2.0. Sa **nature se lit sur `method`**, pas sur `id` : `method` + `id` =
461
+ requête, `method` seul = notification, `id` seul = réponse. Tout le reste est invalide.
462
+
463
+ `JsonRpcFrameKind` (`JsonRpcPeer.ts:133`). Le classement est fait par `JsonRpcPeer.receive()`
464
+ (`JsonRpcPeer.ts:390`), qui **ne lève jamais**.
465
+
466
+ ### `pair` (peer) — le moteur de protocole
467
+
468
+ Client et serveur sont des **pairs** : classer une frame, router, corréler les réponses est le même
469
+ travail des deux côtés. Nodefony l'écrit **une seule fois** ; chaque côté l'entoure de son transport
470
+ et de ses handlers. Aucune dépendance Node — le même fichier tourne dans le navigateur.
471
+
472
+ `JsonRpcPeer` (`JsonRpcPeer.ts:271`), contrat `IRealtimePeer` (`JsonRpcPeer.ts:213`).
473
+
474
+ ### `dispatch` — classer puis router
475
+
476
+ Le geste du pair sur une frame entrante : décider de sa nature, trouver le handler, l'appeler,
477
+ renvoyer la réponse s'il en faut une. Ce n'est pas une méthode publique — c'est un **moment**, et
478
+ c'est précisément là que se greffe le [verrou de frame](#verrou-de-frame--la-décision-par-frame),
479
+ par le crochet `beforeDispatch` (`JsonRpcPeer.ts:172`).
480
+
481
+ ### `transport` — la couche octets
482
+
483
+ La techno réseau qui porte les frames, **interchangeable** : `WsConnectionTransport`
484
+ (`WsConnectionTransport.ts:46`) côté serveur, `BrowserWsTransport` (`BrowserWsTransport.ts:12`) côté
485
+ navigateur. Le pair ignore laquelle est branchée.
486
+
487
+ Contrat `IRealtimeTransport` (`IRealtimeTransport.ts:34`). ⚠️ C'est **le seul** endroit du lexique où
488
+ « socket » pourrait désigner la connexion réseau : ici, ce mot est réservé à la
489
+ [prise applicative](#socket--la-prise-que-tient-ton-code).
490
+
491
+ ### `enveloppe` — la méta à côté du résultat
492
+
493
+ Quand un handler veut joindre des métadonnées serveur à sa réponse, il rend une enveloppe : le
494
+ `result` reste **exactement** ce que rendrait la même route en REST, et la méta voyage dans un champ
495
+ frère. Un pair qui n'en connaît pas les clés les ignore.
496
+
497
+ `RpcEnvelope` (`JsonRpcPeer.ts:104`), `RpcMeta` (`JsonRpcPeer.ts:90`).
498
+
499
+ ### `cadence` — la fréquence portée par le nom
500
+
501
+ La fréquence d'un canal d'état vit **dans son nom** : `nodefony:orm:health` = cadence par défaut,
502
+ `nodefony:orm:health:2000` = un rythme explicite. Conséquence voulue : **un canal, une cadence, un
503
+ comptage** — deux cadences sont deux canaux, jamais réconciliés.
504
+
505
+ `rateChannel()` (`channelRate.ts:44`) côté client, `parseRate()` (`channelRate.ts:63`) côté serveur —
506
+ même fichier, donc même règle des deux côtés.
507
+
508
+ ### `AIMD` — s'ajuster au client lent
509
+
510
+ _Additive Increase, Multiplicative Decrease_ : accélérer par petits pas quand tout va bien, freiner
511
+ d'un coup à la moindre congestion. Le principe de régulation de TCP, appliqué **par canal** à partir
512
+ de ce qui reste en attente dans la connexion.
513
+
514
+ `AdaptiveRate` (`AdaptiveRate.ts:78`), liaison sur un canal par `bindAdaptiveChannel()`
515
+ (`AdaptiveRate.ts:239`).
516
+
517
+ ## 🧩 Le cluster — franchir la frontière du process
518
+
519
+ | Terme | En une ligne |
520
+ | ----------- | ------------------------------------------------------------- |
521
+ | `backplane` | le bus qui propage les publications entre process |
522
+ | `driver` | l'implémentation du backplane, choisie par son nom |
523
+ | `loopback` | le driver « rien à transporter » : un seul process |
524
+ | `cluster` | le driver entre workers d'une même machine |
525
+ | `redis` | le driver multi-machine, en pub/sub |
526
+ | `origine` | l'identité du process émetteur, qui rend l'anti-écho possible |
527
+ | `anti-écho` | ne jamais rejouer localement ce qu'on vient d'émettre |
528
+ | `cloison` | l'espace de noms qui empêche deux applications de se parler |
529
+ | `cross-pod` | la propriété d'un driver : sait-il franchir la machine ? |
530
+
531
+ ### `backplane` — le fond de panier du cluster
532
+
533
+ Le **fond de panier** d'un rack : la carte qui relie toutes les autres. Ici, le bus qui propage les
534
+ publications d'un process aux autres. Un contrat volontairement minuscule — `publish`, `onMessage`,
535
+ `start`, `stop`, `describe` — parce que tout ce qui est riche appartient au hub.
536
+
537
+ `IBackplane` (`IBackplane.ts:75`), message `IBackplaneMessage` (`IBackplane.ts:51`), branchement
538
+ `RealtimeHub.setBackplane()` (`RealtimeHub.ts:640`).
539
+
540
+ > [!TIP]
541
+ > C'est la pièce qui tient la promesse « une ligne de configuration change tout » : le hub **ne sait
542
+ > pas** qu'il est en cluster. Il publie, le backplane se débrouille.
543
+
544
+ ### `driver` — l'implémentation du transport
545
+
546
+ Le backplane est un **contrat** ; un driver en est une réalisation, désignée par son **nom** dans la
547
+ configuration (`backplane.driver`, `config.ts:47`). Un registre associe nom → fabrique : aucune
548
+ cascade de conditions, et n'importe qui peut enregistrer le sien.
549
+
550
+ `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), `listBackplaneDrivers()`
551
+ (`backplaneRegistry.ts:68`). Un nom inconnu au démarrage n'arrête rien : le hub reste local, avec un
552
+ avertissement.
553
+
554
+ ### `loopback` — un seul process
555
+
556
+ Le driver du cas mono-process : il n'y a **rien à transporter**, le hub fait tout localement. C'est
557
+ le défaut, et il ne coûte rien.
558
+
559
+ `LoopbackBackplane` (`LoopbackBackplane.ts:24`).
560
+
561
+ ### `cluster` — entre workers d'une machine
562
+
563
+ Le driver qui relie les workers d'un même hôte par le canal de communication inter-process de Node.
564
+ Multi-process sans aucune infrastructure à installer.
565
+
566
+ `ClusterBackplane` (`ClusterBackplane.ts:89`).
567
+
568
+ ### `redis` — entre machines
569
+
570
+ Le driver qui propage les publications en pub/sub, donc entre pods de machines différentes. Il
571
+ consomme les connexions du module Redis — jamais une dépendance directe. Absent ou injoignable, le
572
+ hub reste local et le démarrage continue.
573
+
574
+ `RedisBackplane` (`RedisBackplane.ts:193`), canal `resolveRedisChannel()` (`RedisBackplane.ts:31`).
575
+
576
+ ### `origine` (originId) — qui a émis
577
+
578
+ L'identifiant **unique dans le cluster** du process émetteur, joint à chaque message. Il est dérivé
579
+ du nom du pod ou de l'hôte, jamais du seul identifiant de processus : deux conteneurs peuvent tous
580
+ deux être le processus n° 1.
581
+
582
+ `resolveBackplaneOriginId()` (`originId.ts:24`), champ `IBackplane.originId` (`IBackplane.ts:77`).
583
+
584
+ ### `anti-écho` — ne pas rejouer sa propre voix
585
+
586
+ La règle qui empêche les tempêtes : un message **reçu** du backplane est réinjecté en diffusion
587
+ **locale seulement**, jamais renvoyé sur le bus ; et un message qui porte sa propre origine est
588
+ ignoré à l'arrivée. Sans l'origine, l'anti-écho ferait taire le voisin au lieu de soi-même.
589
+
590
+ `RealtimeHub.publishLocal()` (`RealtimeHub.ts:549`).
591
+
592
+ ### `cloison` (namespace) — deux applications, un même bus
593
+
594
+ Le suffixe qui isole les canaux d'une application sur un transport mutualisé. Nécessaire parce que le
595
+ numéro de base d'un serveur Redis **ne cloisonne pas** le pub/sub : sans cloison, deux déploiements
596
+ partageant le même serveur se parleraient.
597
+
598
+ `backplane.namespace` (`config.ts:59`), constante de base `REDIS_RT_CHANNEL`
599
+ (`RedisBackplane.ts:20`).
600
+
601
+ ### `cross-pod` — la propriété qui compte
602
+
603
+ L'information que chaque driver déclare sur lui-même : sait-il franchir la machine ? Elle apparaît
604
+ dans la carte d'identité du backplane, journalisée au démarrage et lisible dans la sonde — c'est ce
605
+ qui permet de vérifier en une ligne qu'un déploiement diffuse bien au-delà d'un pod.
606
+
607
+ `IBackplaneInfo` (`IBackplane.ts:74`).
608
+
609
+ ## 🔐 La sécurité — qui parle, qui a le droit
610
+
611
+ | Terme | En une ligne |
612
+ | -------------------- | ---------------------------------------------------------------- |
613
+ | `handshake` | la poignée de main d'ouverture, où tout se décide |
614
+ | `authenticator` | la stratégie qui transforme un handshake en identité |
615
+ | `matcher` | le sélecteur qui dit quel authenticator capture quelle connexion |
616
+ | `jeton` | l'identité résolue, figée pour la durée de la connexion |
617
+ | `politique de canal` | ce qu'un canal exige de son abonné |
618
+ | `verrou de frame` | la décision « cette frame passe-t-elle ? » |
619
+ | `contrôle d'origine` | refuser une ouverture venue d'un site tiers |
620
+ | `révocation` | fermer une connexion dont l'identité n'est plus valable |
621
+ | `refus` | rendre un refus visible côté client |
622
+ | `audit de frame` | tracer les événements notables du protocole |
623
+ | `seam` | le point de greffe prévu pour une couche supérieure |
624
+ | `plafond de canaux` | la borne anti-saturation par connexion |
625
+
626
+ ### `handshake` — la poignée de main
627
+
628
+ Le moment de l'ouverture : c'est **là**, et une seule fois, que l'origine est contrôlée, que
629
+ l'authenticator tourne et que l'identité est figée. Tout ce qui coûte cher se paie ici, jamais par
630
+ frame.
631
+
632
+ DTO neutre `IRealtimeHandshake` (`IRealtimeHandshake.ts:14`) — en-têtes, cookies, URL, origine,
633
+ sous-protocoles. Traitement dans `onHandshake()` (`RealtimeController.ts:312`).
634
+
635
+ ### `authenticator` — du handshake à l'identité
636
+
637
+ La stratégie qui transforme une poignée de main en identité : `supports` (est-ce mon cas ?),
638
+ `authenticate` (voici le jeton), `onSuccess` / `onFailure`. Exactement le motif des authenticators
639
+ HTTP — un seul modèle mental pour les deux transports.
640
+
641
+ `IRealtimeAuthenticator` (`IRealtimeAuthenticator.ts:24`), enregistrement
642
+ `RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:861`).
643
+
644
+ ### `matcher` — qui capture quoi
645
+
646
+ Le sélecteur (motif d'URL, hôte optionnel) qui décide **quel** authenticator prend la connexion. Le
647
+ **premier** qui capture gagne : on enregistre donc du plus spécifique au plus général.
648
+
649
+ `IRealtimeAuthenticatorMatcher` (`IRealtimeAuthenticatorMatcher.ts:25`), résolution
650
+ `resolveAuthenticator()` (`RealtimeHub.ts:871`).
651
+
652
+ ### `jeton` (token) — l'identité figée
653
+
654
+ Ce que l'authenticator produit : type, identifiant, rôles, scopes, et parfois une méthode de
655
+ re-validation. Il est mis en cache sur la connexion et **jamais** recalculé par frame.
656
+
657
+ `IRealtimeToken` (`IRealtimeToken.ts:18`). La lecture ne rend **jamais** `null` : sans authenticator,
658
+ c'est le jeton anonyme gelé `ANONYMOUS_REALTIME_TOKEN` (`AnonymousRealtimeToken.ts:18`) qui répond —
659
+ un refus a donc toujours un auteur identifiable.
660
+
661
+ ### `politique de canal` — ce qu'un canal exige
662
+
663
+ Les exigences déclarées d'un canal : authentifié, rôles, scopes. Elles s'attachent au **nom** du
664
+ canal, pas à la méthode qui l'implémente — parce que c'est le nom que le pare-feu résout.
665
+
666
+ `IChannelPolicy` (`IChannelPolicy.ts:20`), déclaration par `@RealtimeChannel`
667
+ (`realtimeDecorators.ts:192`), résolution `resolveChannelPolicy()` (`RealtimeHub.ts:1063`).
668
+
669
+ ### `verrou de frame` — la décision par frame
670
+
671
+ La fonction qui répond « cette frame passe-t-elle ? » à partir du jeton **déjà** en cache.
672
+ **Synchrone par doctrine** : attendre une réponse distante à chaque frame sérialiserait le flux d'une
673
+ connexion. Un contrôle qui doit interroger le réseau se fait au handshake, pas ici.
674
+
675
+ `FrameAuthorizer` (`RealtimeHub.ts:39`), pose `setFrameAuthorizer()` (`RealtimeHub.ts:949`), appel
676
+ `runAuthorizer()` (`RealtimeHub.ts:1134`).
677
+
678
+ ### `contrôle d'origine` — la défense d'ouverture
679
+
680
+ Le contrôle de l'en-tête `Origin` à l'ouverture (RFC 6455 §10.2) : un site tiers ne doit pas pouvoir
681
+ ouvrir une connexion authentifiée dans le dos de l'utilisateur. Correspondance **exacte**, aucun
682
+ caractère générique.
683
+
684
+ `OriginGuard` (`RealtimeHub.ts:25`), configuration `csrf.checkOrigin` (`config.ts:181`).
685
+
686
+ ### `révocation` — la connexion qui survit à sa session
687
+
688
+ Le problème que ce mot nomme : une connexion ouverte pourrait survivre à la session qui l'a
689
+ autorisée, puisque le verrou de frame ne relit rien. Un contrôle périodique referme l'écart en
690
+ re-validant les identités révocables et en fermant celles qui ne valent plus.
691
+
692
+ `registerRevocable()` (`RealtimeHub.ts:704`), `revalidateRevocable()` (`RealtimeHub.ts:736`), période
693
+ `REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:111`). Une re-validation en erreur **ferme** — jamais
694
+ l'inverse.
695
+
696
+ ### `refus` (denied) — rendre le « non » visible
697
+
698
+ Une notification refusée serait abandonnée en silence, laissant le client aveugle. Le serveur pousse
699
+ donc `realtime:denied`. Le motif est **générique** : jamais le rôle ou le scope manquant, qui
700
+ transformerait le refus en oracle d'autorisation.
701
+
702
+ `IRealtimeDenied` (`RealtimeEventMap.ts:228`), conversion en message d'interface `deniedToNotice()`
703
+ (`notice.ts:45`).
704
+
705
+ ### `audit de frame` — tracer le notable
706
+
707
+ Le signal émis sur les événements protocolaires qui méritent une trace : frame invalide, frame
708
+ refusée, méthode inconnue, erreur interne. Émis sans attente, avec le pair — ce qui permet de
709
+ retrouver **qui** a été refusé, pas seulement d'où venait le paquet.
710
+
711
+ `FrameAuditReason` (`JsonRpcPeer.ts:143`), crochet `onFrameAudit` (`JsonRpcPeer.ts:190`).
712
+
713
+ ### `seam` — le point de greffe
714
+
715
+ Littéralement une **couture** : un point prévu dans une couche basse pour qu'une couche haute y
716
+ greffe du comportement **sans modifier la couche basse**. Le module en expose cinq, et c'est ce qui
717
+ permet à la couche sécurité de se brancher sans qu'aucune ligne de realtime ne la connaisse.
718
+
719
+ Les deux du protocole sont `beforeDispatch` (`JsonRpcPeer.ts:172`) et `onFrameAudit`
720
+ (`JsonRpcPeer.ts:190`) ; les trois du hub sont l'authenticator, le matcher et le contrôle d'origine.
721
+
722
+ → [Sécurité](./securite.md) pour le détail de chacun.
723
+
724
+ ### `plafond de canaux` — la borne anti-saturation
725
+
726
+ Le nombre maximal de canaux qu'**une** connexion peut ouvrir. Chaque canal ouvert coûte des
727
+ ressources ; sans borne, une seule connexion peut les épuiser. Au-delà, l'abonnement est refusé et le
728
+ client en est informé.
729
+
730
+ `limits.maxChannelsPerConnection` (`config.ts:142`), pose `setMaxChannelsPerConnection()`
731
+ (`RealtimeHub.ts:905`).
732
+
733
+ ## 📡 L'observabilité — ce que la socket dit d'elle-même
734
+
735
+ | Terme | En une ligne |
736
+ | ----------------------- | ------------------------------------------------------- |
737
+ | `sonde` | l'état de santé instantané, en JSON |
738
+ | `back-pressure` | ce qui attend d'être envoyé sur une connexion |
739
+ | `consommateur lent` | la connexion qui n'absorbe plus assez vite |
740
+ | `compteurs de canal` | les compteurs par canal, mesurés à l'arrivée des frames |
741
+ | `santé agrégée` | la vue de tous les workers d'un pod, fusionnée |
742
+ | `plan de données admin` | l'endpoint qui sert la sonde à Studio |
743
+
744
+ ### `sonde` (probe) — l'état de santé en JSON
745
+
746
+ L'oscilloscope du module : « voici mon état, maintenant ». Canaux et abonnés, publications et
747
+ livraisons, connexions, octets, back-pressure, carte d'identité du backplane. Lecture **pure**, sans
748
+ allocation sur le chemin chaud.
749
+
750
+ `RealtimeHub.probe()` (`RealtimeHub.ts:775`) rend un `IRealtimeProbe` (`IRealtimeProbe.ts:61`).
751
+
752
+ > [!WARNING]
753
+ > `IRealtimeProbe` est la **forme des données**, pas une interface à implémenter avec une méthode
754
+ > `probe()`. La méthode appartient au hub ; l'interface décrit ce qu'elle rend.
755
+
756
+ ### `back-pressure` — ce qui attend sur le fil
757
+
758
+ La quantité d'octets déjà remise au transport mais pas encore partie. C'est le signal de saturation
759
+ n° 1 : il monte quand le client n'absorbe plus. Deux seuils distincts existent — un pour **compter**
760
+ (la sonde), un pour **agir** (abandonner des frames, puis fermer).
761
+
762
+ Seuil de comptage `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:63`) — une constante, car il ne
763
+ décide de rien. Les seuils d'ACTION, eux, sont des réglages : `websocket.maxBackpressure`,
764
+ `websocket.backpressurePolicy` et `websocket.backpressureCloseAfterDrops` (`http`,
765
+ `http/nodefony/config/config.ts:625`).
766
+
767
+ ### `consommateur lent` — la connexion qui décroche
768
+
769
+ Une connexion dont le back-pressure dépasse le seuil de comptage. C'est une **métrique**, pas une
770
+ sanction : le seuil de comptage se règle indépendamment des seuils d'action.
771
+
772
+ `slowConsumer.bytes` (`config.ts:84`), champ `backpressure.slowConsumers`
773
+ (`IRealtimeProbe.ts:81`).
774
+
775
+ ### `compteurs de canal` — mesurés à l'arrivée
776
+
777
+ Nombre de messages, date du dernier, débit instantané, historique glissant — par canal, calculés au
778
+ point d'arrivée des frames, donc réutilisables par n'importe quelle application.
779
+
780
+ `IChannelStats` (`IRealtimeSocket.ts:64`), côté serveur `IRealtimeChannelStat`
781
+ (`IRealtimeProbe.ts:47`).
782
+
783
+ ### `santé agrégée` — la vue du pod entier
784
+
785
+ En multi-worker, chaque process a **sa** sonde. La vue du pod est leur fusion : cumuls additionnés,
786
+ maxima repris au plus haut, instances listées une à une.
787
+
788
+ `mergeClusterHealth()` (`ClusterProbeClient.ts:46`), forme `IRealtimeClusterHealth`
789
+ (`IRealtimeProbe.ts:178`).
790
+
791
+ ### `plan de données admin` — la sonde servie à Studio
792
+
793
+ Le producteur qui expose la sonde sous `/nodefony/realtime/api/health`, selon la convention de
794
+ routage des administrations de modules. C'est de l'**auto-observabilité** : la socket se regarde
795
+ elle-même par le même chemin que les autres modules.
796
+
797
+ `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:91`), construction `buildOwnHealth()`
798
+ (`RealtimeAdminApi.ts:52`).
799
+
800
+ ## ⚠️ Pièges — les faux-amis du vocabulaire
801
+
802
+ | Symptôme | Cause — le mot pris pour un autre | Correction |
803
+ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
804
+ | « Je suis abonné mais je ne reçois rien » | `subscribe` confondu avec `on` : le flux est demandé, aucun écouteur ne le lit | Les deux : `subscribe(canal)` **et** `on(canal, handler)` (`IRealtimeSocket.ts:141`) |
805
+ | « Ça marche en local, plus rien dès qu'on passe à plusieurs pods » | canal resté **instance-local** — le défaut. Traverser le process est une capacité qu'on **demande** | Déclarer le préfixe dans `@RealtimeBroadcast` (`realtimeDecorators.ts:342`) |
806
+ | « Le client publie, le serveur ignore » | canal non déclaré **entrant**. Un client ne peut rien pousser tant qu'aucun handler n'existe | `@RealtimeInbound("mon:canal")` (`realtimeDecorators.ts:182`) |
807
+ | Deux déploiements se mélangent sur un même serveur Redis | pas de **cloison** — le numéro de base ne cloisonne pas le pub/sub | Poser `backplane.namespace` (`config.ts:59`) |
808
+ | Le fan-out disparaît entre conteneurs identiques | **origine** dérivée du seul identifiant de processus : deux conteneurs sont tous deux le n° 1, l'anti-écho avale tout | `resolveBackplaneOriginId()` (`originId.ts:24`) dérive du pod ou de l'hôte |
809
+ | Deux tickers pour le même tableau de bord | **cadence** différente = **canal** différent, jamais réconcilié | Fabriquer le nom via `rateChannel()` (`channelRate.ts:44`) des deux côtés |
810
+ | Un canal se croit protégé mais laisse tout passer | **politique** déclarée sans **verrou de frame** posé pour la faire respecter | Le hub le détecte et avertit : `hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1020`) |
811
+ | Une connexion garde ses flux après une déconnexion applicative | le **verrou de frame** ne relit rien : par construction, il ne voit pas la session mourir | Inscrire l'identité au registre de **révocation** (`RealtimeHub.ts:433`) |
812
+ | « Le provider a planté après la fermeture d'un onglet » | **provider** confondu avec **sink** : le provider est partagé, il survit à la connexion créatrice | N'y capturer que des dépendances à longue vie (`ChannelFactory`, `RealtimeHub.ts:149`) |
813
+ | « Notre store realtime est indisponible » | **store** employé pour **driver** : le backplane ne conserve rien, il fait passer | Dire « le driver de backplane » — les données ont des stores, les flux ont des drivers |
814
+
815
+ > [!CAUTION]
816
+ > Deux mots à ne jamais intervertir en réunion : le **hub** tombe et **un** pod perd sa diffusion ;
817
+ > le **backplane** tombe et **tous** les pods se retrouvent isolés — chacun continuant à servir ses
818
+ > propres abonnés, sans le savoir. Même symptôme côté utilisateur, causes et remèdes opposés.
819
+
820
+ ## 🧪 Tests & couverture
821
+
822
+ Le vocabulaire n'a pas de tests propres : chaque mot est vérifié par les tests du symbole qu'il
823
+ désigne. Les familles présentes dans le module — les chiffres exacts vivent dans la carte de
824
+ l'aperçu, régénérée depuis vitest et jamais figée ici :
825
+
826
+ - **unitaires** : le hub (`RealtimeHub.test.ts`), sa surface sécurité
827
+ (`RealtimeHubSecurity.test.ts`), le contrôleur, le service, les drivers de backplane, le registre,
828
+ l'origine, les décorateurs, le transport ;
829
+ - **intégration** : le driver Redis contre un serveur réel ;
830
+ - **bout en bout** : boucle locale, chemins de contrôleur, autorisation de canal, câblage du
831
+ pare-feu, cluster par communication inter-process, cluster Redis ;
832
+ - **attaque** : plafond de canaux, révocation, politique non appliquée — les trois pièges de sécurité
833
+ du lexique ont chacun leur banc hostile.
834
+
835
+ Couverture : `npm run coverage` dans `@nodefony/realtime`.
836
+
837
+ ## 🔗 Pour aller plus loin
838
+
839
+ - ⬆️ **Retour au hub** : [Realtime — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
840
+ - [Architecture](./architecture.md) — la pile complète et le trajet d'une frame, mot après mot.
841
+ - [Sécurité](./securite.md) — les points de greffe, le handshake, le verrou de frame en détail.
842
+ - [Configuration](./configuration.md) — driver, cloison, bornes : les mots devenus options.
843
+ - [Cookbook chat](./cookbook-chat.md) — tout le vocabulaire mis en œuvre d'un bout à l'autre.
844
+ - [Lexique sécurité](../../security/docs/lexique.md) — les sigles d'authentification et
845
+ d'autorisation employés par la famille sécurité de cette page.