@nodefony/realtime 10.0.0-beta.3 → 10.0.0-beta.5

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 (51) hide show
  1. package/README.md +29 -10
  2. package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +4 -6
  4. package/dist/nodefony/config/config.js +1 -2
  5. package/dist/nodefony/config/configSchema.js +1 -2
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  7. package/dist/nodefony/decorators/realtimeDecorators.js +1 -2
  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 +1 -2
  12. package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
  13. package/dist/nodefony/src/backplane/bootNotices.js +1 -2
  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 +0 -1
  17. package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -2
  18. package/dist/nodefony/src/guards.js +1 -2
  19. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -2
  20. package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
  21. package/dist/nodefony/src/server/RealtimeController.js +158 -84
  22. package/dist/nodefony/src/server/RealtimeHub.js +3 -3
  23. package/dist/nodefony/src/server/ServerRealtimeSocket.js +18 -9
  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 +1 -2
  27. package/dist/nodefony/src/server/welcomeEnv.js +1 -2
  28. package/dist/nodefony/src/service/RealtimeService.js +1 -2
  29. package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
  30. package/dist/nodefony/testing/index.js +77 -4
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
  33. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
  34. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  35. package/dist/types/nodefony/src/server/RealtimeController.d.ts +60 -2
  36. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
  37. package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
  38. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
  39. package/dist/types/nodefony/testing/index.d.ts +11 -4
  40. package/docs/actions.md +67 -44
  41. package/docs/architecture.md +125 -119
  42. package/docs/cadence-et-contre-pression.md +31 -22
  43. package/docs/configuration.md +106 -82
  44. package/docs/cookbook-chat.md +56 -34
  45. package/docs/index.md +35 -24
  46. package/docs/observabilite.md +49 -49
  47. package/docs/protocole.md +91 -68
  48. package/docs/securite.md +135 -115
  49. package/docs/vocabulaire.md +117 -114
  50. package/package.json +10 -10
  51. package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
@@ -226,18 +226,27 @@ Deux réglages, et le second est celui qu'on oublie :
226
226
  `atelier:load:2000` ouvert à n'importe qui.
227
227
 
228
228
  ```ts
229
+ // nodefony/config/http.ts — le fragment du module, importé par le manifeste
230
+ import type { IHttpConfigInput } from "@nodefony/http";
231
+
232
+ export const httpConfig = () =>
233
+ ({
234
+ websocket: { maxBackpressure: 1048576, backpressureCloseAfterDrops: 200 },
235
+ websocketSecure: {
236
+ maxBackpressure: 1048576,
237
+ backpressureCloseAfterDrops: 200,
238
+ },
239
+ }) satisfies IHttpConfigInput;
240
+ ```
241
+
242
+ ```ts ignore
229
243
  // nodefony.config.ts — extrait
230
244
  import { defineConfig, use } from "nodefony";
245
+ import { httpConfig } from "./nodefony/config/http";
231
246
 
232
247
  export default defineConfig(() => ({
233
248
  modules: [
234
- use("@nodefony/http", {
235
- websocket: { maxBackpressure: 1048576, backpressureCloseAfterDrops: 200 },
236
- websocketSecure: {
237
- maxBackpressure: 1048576,
238
- backpressureCloseAfterDrops: 200,
239
- },
240
- }),
249
+ use("@nodefony/http", httpConfig()),
241
250
  "@nodefony/framework",
242
251
  use("@nodefony/security", {
243
252
  // Préfixe : garde atelier:load ET atelier:load:2000, atelier:load:4000…
@@ -270,8 +279,8 @@ curl -sk https://127.0.0.1:5152/nodefony/realtime/api/health | jq '.backpressure
270
279
  ### La cadence vit dans le nom du canal
271
280
 
272
281
  Un canal d'état cadencé s'appelle `base` (cadence par défaut) ou `base:<ms>` (cadence explicite).
273
- Le client fabrique le nom avec `rateChannel()` (`channelRate.ts:44`), le serveur le relit et le
274
- **borne** avec `parseRate()` (`channelRate.ts:63`). Une même fonction pour les deux bords : la
282
+ Le client fabrique le nom avec `rateChannel()` (`channelRate.ts#rateChannel`), le serveur le relit et le
283
+ **borne** avec `parseRate()` (`channelRate.ts#parseRate`). Une même fonction pour les deux bords : la
275
284
  convention ne peut pas diverger.
276
285
 
277
286
  Conséquences voulues :
@@ -279,7 +288,7 @@ Conséquences voulues :
279
288
  - **1 canal = 1 cadence = 1 ticker.** Deux clients à 2 s partagent `base:2000` ; un troisième à 4 s
280
289
  ouvre `base:4000`. Le hub compte les abonnés et arrête le ticker au dernier.
281
290
  - **Le serveur garde la main.** Un client qui demande `base:10` obtient la borne basse : Studio
282
- borne ses statistiques à 250 ms minimum (`RATE_BOUNDS`, `StudioRealtimeController.ts:61`).
291
+ borne ses statistiques à 250 ms minimum (`RATE_BOUNDS`, `StudioRealtimeController.ts#RATE_BOUNDS`).
283
292
  - **Aucun message de contrôle** : changer de cadence, c'est changer d'abonnement. Le protocole
