@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.
Files changed (63) hide show
  1. package/README.md +2 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +18 -13
  4. package/dist/nodefony/config/config.js +7 -5
  5. package/dist/nodefony/config/configSchema.js +18 -0
  6. package/dist/nodefony/config/defineModuleConfig.js +43 -17
  7. package/dist/nodefony/decorators/realtimeDecorators.js +69 -19
  8. package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
  9. package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
  10. package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
  11. package/dist/nodefony/src/backplane/RedisBackplane.js +22 -6
  12. package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
  13. package/dist/nodefony/src/backplane/bootNotices.js +42 -0
  14. package/dist/nodefony/src/backplane/envelope.js +1 -2
  15. package/dist/nodefony/src/backplane/originId.js +1 -2
  16. package/dist/nodefony/src/backplane/publishQueue.js +1 -5
  17. package/dist/nodefony/src/cluster/ClusterProbeClient.js +16 -6
  18. package/dist/nodefony/src/guards.js +40 -0
  19. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +2 -6
  20. package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
  21. package/dist/nodefony/src/server/RealtimeController.js +226 -82
  22. package/dist/nodefony/src/server/RealtimeHub.js +32 -54
  23. package/dist/nodefony/src/server/ServerRealtimeSocket.js +30 -13
  24. package/dist/nodefony/src/server/deniedDetail.js +1 -2
  25. package/dist/nodefony/src/server/subscribedAck.js +29 -0
  26. package/dist/nodefony/src/server/syslogUplink.js +5 -4
  27. package/dist/nodefony/src/server/welcomeEnv.js +1 -2
  28. package/dist/nodefony/src/service/RealtimeService.js +4 -4
  29. package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
  30. package/dist/nodefony/testing/index.js +118 -5
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/nodefony/config/configSchema.d.ts +12 -0
  33. package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
  34. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
  35. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
  36. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
  37. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
  38. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
  39. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  40. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
  41. package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
  42. package/dist/types/nodefony/src/guards.d.ts +32 -0
  43. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
  44. package/dist/types/nodefony/src/server/RealtimeController.d.ts +82 -4
  45. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
  46. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
  47. package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
  48. package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
  49. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
  50. package/dist/types/nodefony/testing/index.d.ts +8 -3
  51. package/docs/actions.md +129 -45
  52. package/docs/architecture.md +143 -135
  53. package/docs/cadence-et-contre-pression.md +643 -0
  54. package/docs/configuration.md +126 -97
  55. package/docs/cookbook-chat.md +50 -43
  56. package/docs/index.md +63 -38
  57. package/docs/observabilite.md +58 -56
  58. package/docs/protocole.md +110 -76
  59. package/docs/securite.md +128 -117
  60. package/docs/vocabulaire.md +133 -129
  61. package/package.json +17 -13
  62. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
  63. 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
- Trois parcours selon ce que tu viens faire. L'ordre compte : chaque étape suppose la précédente.
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 | Ce qu'elle résout | Tu en as besoin quand… |
77
- | ---------------------------------------- | -------------------------------------------------- | -------------------------------------------- |
78
- | [Vocabulaire](./vocabulaire.md) | les mots du domaine, une bonne fois | tu lis la doc, ou tu discutes archi |
79
- | [Architecture](./architecture.md) | le trajet d'une frame, étage par étage | tu veux comprendre plutôt que régler |
80
- | [Configuration](./configuration.md) | drivers, cloisonnement, bornes, contrôle d'origine | tu déploies, ou tu changes de topologie |
81
- | [Sécurité](./securite.md) | identité à la poignée de main, droits par canal | ta socket est joignable depuis un navigateur |
82
- | [Protocole](./protocole.md) | la grammaire d'une frame, et les codes d'erreur | tu débogues le fil, ou tu écris un client |
83
- | [Actions RPC](./actions.md) | appeler le serveur et attendre une réponse | tu veux savoir si l'appel a marché |
84
- | [Observabilité](./observabilite.md) | la sonde, les canaux de santé, les écrans | tu te demandes si ta socket va bien |
85
- | [Cookbook — un chat](./cookbook-chat.md) | l'exemple complet, client et serveur | tu veux du code qui marche tout de suite |
86
+ | 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:144`) étend le `Controller` du framework : il se déclare avec les mêmes
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:192`), le client appelle et attend une réponse (`@RealtimeAction`,
129
- `realtimeDecorators.ts:142`), le client pousse sans attendre (`@RealtimeInbound`,
130
- `realtimeDecorators.ts:231`). Rien n'est ouvert qui n'ait été déclaré.
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:213`) tient **un seul producteur par canal** : le premier abonné le démarre, le
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:55`) — `loopback` (un seul processus), `cluster`
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:342`) pour qu'il emprunte le backplane. C'est volontaire : un canal
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:144`) et son
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 { RealtimeClient } from "nodefony/client";
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 = RealtimeClient.shared({
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:50`) pour
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 : `RealtimeClient` via le subpath `nodefony/client`, et les hooks React via
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:259`). Six blocs :
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:865`).
354
+ l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts#RealtimeHub.probe`).
330
355
 
