@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,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.
|