284
293
  reste celui des canaux, rien de plus.
285
294
 
@@ -292,7 +301,7 @@ Conséquences voulues :
292
301
 
293
302
  ### Ce que le client mesure
294
303
 
295
- À chaque trame reçue, `AdaptiveRate.noteFrame()` (`AdaptiveRate.ts:137`) compare l'écart depuis
304
+ À chaque trame reçue, `AdaptiveRate.noteFrame()` (`AdaptiveRate.ts#AdaptiveRate.noteFrame`) compare l'écart depuis
296
305
  la trame précédente à la cadence courante. Trois zones, et une seule décision par trame :
297
306
 
298
307
  ```mermaid
@@ -310,7 +319,7 @@ flowchart LR
310
319
  | **Bande morte** | entre les deux | ne change rien, et **remet à zéro** le compte des bons échantillons |
311
320
 
312
321
  Les seuils et la fenêtre sont des options (`starvationFactor`, `healthyFactor`, `recoveryWindow`),
313
- avec ces défauts (`AdaptiveRate.ts:100`).
322
+ avec ces défauts (`AdaptiveRate.ts#AdaptiveRate.constructor`).
314
323
 
315
324
  > [!TIP]
316
325
  > La **bande morte** est ce qui empêche l'oscillation : un réseau « moyen » ne fait ni monter ni
@@ -320,7 +329,7 @@ avec ces défauts (`AdaptiveRate.ts:100`).
320
329
  ### Ralentir vite, accélérer prudemment
321
330
 
322
331
  - **L'échelle** est géométrique : de la cadence désirée, doublée jusqu'à 60 s par défaut — `1 s,
323
- 2 s, 4 s, 8 s, …, 60 s` (`deriveLadder()`, `AdaptiveRate.ts:67`). Une échelle sur mesure se passe
332
+ 2 s, 4 s, 8 s, …, 60 s` (`deriveLadder()`, `AdaptiveRate.ts#deriveLadder`). Une échelle sur mesure se passe
324
333
  par l'option `ladder`.
325
334
  - **Ralentir** = monter d'un cran (×2) **dès la première famine**. Plusieurs famines successives
326
335
  doublent plusieurs fois : c'est ce qui rend la décroissance multiplicative.
@@ -329,15 +338,15 @@ avec ces défauts (`AdaptiveRate.ts:100`).
329
338
  - **Après chaque changement**, la mesure repart de zéro : la première trame du nouveau canal ne sert
330
339
  que de référence.
331
340
  - **Famine totale** : si plus aucune trame n'arrive, `noteFrame()` ne serait jamais appelé. Un chien
332
- de garde, réarmé à chaque cadence, appelle `checkStarvation()` (`AdaptiveRate.ts:174`) et ralentit
341
+ de garde, réarmé à chaque cadence, appelle `checkStarvation()` (`AdaptiveRate.ts#AdaptiveRate.checkStarvation`) et ralentit
333
342
  quand même.
334
343
 
335
344
  ### Changer de cadence sans trou
336
345
 
337
346
  **L'image** : on change de voie sur l'autoroute en s'insérant dans la nouvelle **avant** de quitter
338
- l'ancienne, jamais l'inverse. `bindAdaptiveChannel()` (`AdaptiveRate.ts:242`) fait pareil : il
347
+ l'ancienne, jamais l'inverse. `bindAdaptiveChannel()` (`AdaptiveRate.ts#bindAdaptiveChannel`) fait pareil : il
339
348
  **s'abonne au nouveau canal, puis quitte l'ancien**, dans le même geste (`applyDecision()`,
340
- `AdaptiveRate.ts:295`).
349
+ `AdaptiveRate.ts#bindAdaptiveChannel`).
341
350
 
342
351
  ```mermaid
343
352
  flowchart LR
@@ -394,12 +403,12 @@ rétabli. Il sonde prudemment, recule s'il s'est trompé, et ne le paie que de q
394
403
  Quand le serveur envoie une trame, la socket l'accepte et la place dans sa **file d'envoi** :
395
404
  `bufferedAmount` compte les octets qui attendent le réseau. Un client qui ne lit plus fait grossir
396
405
  cette file sans fin. Nodefony la plafonne à **`maxBackpressure`**, 4 Mio par défaut
397
- (`@nodefony/http/nodefony/config/config.ts:651`).
406
+ (`@nodefony/http/nodefony/config/config.ts#websocketSchema`).
398
407
 
399
408
  La décision est prise **trame par trame**, par une seule fonction : `decideSend()`
400
- (`wsBackpressure.ts:89`). Elle sert à la fois le `send()`/`broadcast()` d'un contexte WebSocket de
409
+ (`wsBackpressure.ts#decideSend`). Elle sert à la fois le `send()`/`broadcast()` d'un contexte WebSocket de
401
410
  `@nodefony/http` et chaque connexion temps réel (`WsConnectionTransport.send()`,
402
- `WsConnectionTransport.ts:82`). Une seule règle, deux usages.
411
+ `WsConnectionTransport.ts#WsConnectionTransport.send`). Une seule règle, deux usages.
403
412
 
