@nodefony/realtime 10.0.0-beta.2 → 10.0.0-beta.4
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/README.md +2 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +18 -13
- package/dist/nodefony/config/config.js +7 -5
- package/dist/nodefony/config/configSchema.js +18 -0
- package/dist/nodefony/config/defineModuleConfig.js +43 -17
- package/dist/nodefony/decorators/realtimeDecorators.js +69 -19
- package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
- package/dist/nodefony/src/backplane/RedisBackplane.js +22 -6
- package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
- package/dist/nodefony/src/backplane/bootNotices.js +42 -0
- package/dist/nodefony/src/backplane/envelope.js +1 -2
- package/dist/nodefony/src/backplane/originId.js +1 -2
- package/dist/nodefony/src/backplane/publishQueue.js +1 -5
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +16 -6
- package/dist/nodefony/src/guards.js +40 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +2 -6
- package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
- package/dist/nodefony/src/server/RealtimeController.js +226 -82
- package/dist/nodefony/src/server/RealtimeHub.js +32 -54
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +30 -13
- package/dist/nodefony/src/server/deniedDetail.js +1 -2
- package/dist/nodefony/src/server/subscribedAck.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +5 -4
- package/dist/nodefony/src/server/welcomeEnv.js +1 -2
- package/dist/nodefony/src/service/RealtimeService.js +4 -4
- package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
- package/dist/nodefony/testing/index.js +118 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/nodefony/config/configSchema.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
- package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
- package/dist/types/nodefony/src/guards.d.ts +32 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +82 -4
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
- package/dist/types/nodefony/testing/index.d.ts +8 -3
- package/docs/actions.md +129 -45
- package/docs/architecture.md +143 -135
- package/docs/cadence-et-contre-pression.md +643 -0
- package/docs/configuration.md +126 -97
- package/docs/cookbook-chat.md +50 -43
- package/docs/index.md +63 -38
- package/docs/observabilite.md +58 -56
- package/docs/protocole.md +110 -76
- package/docs/securite.md +128 -117
- package/docs/vocabulaire.md +133 -129
- package/package.json +17 -13
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
- package/docs/couverture-bancs.md +0 -139
package/docs/index.md
CHANGED
|
@@ -37,7 +37,7 @@ coverageModule: realtime
|
|
|
37
37
|
|
|
38
38
|
## 🧭 Par où commencer
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Quatre parcours selon ce que tu viens faire. L'ordre compte : chaque étape suppose la précédente.
|
|
41
41
|
|
|
42
42
|
**Je découvre le temps réel Nodefony** — comprendre le modèle avant d'écrire quoi que ce soit.
|
|
43
43
|
|
|
@@ -69,20 +69,31 @@ abonné connecté à l'autre.
|
|
|
69
69
|
3. [`@nodefony/security`](../../security/docs/index.md) — le pare-feu applicatif, qui protège le
|
|
70
70
|
temps réel avec **le même** modèle de zones que le web.
|
|
71
71
|
|
|
72
|
+
**Un client ne suit plus** — l'écran prend du retard, la mémoire du serveur monte, une connexion
|
|
73
|
+
se ferme en `1013`.
|
|
74
|
+
|
|
75
|
+
1. [Cadence et contre-pression](./cadence-et-contre-pression.md) — les deux étages qui encaissent
|
|
76
|
+
un client lent : la cadence que le client ralentit lui-même, la file que le serveur refuse de
|
|
77
|
+
laisser grossir. **Commence par le schéma général**, puis les courbes.
|
|
78
|
+
2. [Observabilité](./observabilite.md) — lire les clients lents et les trames refusées dans la
|
|
79
|
+
sonde, avant que quelqu'un ne se plaigne.
|
|
80
|
+
3. [Configuration](./configuration.md) — serrer les seuils, sur les **deux** serveurs WebSocket.
|
|
81
|
+
|
|
72
82
|
## 🗂️ Les pages du module
|
|
73
83
|
|
|
74
84
|
Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce qu'on y trouve.
|
|
75
85
|
|
|
76
|
-
| Page
|
|
77
|
-
|
|
|
78
|
-
| [Vocabulaire](./vocabulaire.md)
|
|
79
|
-
| [Architecture](./architecture.md)
|
|
80
|
-
| [Configuration](./configuration.md)
|
|
81
|
-
| [Sécurité](./securite.md)
|
|
82
|
-
| [Protocole](./protocole.md)
|
|
83
|
-
| [Actions RPC](./actions.md)
|
|
84
|
-
| [Observabilité](./observabilite.md)
|
|
85
|
-
| [
|
|
86
|
+
| Page | Ce qu'elle résout | Tu en as besoin quand… |
|
|
87
|
+
| ------------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------- |
|
|
88
|
+
| [Vocabulaire](./vocabulaire.md) | les mots du domaine, une bonne fois | tu lis la doc, ou tu discutes archi |
|
|
89
|
+
| [Architecture](./architecture.md) | le trajet d'une frame, étage par étage | tu veux comprendre plutôt que régler |
|
|
90
|
+
| [Configuration](./configuration.md) | drivers, cloisonnement, bornes, contrôle d'origine | tu déploies, ou tu changes de topologie |
|
|
91
|
+
| [Sécurité](./securite.md) | identité à la poignée de main, droits par canal | ta socket est joignable depuis un navigateur |
|
|
92
|
+
| [Protocole](./protocole.md) | la grammaire d'une frame, et les codes d'erreur | tu débogues le fil, ou tu écris un client |
|
|
93
|
+
| [Actions RPC](./actions.md) | appeler le serveur et attendre une réponse | tu veux savoir si l'appel a marché |
|
|
94
|
+
| [Observabilité](./observabilite.md) | la sonde, les canaux de santé, les écrans | tu te demandes si ta socket va bien |
|
|
95
|
+
| [Cadence et contre-pression](./cadence-et-contre-pression.md) | un client trop lent : ralentir, refuser, couper | un écran prend du retard, ou la mémoire monte |
|
|
96
|
+
| [Cookbook — un chat](./cookbook-chat.md) | l'exemple complet, client et serveur | tu veux du code qui marche tout de suite |
|
|
86
97
|
|
|
87
98
|
```nodefony-cards
|
|
88
99
|
[
|
|
@@ -107,6 +118,9 @@ Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce q
|
|
|
107
118
|
{ "icon": "📡", "title": "observabilite", "href": "./observabilite.md",
|
|
108
119
|
"desc": "Ce que la sonde du hub expose, les canaux de santé et leur cadence, ce que Studio en montre — et comment écrire la sienne sans faire fuir la mémoire ni empêcher le processus de s'arrêter.",
|
|
109
120
|
"meta": "tu te demandes si ta socket va bien" },
|
|
121
|
+
{ "icon": "🚦", "title": "cadence-et-contre-pression", "href": "./cadence-et-contre-pression.md",
|
|
122
|
+
"desc": "Deux étages qui ne se parlent pas et se rencontrent quand même : le client ralentit la cadence de ses canaux d'état (AIMD), le serveur refuse d'empiler au-delà de 4 Mio puis ferme en 1013. Avec les courbes calculées par le vrai code, et les sondes qui le montrent en production.",
|
|
123
|
+
"meta": "un écran prend du retard, ou la mémoire du serveur monte" },
|
|
110
124
|
{ "icon": "🍳", "title": "cookbook-chat", "href": "./cookbook-chat.md",
|
|
111
125
|
"desc": "Un salon de discussion complet : configuration, service métier, contrôleur, client navigateur, puis le passage en cluster.",
|
|
112
126
|
"meta": "du code qui tourne, à déformer vers ton besoin" }
|
|
@@ -119,34 +133,34 @@ Quatre propriétés, toutes vérifiables dans le code — c'est ce qui justifie
|
|
|
119
133
|
WebSocket brute.
|
|
120
134
|
|
|
121
135
|
**Un contrôleur temps réel est un contrôleur.** `RealtimeController`
|
|
122
|
-
(`RealtimeController.ts
|
|
136
|
+
(`RealtimeController.ts#RealtimeController`) étend le `Controller` du framework : il se déclare avec les mêmes
|
|
123
137
|
décorateurs de route, reçoit la même injection, passe par le même pare-feu. HTTP et WebSocket ne sont
|
|
124
138
|
pas deux applications à écrire deux fois, mais deux entrées du même pipeline.
|
|
125
139
|
|
|
126
140
|
**Une connexion, N canaux, dans les deux sens.** Le client s'abonne à autant de canaux qu'il veut sur
|
|
127
141
|
la même socket. Trois formes de trafic coexistent : le serveur diffuse (`@RealtimeChannel`,
|
|
128
|
-
`realtimeDecorators.ts
|
|
129
|
-
`realtimeDecorators.ts
|
|
130
|
-
`realtimeDecorators.ts
|
|
142
|
+
`realtimeDecorators.ts#RealtimeChannel`), le client appelle et attend une réponse (`@RealtimeAction`,
|
|
143
|
+
`realtimeDecorators.ts#RealtimeAction`), le client pousse sans attendre (`@RealtimeInbound`,
|
|
144
|
+
`realtimeDecorators.ts#RealtimeInbound`). Rien n'est ouvert qui n'ait été déclaré.
|
|
131
145
|
|
|
132
146
|
**Le travail est fait une fois par processus, pas une fois par client.** Le `RealtimeHub`
|
|
133
|
-
(`RealtimeHub.ts
|
|
147
|
+
(`RealtimeHub.ts#RealtimeHub`) tient **un seul producteur par canal** : le premier abonné le démarre, le
|
|
134
148
|
dernier départ l'arrête. Chaque connexion supplémentaire n'ajoute qu'une destination de diffusion.
|
|
135
149
|
Un salon suivi par mille personnes coûte un producteur, pas mille.
|
|
136
150
|
|
|
137
151
|
**Le backplane est un transport, jamais un magasin.** Il relaie les publications d'un processus vers
|
|
138
152
|
les autres, et rien de plus : aucun historique, aucune reprise. Les drivers vivent dans un registre
|
|
139
|
-
(`registerBackplaneDriver`, `backplaneRegistry.ts
|
|
153
|
+
(`registerBackplaneDriver`, `backplaneRegistry.ts#registerBackplaneDriver`) — `loopback` (un seul processus), `cluster`
|
|
140
154
|
(échanges entre workers d'un même pod), `redis` (entre machines) — et tu peux y inscrire le tien sans
|
|
141
155
|
toucher au cœur.
|
|
142
156
|
|
|
143
157
|
> [!IMPORTANT]
|
|
144
158
|
> **Rien ne franchit la frontière du processus sans intention explicite.** Par défaut, un canal reste
|
|
145
159
|
> local. Il faut le déclarer diffusable (`@RealtimeBroadcast`,
|
|
146
|
-
> `realtimeDecorators.ts
|
|
160
|
+
> `realtimeDecorators.ts#RealtimeBroadcast`) pour qu'il emprunte le backplane. C'est volontaire : un canal
|
|
147
161
|
> d'observation qui décrit l'état d'**un** pod n'aurait aucun sens répliqué sur les autres.
|
|
148
162
|
|
|
149
|
-
Le module se déclare par ailleurs **non critique** (`Realtime.critical`, `index.ts
|
|
163
|
+
Le module se déclare par ailleurs **non critique** (`Realtime.critical`, `index.ts#IRealtimeHarness.call`) et son
|
|
150
164
|
démarrage de backplane est borné dans le temps : un Redis injoignable n'empêche pas l'application de
|
|
151
165
|
monter, le hub reste local, et la dégradation est annoncée dans les journaux plutôt que subie.
|
|
152
166
|
|
|
@@ -158,24 +172,35 @@ fichiers.
|
|
|
158
172
|
### 1. Déclarer le module
|
|
159
173
|
|
|
160
174
|
```ts
|
|
175
|
+
// nodefony/config/realtime.ts — le fragment du module, importé par le manifeste
|
|
176
|
+
import type { IRealtimeConfigInput } from "@nodefony/realtime";
|
|
177
|
+
|
|
178
|
+
export const realtimeConfig = () =>
|
|
179
|
+
({
|
|
180
|
+
// Un seul processus en développement : le hub distribue localement,
|
|
181
|
+
// aucun transport réseau n'est ouvert. Passer à "redis" en production.
|
|
182
|
+
backplane: { driver: "loopback" },
|
|
183
|
+
// Défense CSRF à la poignée de main (RFC 6455 §10.2) : sans elle, un site
|
|
184
|
+
// tiers peut ouvrir une socket avec les cookies de ton utilisateur.
|
|
185
|
+
csrf: {
|
|
186
|
+
checkOrigin: {
|
|
187
|
+
enabled: true,
|
|
188
|
+
allowList: ["https://127.0.0.1:5152"],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
}) satisfies IRealtimeConfigInput;
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```ts ignore
|
|
161
195
|
// nodefony.config.ts — l'orchestrateur de l'application
|
|
196
|
+
import { defineConfig, use } from "nodefony";
|
|
197
|
+
import { realtimeConfig } from "./nodefony/config/realtime";
|
|
198
|
+
|
|
162
199
|
export default defineConfig(() => ({
|
|
163
200
|
modules: [
|
|
164
201
|
"@nodefony/http",
|
|
165
202
|
"@nodefony/framework",
|
|
166
|
-
use("@nodefony/realtime",
|
|
167
|
-
// Un seul processus en développement : le hub distribue localement,
|
|
168
|
-
// aucun transport réseau n'est ouvert. Passer à "redis" en production.
|
|
169
|
-
backplane: { driver: "loopback" },
|
|
170
|
-
// Défense CSRF à la poignée de main (RFC 6455 §10.2) : sans elle, un site
|
|
171
|
-
// tiers peut ouvrir une socket avec les cookies de ton utilisateur.
|
|
172
|
-
csrf: {
|
|
173
|
-
checkOrigin: {
|
|
174
|
-
enabled: true,
|
|
175
|
-
allowList: ["https://127.0.0.1:5152"],
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
}),
|
|
203
|
+
use("@nodefony/realtime", realtimeConfig()),
|
|
179
204
|
],
|
|
180
205
|
}));
|
|
181
206
|
```
|
|
@@ -239,11 +264,11 @@ export default ChatController;
|
|
|
239
264
|
Le client est isomorphe : c'est le cœur `nodefony` lui-même, importable côté navigateur.
|
|
240
265
|
|
|
241
266
|
```ts
|
|
242
|
-
import {
|
|
267
|
+
import { NodefonySocket } from "nodefony/client";
|
|
243
268
|
|
|
244
269
|
// `shared` réutilise la connexion existante pour une même URL : dix composants
|
|
245
270
|
// qui écoutent dix canaux ouvrent UNE socket, pas dix.
|
|
246
|
-
const socket =
|
|
271
|
+
const socket = NodefonySocket.shared({
|
|
247
272
|
url: "wss://127.0.0.1:5152/chat/realtime",
|
|
248
273
|
});
|
|
249
274
|
|
|
@@ -291,11 +316,11 @@ reste local et le dit.
|
|
|
291
316
|
## 🧰 Surface publique
|
|
292
317
|
|
|
293
318
|
Côté serveur : `RealtimeController` (la classe à étendre), les décorateurs `RealtimeChannel`,
|
|
294
|
-
`RealtimeAction` et `RealtimeInbound`, le service `RealtimeService` (`RealtimeService.ts
|
|
319
|
+
`RealtimeAction` et `RealtimeInbound`, le service `RealtimeService` (`RealtimeService.ts#RealtimeService`) pour
|
|
295
320
|
publier depuis n'importe quel service injecté, le `RealtimeHub` et sa sonde, les trois backplanes
|
|
296
321
|
natifs, et le registre de drivers pour brancher le tien.
|
|
297
322
|
|
|
298
|
-
Côté client : `
|
|
323
|
+
Côté client : `NodefonySocket` via le subpath `nodefony/client`, et les hooks React via
|
|
299
324
|
`nodefony/react`.
|
|
300
325
|
|
|
301
326
|
Les signatures exactes vivent dans le graphe généré — `jq '.symbols.RealtimeHub' .ai/symbols.json` —
|
|
@@ -304,7 +329,7 @@ jamais recopiées ici : elles divergeraient en silence.
|
|
|
304
329
|
## ⚙️ Configuration
|
|
305
330
|
|
|
306
331
|
Un seul point d'entrée : `use("@nodefony/realtime", { … })` dans `nodefony.config.ts`, validé au boot
|
|
307
|
-
contre le schéma du module (`realtimeConfigSchema`, `config.ts
|
|
332
|
+
contre le schéma du module (`realtimeConfigSchema`, `config.ts#realtimeConfigSchema`). Six blocs :
|
|
308
333
|
|
|
309
334
|
- `backplane` — le driver de fan-out et son espace de nommage. Ce cloisonnement devient
|
|
310
335
|
indispensable dès que **deux déploiements partagent le même Redis** : sans lui, leurs publications
|
|
@@ -326,7 +351,7 @@ vue de tous les workers d'un pod.
|
|
|
326
351
|
|
|
327
352
|
Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
|
|
328
353
|
diffusion, connexions, octets et frames, pression d'écriture. C'est la même donnée que celle rendue à
|
|
329
|
-
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts
|
|
354
|
+
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts#RealtimeHub.probe`).
|
|
330
355
|
|
|
331
356
|
## 🧪 Tests & couverture
|
|
332
357
|
|
package/docs/observabilite.md
CHANGED
|
@@ -96,7 +96,7 @@ temps de réponse — elle mesure **ce qui n'est pas encore parti**.
|
|
|
96
96
|
Trois partis pris, tous lisibles dans le code.
|
|
97
97
|
|
|
98
98
|
**1. La sonde est une lecture pure, jamais un collecteur.** `RealtimeHub.probe()`
|
|
99
|
-
(`RealtimeHub.ts
|
|
99
|
+
(`RealtimeHub.ts#RealtimeHub.probe`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
|
|
100
100
|
Aucune entrée-sortie, aucune exception possible, aucun état de lecture conservé. Appeler la sonde
|
|
101
101
|
mille fois par seconde ne changerait rien à ce que le hub fait par ailleurs.
|
|
102
102
|
|
|
@@ -107,7 +107,7 @@ divergence entre deux consommateurs, et un rafraîchissement qui se choisit libr
|
|
|
107
107
|
|
|
108
108
|
**3. La mesure voyage sur la socket qu'elle décrit.** Le même producteur alimente l'endpoint HTTP
|
|
109
109
|
(premier affichage, sans attendre un tick) et le canal temps réel (le suivi). Un seul code, deux
|
|
110
|
-
portes — `buildRealtimeHealth()` (`RealtimeAdminApi.ts
|
|
110
|
+
portes — `buildRealtimeHealth()` (`RealtimeAdminApi.ts#buildRealtimeHealth`).
|
|
111
111
|
|
|
112
112
|
Le compromis assumé : cette sonde est **per-instance par nature**. Un pod à quatre workers rend quatre
|
|
113
113
|
vérités partielles. Nodefony ne les moyenne pas au hasard de la requête — il pousse une vue agrégée
|
|
@@ -228,10 +228,10 @@ Côté navigateur, deux lignes suffisent à consommer le canal :
|
|
|
228
228
|
|
|
229
229
|
```ts
|
|
230
230
|
// frontend/src/shopHealth.ts
|
|
231
|
-
import {
|
|
231
|
+
import { NodefonySocket } from "nodefony/client";
|
|
232
232
|
|
|
233
233
|
const scheme = window.location.protocol === "https:" ? "wss" : "ws";
|
|
234
|
-
const socket = new
|
|
234
|
+
const socket = new NodefonySocket({
|
|
235
235
|
url: `${scheme}://${window.location.host}/shop/realtime`,
|
|
236
236
|
});
|
|
237
237
|
|
|
@@ -247,26 +247,26 @@ socket.subscribe("shop:health");
|
|
|
247
247
|
|
|
248
248
|
## 🧰 Ce que la sonde expose, champ par champ
|
|
249
249
|
|
|
250
|
-
Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts
|
|
250
|
+
Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts#IRealtimeProbe`). Quatre familles, un discriminant.
|
|
251
251
|
|
|
252
252
|
### Canaux — qui vit, qui écoute
|
|
253
253
|
|
|
254
|
-
| Champ | Ce qu'il dit
|
|
255
|
-
| ------------------------ |
|
|
256
|
-
| `channels[]` | Une entrée par canal **actif** (au moins un abonné), cf `IRealtimeChannelStat` (`IRealtimeProbe.ts
|
|
257
|
-
| `channels[].channel` | Le nom exact souscrit, **suffixe de cadence compris** (`nodefony:orm:health:2000`).
|
|
258
|
-
| `channels[].subscribers` | Abonnés locaux vivants. Instantané, pas un cumul.
|
|
259
|
-
| `channels[].messages` | Publications cumulées sur ce canal.
|
|
260
|
-
| `channelCount` | Nombre de canaux actifs sur ce processus.
|
|
254
|
+
| Champ | Ce qu'il dit |
|
|
255
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| `channels[]` | Une entrée par canal **actif** (au moins un abonné), cf `IRealtimeChannelStat` (`IRealtimeProbe.ts#IRealtimeChannelStat`). |
|
|
257
|
+
| `channels[].channel` | Le nom exact souscrit, **suffixe de cadence compris** (`nodefony:orm:health:2000`). |
|
|
258
|
+
| `channels[].subscribers` | Abonnés locaux vivants. Instantané, pas un cumul. |
|
|
259
|
+
| `channels[].messages` | Publications cumulées sur ce canal. |
|
|
260
|
+
| `channelCount` | Nombre de canaux actifs sur ce processus. |
|
|
261
261
|
|
|
262
262
|
### Fan-out — l'amplification réelle du broker
|
|
263
263
|
|
|
264
|
-
| Champ | Ce qu'il dit
|
|
265
|
-
| ---------------------- |
|
|
266
|
-
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.**
|
|
267
|
-
| `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.**
|
|
268
|
-
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts
|
|
269
|
-
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable.
|
|
264
|
+
| Champ | Ce qu'il dit |
|
|
265
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
266
|
+
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
|
|
267
|
+
| `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
|
|
268
|
+
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts#RealtimeHub.recordInbound`). |
|
|
269
|
+
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
|
|
270
270
|
|
|
271
271
|
Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
|
|
272
272
|
moyenne d'un salon**. S'il vaut 1, chaque publication ne touche qu'une personne — un canal par client,
|
|
@@ -314,15 +314,17 @@ deux autres. Le débit en octets par seconde se dérive de deux photos.
|
|
|
314
314
|
| `backpressure.slowConsumers` | Connexions au-dessus du seuil de comptage. |
|
|
315
315
|
| `backpressure.drops` | Cumul des frames **jetées** pour protéger la mémoire. |
|
|
316
316
|
|
|
317
|
-
La source par connexion est `IRealtimeConnProbe` (`IRealtimeProbe.ts
|
|
317
|
+
La source par connexion est `IRealtimeConnProbe` (`IRealtimeProbe.ts#IRealtimeConnProbe`), implémentée par le
|
|
318
318
|
transport. Les seuils d'ACTION sont distincts du seuil de comptage, et ils sont
|
|
319
|
-
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts
|
|
319
|
+
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts#httpConfigSchema`) : au-delà de
|
|
320
320
|
`websocket.maxBackpressure` (4 MiB par défaut) la politique `websocket.backpressurePolicy`
|
|
321
|
-
s'applique — `drop` par défaut, la frame est **jetée
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
321
|
+
s'applique — `drop` par défaut, la frame est **jetée**. Sur un canal d'état c'est sans
|
|
322
|
+
conséquence, le prochain instantané la remplace ; mais le rejet ne trie rien, et une réponse RPC
|
|
323
|
+
ou un événement jetés sont **perdus**. La connexion est **fermée** en `1013` (« réessaie plus
|
|
324
|
+
tard ») quand le **solde** de refus atteint `websocket.backpressureCloseAfterDrops` (1000 par
|
|
325
|
+
défaut) — +1 par frame jetée, −1 par frame envoyée, si bien qu'un pic passager redescend — ou
|
|
326
|
+
dès le premier dépassement si la politique est `close`. Le mécanisme complet, avec ses courbes :
|
|
327
|
+
[Cadence et contre-pression](./cadence-et-contre-pression.md).
|
|
326
328
|
|
|
327
329
|
> [!IMPORTANT]
|
|
328
330
|
> `drops` qui croît n'est **pas** une panne : c'est la protection qui fonctionne. Ce qui serait une
|
|
@@ -355,8 +357,8 @@ Trois champs n'apparaissent que lorsqu'ils ont un sens :
|
|
|
355
357
|
|
|
356
358
|
### La couche d'identité, au-dessus
|
|
357
359
|
|
|
358
|
-
`buildOwnHealth()` (`RealtimeAdminApi.ts
|
|
359
|
-
`IRealtimeHealth` (`IRealtimeProbe.ts
|
|
360
|
+
`buildOwnHealth()` (`RealtimeAdminApi.ts#buildOwnHealth`) enrichit ce snapshot pur pour produire
|
|
361
|
+
`IRealtimeHealth` (`IRealtimeProbe.ts#IRealtimeHealth`) : `instanceId`, la santé **process** du worker
|
|
360
362
|
(CPU, mémoire, boucle d'événements), et — si elles ont été branchées — la santé ORM et les compteurs
|
|
361
363
|
d'erreurs du journal. Ces trois derniers champs sont **additifs et optionnels** : un consommateur qui
|
|
362
364
|
ne les connaît pas les ignore.
|
|
@@ -368,22 +370,22 @@ resterait vide jusqu'au premier tick.
|
|
|
368
370
|
|
|
369
371
|
### L'instantané HTTP
|
|
370
372
|
|
|
371
|
-
`GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts
|
|
373
|
+
`GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts#createRealtimeAdminApi`)
|
|
372
374
|
sous le namespace `realtime` du data plane admin. Sert le premier affichage, les sondes de liveness,
|
|
373
375
|
et tout script qui ne veut pas ouvrir une socket.
|
|
374
376
|
|
|
375
377
|
### Le canal temps réel
|
|
376
378
|
|
|
377
379
|
`nodefony:socket`. Le producteur est un minuteur qui rappelle **le même endpoint** à travers le
|
|
378
|
-
courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts
|
|
379
|
-
via `createBrokerTicker()` (`providers.ts
|
|
380
|
+
courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts#StudioRealtimeController.createRealtimeChannel`,
|
|
381
|
+
via `createBrokerTicker()` (`providers.ts#createBrokerTicker`)). Rien n'est dupliqué : le canal est un endpoint
|
|
380
382
|
rejoué.
|
|
381
383
|
|
|
382
384
|
### La cadence vit dans le nom du canal
|
|
383
385
|
|
|
384
386
|
Convention isomorphe, partagée par les deux bords : `base` nu = cadence par défaut du serveur ;
|
|
385
|
-
`base:<ms>` = cadence explicite. Le client fabrique le nom avec `rateChannel()` (`channelRate.ts
|
|
386
|
-
le serveur le résout et le **borne** avec `parseRate()` (`channelRate.ts
|
|
387
|
+
`base:<ms>` = cadence explicite. Le client fabrique le nom avec `rateChannel()` (`channelRate.ts#rateChannel`),
|
|
388
|
+
le serveur le résout et le **borne** avec `parseRate()` (`channelRate.ts#parseRate`).
|
|
387
389
|
|
|
388
390
|
Conséquence voulue : **un canal = une cadence = un compteur de références**. `nodefony:socket:500` et
|
|
389
391
|
`nodefony:socket` sont deux minuteurs distincts, jamais réconciliés — et deux consommateurs qui
|
|
@@ -392,23 +394,23 @@ veulent tous deux le défaut partagent un seul minuteur, parce que la cadence pa
|
|
|
392
394
|
|
|
393
395
|
### Les canaux de santé qui circulent sur la socket
|
|
394
396
|
|
|
395
|
-
Les bornes sont déclarées dans `RATE_BOUNDS` (`StudioRealtimeController.ts
|
|
396
|
-
`CHANNELS` (`providers.ts
|
|
397
|
+
Les bornes sont déclarées dans `RATE_BOUNDS` (`StudioRealtimeController.ts#RATE_BOUNDS`), les noms dans
|
|
398
|
+
`CHANNELS` (`providers.ts#CHANNELS`). Une cadence hors bornes est **ramenée dans l'intervalle**, jamais
|
|
397
399
|
refusée.
|
|
398
400
|
|
|
399
|
-
| Canal | Qui le produit
|
|
400
|
-
| ---------------------- |
|
|
401
|
-
| `nodefony:socket` | La sonde du hub, rejouée depuis l'endpoint admin `realtime`
|
|
402
|
-
| `nodefony:supervision` | `createStatsTicker()` (`providers.ts
|
|
403
|
-
| `nodefony:debugbar` | Le même producteur, canal séparé pour la barre de débogage
|
|
404
|
-
| `nodefony:orm:health` | Endpoint admin `orm/connection/health`, rejoué
|
|
405
|
-
| `nodefony:orm:flow` | Endpoint admin `orm/flow` (débit de requêtes)
|
|
406
|
-
| `nodefony:syslog` | `createSyslogBridge()` (`providers.ts
|
|
401
|
+
| Canal | Qui le produit | Cadence par défaut | Bornes |
|
|
402
|
+
| ---------------------- | -------------------------------------------------------------------------------- | ------------------ | ------------- |
|
|
403
|
+
| `nodefony:socket` | La sonde du hub, rejouée depuis l'endpoint admin `realtime` | 2 s | 500 ms – 60 s |
|
|
404
|
+
| `nodefony:supervision` | `createStatsTicker()` (`providers.ts#createStatsTicker`) — CPU, mémoire, GC, ELU | 1 s | 250 ms – 60 s |
|
|
405
|
+
| `nodefony:debugbar` | Le même producteur, canal séparé pour la barre de débogage | 1 s | 250 ms – 60 s |
|
|
406
|
+
| `nodefony:orm:health` | Endpoint admin `orm/connection/health`, rejoué | 5 s | 1 s – 60 s |
|
|
407
|
+
| `nodefony:orm:flow` | Endpoint admin `orm/flow` (débit de requêtes) | 2 s | 500 ms – 60 s |
|
|
408
|
+
| `nodefony:syslog` | `createSyslogBridge()` (`providers.ts#createSyslogBridge`) | **événementiel** | — (coalescé) |
|
|
407
409
|
|
|
408
410
|
> [!WARNING]
|
|
409
411
|
> **`nodefony:dashboard` n'est servi par aucun fournisseur.** Le canal de supervision s'appelle
|
|
410
412
|
> `nodefony:supervision`. Le nom survit pourtant comme constante exportée, dépréciée
|
|
411
|
-
> (`PLATFORM_CHANNELS.dashboard`, `platformChannels.ts
|
|
413
|
+
> (`PLATFORM_CHANNELS.dashboard`, `platformChannels.ts#PLATFORM_CHANNELS`, retrait en 11.0.0) — c'est ce qui le rend piégeux. S'abonner à un canal inconnu ne lève **aucune
|
|
412
414
|
> erreur** — le provider rend `null` et rien n'arrive jamais. C'est le piège numéro un de la page.
|
|
413
415
|
|
|
414
416
|
`nodefony:syslog` est le seul de la liste à ne pas être cadencé : il **relaie** au lieu de sonder. Comme
|
|
@@ -439,11 +441,11 @@ après fusion et validation — la seule vue qui dit ce qui s'applique, plutôt
|
|
|
439
441
|
|
|
440
442
|
## ⚙️ Configuration
|
|
441
443
|
|
|
442
|
-
| Clé | Défaut | Effet
|
|
443
|
-
| ------------------------ | ------ |
|
|
444
|
-
| `slowConsumer.bytes` | 1 Mio | Seuil de **comptage** de `slowConsumers` dans la sonde (`config.ts
|
|
445
|
-
| `cluster.probe.enabled` | `true` | Branche la sonde agrégée pod en worker de cluster (`config.ts
|
|
446
|
-
| `NF_CLUSTER_PROBE` (env) | — | Mis à `0`, coupe la sonde pod quelle que soit la configuration.
|
|
444
|
+
| Clé | Défaut | Effet |
|
|
445
|
+
| ------------------------ | ------ | ---------------------------------------------------------------------------------------- |
|
|
446
|
+
| `slowConsumer.bytes` | 1 Mio | Seuil de **comptage** de `slowConsumers` dans la sonde (`config.ts#slowConsumerSchema`). |
|
|
447
|
+
| `cluster.probe.enabled` | `true` | Branche la sonde agrégée pod en worker de cluster (`config.ts#probeSchema`). |
|
|
448
|
+
| `NF_CLUSTER_PROBE` (env) | — | Mis à `0`, coupe la sonde pod quelle que soit la configuration. |
|
|
447
449
|
|
|
448
450
|
> [!CAUTION]
|
|
449
451
|
> `slowConsumer.bytes` ne pilote **que la métrique**. Les seuils qui jettent une frame ou ferment une
|
|
@@ -452,7 +454,7 @@ après fusion et validation — la seule vue qui dit ce qui s'applique, plutôt
|
|
|
452
454
|
> ne change rien au comportement de la socket.
|
|
453
455
|
|
|
454
456
|
Couper la sonde cluster est un **contournement total**, pas une mise en sourdine : sans elle, il n'y a
|
|
455
|
-
ni client, ni minuteur, ni écouteur, ni message IPC (`Realtime.wireClusterProbe()`, `index.ts
|
|
457
|
+
ni client, ni minuteur, ni écouteur, ni message IPC (`Realtime.wireClusterProbe()`, `index.ts#createRealtimeHarness`), et
|
|
456
458
|
l'endpoint retombe sur la vue per-instance. Aucun coût « au cas où ».
|
|
457
459
|
|
|
458
460
|
## 🏗️ En cluster — par pod ou par worker ?
|
|
@@ -462,11 +464,11 @@ instantané est exact — et ne dit rien des trois autres. Pire : deux appels co
|
|
|
462
464
|
sur deux workers et sembler se contredire.
|
|
463
465
|
|
|
464
466
|
**La réponse de Nodefony est un modèle _push_.** Chaque worker remonte périodiquement sa santé au
|
|
465
|
-
maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts
|
|
467
|
+
maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts#ClusterProbeClient.start`) ; le maître fusionne et
|
|
466
468
|
rediffuse ; chaque worker met le résultat en cache. **N'importe lequel** sert alors la vue pod en temps
|
|
467
|
-
constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts
|
|
469
|
+
constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts#ClusterProbeClient.getClusterHealth`).
|
|
468
470
|
|
|
469
|
-
La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts
|
|
471
|
+
La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts#mergeClusterHealth`), est une fonction pure — et sa règle
|
|
470
472
|
mérite d'être connue :
|
|
471
473
|
|
|
472
474
|
| Grandeur | Agrégation | Pourquoi |
|
|
@@ -476,14 +478,14 @@ mérite d'être connue :
|
|
|
476
478
|
| `backpressure.maxBufferedAmount` | **maximum** | La santé d'une flotte se juge sur son **pire** membre, pas sur sa moyenne. |
|
|
477
479
|
| `slowConsumers`, `drops` | somme | Ce sont des dénombrements. |
|
|
478
480
|
|
|
479
|
-
Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`IRealtimeProbe.ts
|
|
481
|
+
Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`IRealtimeProbe.ts#IRealtimeClusterHealth`) :
|
|
480
482
|
un consommateur sait immédiatement s'il lit une vue pod ou une vue per-instance, et `instances[]`
|
|
481
483
|
garde le détail par worker pour le forage.
|
|
482
484
|
|
|
483
485
|
### On paie ce qu'on regarde
|
|
484
486
|
|
|
485
487
|
Les sondes coûteuses ne tournent pas en permanence. Le maître peut demander à un worker précis
|
|
486
|
-
d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts
|
|
488
|
+
d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts#ClusterProbeClient.requestEnrich`) — quand
|
|
487
489
|
un humain ouvre le forage de ce `pid`. La sonde riche (espaces mémoire, cycles de ramasse-miettes,
|
|
488
490
|
descripteurs actifs) est alors allouée ; à la fermeture de l'écran, elle est libérée et son observateur
|
|
489
491
|
détaché.
|
|
@@ -504,8 +506,8 @@ l'orchestrateur finit par le tuer de force à l'expiration du délai de grâce.
|
|
|
504
506
|
|
|
505
507
|
La règle est tenue partout dans le code, y compris là où on ne l'attendrait pas — le tick de
|
|
506
508
|
revalidation d'identité du hub, qui ne démarre qu'au premier inscrit, est lui aussi détaché
|
|
507
|
-
(`RealtimeHub.ts
|
|
508
|
-
minuteur générique des canaux (`providers.ts
|
|
509
|
+
(`RealtimeHub.ts#RealtimeHub.subscribeClient`), tout comme le report de la sonde cluster (`ClusterProbeClient.ts#ClusterProbeClient.#ormRich`) et le
|
|
510
|
+
minuteur générique des canaux (`providers.ts#createBrokerTicker`).
|
|
509
511
|
|
|
510
512
|
```ts ignore
|
|
511
513
|
const timer = setInterval(tick, 2000);
|