331
356
  ## 🧪 Tests & couverture
332
357
 
@@ -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:865`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
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:74`).
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 { RealtimeClient } from "nodefony/client";
231
+ import { NodefonySocket } from "nodefony/client";
232
232
 
233
233
  const scheme = window.location.protocol === "https:" ? "wss" : "ws";
234
- const socket = new RealtimeClient({
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:61`). Quatre familles, un discriminant.
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:47`). |
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:854`). |
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:25`), implémentée par le
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:1044`) : au-delà de
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** puisque les canaux d'état sont « le
322
- dernier gagne » et que le prochain instantané la remplacera. La connexion est **fermée** en
323
- `1013` (« réessaie plus tard ») après `websocket.backpressureCloseAfterDrops` frames jetées
324
- CONSÉCUTIVES (1000 par défaut) — une seule frame qui repart remet le compteur à zéro — ou
325
- dès le premier dépassement si la politique est `close`.
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:52`) enrichit ce snapshot pur pour produire
359
- `IRealtimeHealth` (`IRealtimeProbe.ts:121`) : `instanceId`, la santé **process** du worker
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:98`)
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:212`,
379
- via `createBrokerTicker()` (`providers.ts:465`)). Rien n'est dupliqué : le canal est un endpoint
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:44`),
386
- le serveur le résout et le **borne** avec `parseRate()` (`channelRate.ts:63`).
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:59`), les noms dans
396
- `CHANNELS` (`providers.ts:98`). Une cadence hors bornes est **ramenée dans l'intervalle**, jamais
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 | Cadence par défaut | Bornes |
400
- | ---------------------- | ------------------------------------------------------------------ | ------------------ | ------------- |
401
- | `nodefony:socket` | La sonde du hub, rejouée depuis l'endpoint admin `realtime` | 2 s | 500 ms – 60 s |
402
- | `nodefony:supervision` | `createStatsTicker()` (`providers.ts:222`) — CPU, mémoire, GC, ELU | 1 s | 250 ms – 60 s |
403
- | `nodefony:debugbar` | Le même producteur, canal séparé pour la barre de débogage | 1 s | 250 ms – 60 s |
404
- | `nodefony:orm:health` | Endpoint admin `orm/connection/health`, rejoué | 5 s | 1 s – 60 s |
405
- | `nodefony:orm:flow` | Endpoint admin `orm/flow` (débit de requêtes) | 2 s | 500 ms – 60 s |
406
- | `nodefony:syslog` | `createSyslogBridge()` (`providers.ts:145`) | **événementiel** | — (coalescé) |
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:58`, retrait en 11.0.0) — c'est ce qui le rend piégeux. S'abonner à un canal inconnu ne lève **aucune
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:121`). |
445
- | `cluster.probe.enabled` | `true` | Branche la sonde agrégée pod en worker de cluster (`config.ts:30`). |
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:351`), et
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:170`) ; le maître fusionne et
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:291`).
469
+ constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts#ClusterProbeClient.getClusterHealth`).
468
470
 
469
- La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:46`), est une fonction pure — et sa règle
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:178`) :
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:239`) — quand
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:437`), tout comme le report de la sonde cluster (`ClusterProbeClient.ts:170`) et le
508
- minuteur générique des canaux (`providers.ts:465`).
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);