404
413
  ```mermaid
405
414
  flowchart LR
@@ -488,8 +497,8 @@ Le client **n'apprend jamais** les seuils : il ne reçoit que leur effet.
488
497
  - Une trame jetée **n'existe pas** pour lui : il voit seulement un écart plus long entre deux
489
498
  trames.
490
499
  - Une fermeture arrive avec le code `1013`. Il le classe **transitoire** : ce n'est pas un des codes
491
- définitifs (`FATAL_CLOSE_CODES`, `notice.ts:176`), donc `isReconnectableCloseCode()`
492
- (`notice.ts:192`) répond oui, et il se reconnecte avec une attente croissante.
500
+ définitifs (`FATAL_CLOSE_CODES`, `notice.ts#FATAL_CLOSE_CODES`), donc `isReconnectableCloseCode()`
501
+ (`notice.ts#isReconnectableCloseCode`) répond oui, et il se reconnecte avec une attente croissante.
493
502
 
494
503
  ## 🔌 Comment les deux étages se rencontrent
495
504
 
@@ -514,7 +523,7 @@ sans activer de trace.
514
523
 
515
524
  ### Ce que la sonde du hub expose
516
525
 
517
- `RealtimeHub.probe()` (`RealtimeHub.ts:847`) parcourt les connexions et rend un bloc
526
+ `RealtimeHub.probe()` (`RealtimeHub.ts#RealtimeHub.probe`) parcourt les connexions et rend un bloc
518
527
  `backpressure` :
519
528
 
520
529
  | Champ | Ce qu'il dit | Lecture |
@@ -125,19 +125,19 @@ fichiers, jamais trois**.
125
125
  | `nodefony/config/config.ts` | **le quoi** | le schéma Zod commenté, source **unique** des valeurs d'usine, et les défauts matérialisés |
126
126
  | `nodefony/config/defineModuleConfig.ts` | **le comment** | le builder pur : analyse, surcharge d'environnement, gel — il ne retape jamais une valeur d'usine |
127
127
 
128
- Le schéma `realtimeConfigSchema` (`config.ts:259`) porte chaque valeur en `.default()` et chaque
128
+ Le schéma `realtimeConfigSchema` (`config.ts#realtimeConfigSchema`) porte chaque valeur en `.default()` et chaque
129
129
  explication en `.describe()`. Les défauts effectifs sont **dérivés** du schéma lui-même
130
- (`realtimeConfigSchema.parse({})`, `config.ts:231`) : il n'existe pas de second endroit où une valeur
130
+ (`realtimeConfigSchema.parse({})`, `config.ts#clientLogsSchema`) : il n'existe pas de second endroit où une valeur
131
131
  d'usine serait écrite, donc pas de dérive silencieuse possible entre la doc du champ et son
132
132
  comportement.
133
133
 
134
- Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts:39`) reste **pur** : il analyse l'entrée
134
+ Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts#defineRealtimeConfig`) reste **pur** : il analyse l'entrée
135
135
  et gèle le résultat. Il ne lit aucune variable d'environnement. Les trois variables du module
136
136
  (`NF_REALTIME_DRIVER`, `NF_REALTIME_BACKPLANE_NAMESPACE`, `NF_REALTIME_BACKPLANE_SECRET`) sont
137
- déclarées sur leur champ dans le schéma (`.meta({ env })`, `config.ts:59`, `:76`, `:93`) et posées
137
+ déclarées sur leur champ dans le schéma (`.meta({ env })`, `config.ts#backplaneSchema`) et posées
138
138
  par le Kernel **avant** l'analyse : le schéma les valide comme une valeur écrite dans le fichier.
139
139
 
140
- Le module **augmente le registre de types** (`index.ts:167`) : `use("@nodefony/realtime", { … })`
140
+ Le module **augmente le registre de types** (`index.ts#tick`) : `use("@nodefony/realtime", { … })`
141
141
  propose ses clés en autocomplétion et **refuse** une clé inconnue à la compilation.
142
142
 
143
143
  > [!IMPORTANT]
@@ -157,31 +157,42 @@ On garde le fond de panier local, on ferme la porte d'entrée, on abaisse le pla
157
157
  dont l'application a réellement besoin.
158
158
 
159
159
  ```ts
160
+ // nodefony/config/realtime.ts — le fragment du module, importé par le manifeste
161
+ import type { IRealtimeConfigInput } from "@nodefony/realtime";
162
+
163
+ export const realtimeConfig = () =>
164
+ ({
165
+ // Un seul processus : le hub diffuse localement, aucun transport n'est ouvert.
166
+ backplane: { driver: "loopback" },
167
+
168
+ // Défense CSWSH (RFC 6455 §10.2). Sans elle, une page tierce ouvre une socket
169
+ // avec les cookies de ton utilisateur : le navigateur n'applique pas CORS ici.
170
+ csrf: {
171
+ checkOrigin: {
172
+ enabled: true,
173
+ allowList: ["https://127.0.0.1:5152"],
174
+ // Un client non-navigateur n'envoie pas d'en-tête Origin. Le laisser à
175
+ // false n'a de sens que si tout ton trafic vient d'un navigateur.
176
+ allowMissingOrigin: false,
177
+ },
178
+ },
179
+
180
+ // Garde anti-saturation : le défaut de 256 est généreux. Un écran qui suit
181
+ // dix canaux n'a aucune raison d'en ouvrir plus de quelques dizaines.
182
+ limits: { maxChannelsPerConnection: 32 },
183
+ }) satisfies IRealtimeConfigInput;
184
+ ```
185
+
186
+ ```ts ignore
160
187
  // nodefony.config.ts — le manifeste de l'application
188
+ import { defineConfig, use } from "nodefony";
189
+ import { realtimeConfig } from "./nodefony/config/realtime";
190
+
161
191
  export default defineConfig(() => ({
162
192
  modules: [
163
193
  "@nodefony/http",
164
194
  "@nodefony/framework",
165
- use("@nodefony/realtime", {
166
- // Un seul processus : le hub diffuse localement, aucun transport n'est ouvert.
167
- backplane: { driver: "loopback" },
168
-
169
- // Défense CSWSH (RFC 6455 §10.2). Sans elle, une page tierce ouvre une socket
170
- // avec les cookies de ton utilisateur : le navigateur n'applique pas CORS ici.
171
- csrf: {
172
- checkOrigin: {
173
- enabled: true,
174
- allowList: ["https://127.0.0.1:5152"],
175
- // Un client non-navigateur n'envoie pas d'en-tête Origin. Le laisser à
176
- // false n'a de sens que si tout ton trafic vient d'un navigateur.
177
- allowMissingOrigin: false,
178
- },
179
- },
180
-
181
- // Garde anti-saturation : le défaut de 256 est généreux. Un écran qui suit
182
- // dix canaux n'a aucune raison d'en ouvrir plus de quelques dizaines.
183
- limits: { maxChannelsPerConnection: 32 },
184
- }),
195
+ use("@nodefony/realtime", realtimeConfig()),
185
196
  ],
186
197
  }));
187
198
  ```
@@ -191,8 +202,39 @@ export default defineConfig(() => ({
191
202
  Passer à N pods sur N machines ne change **ni le contrôleur, ni le client, ni un seul canal** : on
192
203
  remplace le nom du driver, et on déclare le module qui fournit les connexions pub/sub.
193
204
 
194
- ```ts
205
+ ```ts ignore
206
+ // nodefony/config/realtime.ts — le fragment du module, importé par le manifeste
207
+ import type { ConfigContext } from "nodefony";
208
+ import type { IRealtimeConfigInput } from "@nodefony/realtime";
209
+ import type { env } from "../../env";
210
+
211
+ export const realtimeConfig = (ctx: ConfigContext<typeof env>) =>
212
+ ({
213
+ backplane: {
214
+ // En production seulement : en développement, le fond de panier local évite
215
+ // d'exiger une infrastructure pour lancer l'application.
216
+ driver: ctx.isProd ? "redis" : "loopback",
217
+
218
+ // La cloison. Le numéro de base Redis ne cloisonne PAS le pub/sub : sans ce
219
+ // nom, préproduction et production sur un même Redis échangeraient leurs
220
+ // diffusions. À poser explicitement, et différemment par déploiement.
221
+ namespace: ctx.isProd ? "boutique.prod" : "boutique.dev",
222
+ },
223
+ csrf: {
224
+ checkOrigin: {
225
+ enabled: true,
226
+ allowList: ["https://boutique.example.com"],
227
+ },
228
+ },
229
+ limits: { maxChannelsPerConnection: 32 },
230
+ }) satisfies IRealtimeConfigInput;
231
+ ```
232
+
233
+ ```ts ignore
195
234
  // nodefony.config.ts — la même application, déployée en plusieurs répliques
235
+ import { defineConfig, use } from "nodefony";
236
+ import { realtimeConfig } from "./nodefony/config/realtime";
237
+
196
238
  export default defineConfig((ctx) => ({
197
239
  modules: [
198
240
  "@nodefony/http",
@@ -202,25 +244,7 @@ export default defineConfig((ctx) => ({
202
244
  // module. Sans lui déclaré, le hub reste local — et le dit dans les journaux.
203
245
  "@nodefony/redis",
204
246
 
205
- use("@nodefony/realtime", {
206
- backplane: {
207
- // En production seulement : en développement, le fond de panier local évite
208
- // d'exiger une infrastructure pour lancer l'application.
209
- driver: ctx.isProd ? "redis" : "loopback",
210
-
211
- // La cloison. Le numéro de base Redis ne cloisonne PAS le pub/sub : sans ce
212
- // nom, préproduction et production sur un même Redis échangeraient leurs
213
- // diffusions. À poser explicitement, et différemment par déploiement.
214
- namespace: ctx.isProd ? "boutique.prod" : "boutique.dev",
215
- },
216
- csrf: {
217
- checkOrigin: {
218
- enabled: true,
219
- allowList: ["https://boutique.example.com"],
220
- },
221
- },
222
- limits: { maxChannelsPerConnection: 32 },
223
- }),
247
+ use("@nodefony/realtime", realtimeConfig(ctx)),
224
248
  ],
225
249
  }));
226
250
  ```
@@ -252,7 +276,7 @@ regarder quand un message ne traverse pas.
252
276
 
253
277
  > [!TIP]
254
278
  > Ces quatre lignes viennent du même endroit du code (`Realtime.#wireBackplane()`,
255
- > `src/packages/@nodefony/realtime/index.ts:292`) et de la carte d'identité que chaque driver publie.
279
+ > `src/packages/@nodefony/realtime/index.ts#Realtime.#wireBackplane`) et de la carte d'identité que chaque driver publie.
256
280
  > Aucun repli n'est silencieux : si le fan-out est dégradé, c'est écrit.
257
281
 
258
282
  ## ⚙️ Le schéma, clé par clé
@@ -289,8 +313,8 @@ contre-pression, eux, se règlent sur le serveur WebSocket de `@nodefony/http`
289
313
 
290
314
  Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
291
315
  enregistrée, pas de backplane câblé, pas de sonde de pod
292
- (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:246`), et le service devient
293
- inerte (`RealtimeService.init()`, `RealtimeService.ts:77`).
316
+ (`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts#Realtime.onKernelBoot`), et le service devient
317
+ inerte (`RealtimeService.init()`, `RealtimeService.ts#RealtimeService.init`).
294
318
 
295
319
  > [!WARNING]
296
320
  > **`enabled: false` désarme aussi tes gardes.** Le hub garde son plafond interne de 256 canaux,
@@ -305,7 +329,7 @@ désactiver vraiment, retire le module du manifeste.
305
329
 
306
330
  C'est **le** réglage du module. Le champ est une chaîne libre et non une énumération, volontairement :
307
331
  la liste réelle est celle du **registre** de drivers (`listBackplaneDrivers()`,
308
- `backplaneRegistry.ts:68`), ouverte aux drivers que tu écris toi-même. Une énumération figée dans le
332
+ `backplaneRegistry.ts#listBackplaneDrivers`), ouverte aux drivers que tu écris toi-même. Une énumération figée dans le
309
333
  schéma fermerait cette porte.
310
334
 
311
335
  Trois noms sont enregistrés par le module au chargement :
@@ -327,10 +351,10 @@ de frappe dans une variable de déploiement ne doit pas empêcher l'application
327
351
  ### `backplane.namespace` — la cloison sur un bus partagé
328
352
 
329
353
  Ne concerne que les drivers qui traversent les machines. Le canal de transport devient
330
- `nodefony:realtime:<namespace>` (`resolveRedisChannel()`, `RedisBackplane.ts:31`).
354
+ `nodefony:realtime:<namespace>` (`resolveRedisChannel()`, `RedisBackplane.ts#resolveRedisChannel`).
331
355
 
332
356
  Le défaut est **dérivé du nom de l'application**
333
- (`src/packages/@nodefony/realtime/index.ts:139`) — ce qui suffit à séparer deux applications
357
+ (`src/packages/@nodefony/realtime/index.ts`) — ce qui suffit à séparer deux applications
334
358
  distinctes sur un Redis mutualisé, et **ne suffit pas** à séparer deux déploiements de la **même**
335
359
  application. Préproduction et production portent le même nom d'application, donc le même canal
336
360
  dérivé, donc le même flux.
@@ -342,7 +366,7 @@ dérivé, donc le même flux.
342
366
  > utilisateurs. Pose-la dès que deux environnements partagent un Redis.
343
367
 
344
368
  Caractères acceptés : lettres, chiffres, `_`, `.`, `-` — le motif est validé par le schéma
345
- (`backplaneSchema`, `config.ts:45`), donc un nom fautif arrête le démarrage plutôt que de produire un
369
+ (`backplaneSchema`, `config.ts#backplaneSchema`), donc un nom fautif arrête le démarrage plutôt que de produire un
346
370
  canal exotique.
347
371
 
348
372
  ### `backplane.secret` — sceller ce qui circule sur le bus
@@ -350,7 +374,7 @@ canal exotique.
350
374
  Ne concerne, là aussi, que les transports **partagés**. Un bus pub/sub ne dit pas _qui_ a publié :
351
375
  sans secret, tout ce qui atterrit sur le canal — venu d'un pod ou d'un tiers qui sait écrire dans ce
352
376
  Redis — est traité comme un message légitime. Poser un secret fait porter à chaque message un sceau
353
- HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts:112`) : un message non
377
+ HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts#openBackplaneEnvelope`) : un message non
354
378
  scellé, altéré ou repointé vers un autre canal est **ignoré**, sans exception possible.
355
379
 
356
380
  Le secret doit être **identique sur tous les pods** — c'est lui qui les reconnaît entre eux. En
@@ -370,7 +394,7 @@ sonde de santé — il reste à zéro en régime normal.
370
394
  Sans effet hors d'un worker lancé par `nodefony cluster`. Dans ce contexte, la sonde remonte
371
395
  périodiquement la santé de **son** processus au maître, qui la consolide, et redistribue
372
396
  l'instantané : n'importe quel worker peut alors servir la vue « pod entier » sans interroger les
373
- autres (`ClusterProbeClient`, `ClusterProbeClient.ts:138`).
397
+ autres (`ClusterProbeClient`, `ClusterProbeClient.ts#ClusterProbeClient`).
374
398
 
375
399
  Le coût est faible mais réel : un minuteur (déréférencé, il n'empêche pas le processus de sortir) et
376
400
  un écouteur IPC par worker. Poser `false` produit un **contournement complet** — le client n'est
@@ -378,13 +402,13 @@ jamais instancié, donc zéro minuteur, zéro message IPC — et l'endpoint de s
378
402
  de l'instance courante.
379
403
 
380
404
  Deux leviers coupent la sonde, et **l'un ou l'autre suffit** : cette clé, ou la variable
381
- `NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts:404`). C'est ce qui permet
405
+ `NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts#Realtime.#wireClusterProbe`). C'est ce qui permet
382
406
  d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
383
407
 
384
408
  ### `slowConsumer.bytes` — un compteur, pas un frein
385
409
 
386
410
  Seuil de `bufferedAmount` — les octets en attente d'envoi sur une socket — au-delà duquel la sonde
387
- compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:847`).
411
+ compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts#RealtimeHub.probe`).
388
412
 
389
413
  > [!IMPORTANT]
390
414
  > **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
@@ -440,7 +464,7 @@ des canaux d'état, où seul le dernier instantané compte.
440
464
  Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
441
465
  côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
442
466
  processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
443
- `RealtimeController.ts:869`).
467
+ `RealtimeController.ts#RealtimeController.startChannel`).
444
468
 
445
469
  Quatre propriétés qui décident du bon réglage :
446
470
 
@@ -451,7 +475,7 @@ Quatre propriétés qui décident du bon réglage :
451
475
  - Un réabonnement à un canal **déjà tenu** ne consomme pas de place — l'idempotence est vérifiée
452
476
  avant le plafond. Un client qui redemande son canal n'est pas puni.
453
477
  - La garde existe **même sans configuration** : le hub porte le même défaut de 256
454
- (`RealtimeHub.ts:218`), il n'y a pas de fenêtre où elle serait absente.
478
+ (`RealtimeHub.ts#RealtimeHub`), il n'y a pas de fenêtre où elle serait absente.
455
479
 
456
480
  `null` retire la borne. C'est un retrait explicite, à réserver aux déploiements dont tu maîtrises
457
481
  les clients — un tableau de bord interne qui compose des dizaines de flux, par exemple.
@@ -463,13 +487,13 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
463
487
  ### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
464
488
 
465
489
  Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
466
- (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:967`). Une origine refusée ferme la socket avec le code
490
+ (`RealtimeHub.checkOrigin()`, `RealtimeHub.ts#RealtimeHub.checkOrigin`). Une origine refusée ferme la socket avec le code
467
491
  `4003`.
468
492
 
469
493
  Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
470
494
  active-le dès que ta socket est joignable depuis un navigateur.
471
495
 
472
- Le comportement se lit en trois lignes (`buildOriginGuard()`, `RealtimeService.ts:313`) :
496
+ Le comportement se lit en trois lignes (`buildOriginGuard()`, `RealtimeService.ts#buildOriginGuard`) :
473
497
 
474
498
  | Situation | Résultat |
475
499
  | ------------------------------------------------ | ------------------------------------------------------------------------ |
@@ -528,7 +552,7 @@ use("@nodefony/realtime", { backplane: { driver: "cluster" } });
528
552
  **Ce qui se passe.** Un worker Node ne peut parler qu'au maître. Il lui envoie ses publications, et le
529
553
  maître les redistribue aux autres workers. Le driver ne s'active qu'en **rôle worker** et avec la
530
554
  variable `NF_CLUSTER=1`, posée par la commande de cluster
531
- (`src/packages/@nodefony/realtime/index.ts:106`) ; ailleurs, il rend `null` et le hub reste local.
555
+ (`src/packages/@nodefony/realtime/index.ts`) ; ailleurs, il rend `null` et le hub reste local.
532
556
 
533
557
  **Pourquoi c'est précieux.** C'est le banc d'essai qui stabilise l'architecture multi-processus avant
534
558
  d'ajouter du réseau. Ton contrôleur, tes canaux diffusables et ton client voient un vrai fan-out
@@ -553,7 +577,7 @@ use("@nodefony/realtime", {
553
577
 
554
578
  **Comment il se branche.** Le driver ne dépend pas de la bibliothèque `redis` : il consomme deux
555
579
  connexions du module `@nodefony/redis` par un adaptateur purement structurel
556
- (`createRedisServiceTransport()`, `RedisBackplane.ts:137`). Deux et non une, parce qu'un client Redis
580
+ (`createRedisServiceTransport()`, `RedisBackplane.ts#createRedisServiceTransport`). Deux et non une, parce qu'un client Redis
557
581
  abonné ne peut plus émettre de commandes ordinaires — le module fournit précisément des connexions
558
582
  nommées `publish` et `subscribe` dans ses défauts.
559
583
 
@@ -563,7 +587,7 @@ coupure. C'est acceptable pour un salon de discussion — le message est en base
563
587
  un bus d'événements critiques. Ne construis pas au-dessus une fiabilité que le support n'offre pas.
564
588
 
565
589
  **Si Redis est injoignable.** Le démarrage du backplane est attendu **explicitement**, et **borné à
566
- cinq secondes** (`src/packages/@nodefony/realtime/index.ts:90`). Au-delà, ou en cas d'échec, le
590
+ cinq secondes** (`src/packages/@nodefony/realtime/index.ts#BACKPLANE_START_TIMEOUT_MS`). Au-delà, ou en cas d'échec, le
567
591
  module renonce, ferme proprement ce qu'il avait ouvert, et laisse le hub local — le démarrage
568
592
  continue. Une base de messages en panne ne doit pas empêcher tes serveurs HTTP de monter.
569
593
 
@@ -637,21 +661,21 @@ Quatre points que la fabrique doit respecter :
637
661
  délai.
638
662
  2. Elle peut rendre **`null`** pour dire « inactif dans ce contexte » — mauvais rôle, infrastructure
639
663
  absente. Le hub reste alors local, sans erreur.
640
- 3. Elle reçoit un contexte complet (`IBackplaneFactoryContext`, `backplaneRegistry.ts:27`) : le
664
+ 3. Elle reçoit un contexte complet (`IBackplaneFactoryContext`, `backplaneRegistry.ts#IBackplaneFactoryContext`) : le
641
665
  module, l'`originId` déjà résolu, le rôle dans la topologie et la configuration validée.
642
666
  4. `describe()` alimente **trois** sorties depuis une source unique : la ligne de journal, la sonde
643
667
  de santé et l'écran Studio. Remplis-la honnêtement, en particulier `crossPod`.
644
668
 
645
669
  **L'autre voie : fournir une instance déjà construite.** Déclare un service nommé
646
670
  `realtimeBackplane` dans le conteneur d'injection de ton module. `RealtimeService.init()`
647
- (`RealtimeService.ts:77`) le lit et le branche **avant** que le registre soit consulté : une instance
671
+ (`RealtimeService.ts#RealtimeService.init`) le lit et le branche **avant** que le registre soit consulté : une instance
648
672
  présente court-circuite la sélection par nom.
649
673
 
650
674
  > [!WARNING]
651
675
  > Le builder accepte aussi une instance en second argument (`defineRealtimeConfig(config, {
652
676
  backplane })`), et la documentation d'architecture présente cette voie. En pratique elle n'est pas
653
677
  > atteignable depuis `nodefony.config.ts` : le module appelle le builder avec la seule configuration
654
- > fusionnée (`src/packages/@nodefony/realtime/index.ts:230`), et le schéma Zod est strict : une clé
678
+ > fusionnée (`src/packages/@nodefony/realtime/index.ts#Realtime.onKernelRegister`), et le schéma Zod est strict : une clé
655
679
  > inconnue, dont une instance de classe posée dans `backplane`, fait échouer le boot. Pour brancher un objet déjà construit, utilise
656
680
  > le service `realtimeBackplane`.
657
681
 
@@ -680,7 +704,7 @@ L'ordre de recouvrement, du plus faible au plus fort :
680
704
  2. **La configuration de l'application** — ton `use("@nodefony/realtime", { … })`.
681
705
  3. **`NF_REALTIME_DRIVER`**, **`NF_REALTIME_BACKPLANE_NAMESPACE`** et
682
706
  **`NF_REALTIME_BACKPLANE_SECRET`** — posées par le Kernel après la fusion et **avant** la
683
- validation Zod (`applyEnvShortcuts`, `Kernel.ts:1989`) : une valeur fausse **arrête le démarrage** en nommant la
707
+ validation Zod (`applyEnvShortcuts`, `Kernel.ts#Kernel.applyEnvConfigOverrides`) : une valeur fausse **arrête le démarrage** en nommant la
684
708
  variable (un secret de moins de 32 caractères, un espace de noms qui contient `:`). Chacune
685
709
  accepte aussi sa forme `<NOM>_FILE` (secret monté par Docker ou Kubernetes).
686
710
  4. **`NF__REALTIME__<CHEMIN>`** — même moment, même validation. Un chemin introuvable produit un
@@ -712,10 +736,10 @@ NF_CLUSTER_PROBE=0 # couper la sonde de pod sur une réplique en incident
712
736
 
713
737
  Deux fonctions publiques, utiles surtout aux tests et aux outils.
714
738
 
715
- `defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:39`) analyse et **gèle**. Appelé
739
+ `defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts#defineRealtimeConfig`) analyse et **gèle**. Appelé
716
740
  hors Kernel (un test, un outil), il ne voit pas les variables d'environnement : c'est le Kernel
717
741
  qui les pose. Le module l'appelle lui-même à l'enregistrement
718
- (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:221`) : tu n'as pas à
742
+ (`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts#Realtime.onKernelRegister`) : tu n'as pas à
719
743
  l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
720
744
  sans démarrer un serveur.
721
745
 
@@ -727,15 +751,15 @@ const cfg = defineRealtimeConfig({ backplane: { driver: "redis" } });
727
751
  // Une valeur invalide ou une clé inconnue lève une BootConfigurationError, avec le chemin exact du champ fautif.
728
752
  ```
729
753
 
730
- `realtimeConfigJsonSchema()` (`defineModuleConfig.ts:99`) produit le schéma JSON du module, chaque
754
+ `realtimeConfigJsonSchema()` (`defineModuleConfig.ts#realtimeConfigJsonSchema`) produit le schéma JSON du module, chaque
731
755
  champ portant sa description. C'est ce que consomme la page module de Studio pour afficher la
732
756
  configuration attendue. L'instance de backplane éventuelle en est absente : une classe n'a rien à
733
757
  faire dans un schéma sérialisable.
734
758
 
735
759
  Un message d'erreur de validation est reformaté par le cœur avant d'être levé (`parseModuleConfig`,
736
760
  appelé par `defineRealtimeConfig`) : chaque problème apparaît sous la forme `chemin : message`
737
- (`src/nodefony/src/kernel/moduleConfig.ts:42`), séparés par des points médians
738
- (`src/nodefony/src/kernel/moduleConfig.ts:58`). Tu lis quel champ est fautif, pas une trace Zod
761
+ (`src/nodefony/src/kernel/moduleConfig.ts#formatConfigIssue`), séparés par des points médians
762
+ (`src/nodefony/src/kernel/moduleConfig.ts#formatConfigIssues`). Tu lis quel champ est fautif, pas une trace Zod
739
763
  brute.
740
764
 
741
765
  ## 📡 Observabilité — relire ce qui s'applique vraiment
@@ -758,16 +782,16 @@ Trois endroits, par ordre de fiabilité décroissante.
758
782
 
759
783
  ## ⚠️ Pièges
760
784
 
761
- | Symptôme | Cause | Correction |
762
- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
763
- | Un message n'arrive qu'à la moitié des clients, sans aucune erreur | plusieurs répliques avec `driver: "loopback"` | passer à `redis` et déclarer `@nodefony/redis` dans le manifeste |
764
- | Driver `redis` configuré, journal `RealtimeHub reste local` | module `@nodefony/redis` absent du manifeste, ou connexions `publish`/`subscribe` indisponibles | déclarer le module ; vérifier que Redis répond |
765
- | Le fan-out traverse, mais des messages d'un autre environnement arrivent | `backplane.namespace` non posé : deux déploiements de la même application dérivent le même canal | poser un namespace explicite et distinct par environnement |
766
- | Le démarrage échoue avec `clé inconnue : …` | clé absente du schéma strict (faute de frappe, ou clé retirée) | corriger ou retirer la clé que nomme le message |
767
- | Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde | régler `websocketSecure.maxBackpressure` / `.backpressureCloseAfterDrops` (@nodefony/http) |
768
- | Toutes les connexions tombent en `4003` après activation du contrôle d'origine | `allowList` vide, ou origine non identique au caractère près (port, schéma, sous-domaine) | inscrire l'origine exacte ; un client non-navigateur relève de `allowMissingOrigin` |
769
- | `enabled: false` posé « pour désactiver », et la garde d'origine ne s'applique plus | le service devient inerte et ne pose plus les politiques sur le hub | retirer le module du manifeste plutôt que l'éteindre |
770
- | Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts:218`) | vérifier le motif de refus `realtime:denied` côté client |
785
+ | Symptôme | Cause | Correction |
786
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
787
+ | Un message n'arrive qu'à la moitié des clients, sans aucune erreur | plusieurs répliques avec `driver: "loopback"` | passer à `redis` et déclarer `@nodefony/redis` dans le manifeste |
788
+ | Driver `redis` configuré, journal `RealtimeHub reste local` | module `@nodefony/redis` absent du manifeste, ou connexions `publish`/`subscribe` indisponibles | déclarer le module ; vérifier que Redis répond |
789
+ | Le fan-out traverse, mais des messages d'un autre environnement arrivent | `backplane.namespace` non posé : deux déploiements de la même application dérivent le même canal | poser un namespace explicite et distinct par environnement |
790
+ | Le démarrage échoue avec `clé inconnue : …` | clé absente du schéma strict (faute de frappe, ou clé retirée) | corriger ou retirer la clé que nomme le message |
791
+ | Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde | régler `websocketSecure.maxBackpressure` / `.backpressureCloseAfterDrops` (@nodefony/http) |
792
+ | Toutes les connexions tombent en `4003` après activation du contrôle d'origine | `allowList` vide, ou origine non identique au caractère près (port, schéma, sous-domaine) | inscrire l'origine exacte ; un client non-navigateur relève de `allowMissingOrigin` |
793
+ | `enabled: false` posé « pour désactiver », et la garde d'origine ne s'applique plus | le service devient inerte et ne pose plus les politiques sur le hub | retirer le module du manifeste plutôt que l'éteindre |
794
+ | Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts#RealtimeHub`) | vérifier le motif de refus `realtime:denied` côté client |
771
795
 
772
796
  ## 🧪 Tests & couverture
773
797