@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
@@ -35,6 +35,25 @@ coverageModule: realtime
35
35
 
36
36
  📍 [Documentation](../../../../../docs/index.md) › [@nodefony/realtime](index.md) › **Cookbook — un chat**
37
37
 
38
+ ## ⚡ Le geste — deux commandes avant d'écrire une ligne
39
+
40
+ Une ressource REST et un canal temps réel ne s'écrivent pas à la main : les générateurs de
41
+ l'application les posent, câblés et testés. Depuis la racine de l'application :
42
+
43
+ ```bash
44
+ npx nodefony create entity Message body:text room:string:index --route /api/messages
45
+ npx nodefony create controller room --kind realtime
46
+ ```
47
+
48
+ | Commande | Ce qu'elle pose |
49
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | `create entity` | la table (`nodefony/entity/Message.ts`), ses schémas de validation, un service CRUD, `MessageController` — REST sur `/api/messages`, et ses lectures répondent aussi par la socket —, un test unitaire et un test e2e, le tout inscrit dans `index.ts` — puis, si l'application versionne ses migrations (toute application complète), la migration de la table, écrite et appliquée à la base de développement ; sinon la table naît au prochain démarrage |
51
+ | `create controller --kind realtime` | `RoomController` : la socket `/api/room/realtime`, le canal `room:events`, ses actions, et un test qui la pilote sans serveur ni navigateur |
52
+
53
+ La suite de ce cookbook construit le même salon À LA MAIN, dans un seul contrôleur : c'est ce qui
54
+ permet de comprendre ce que les générateurs écrivent, et de le retoucher. Pour avancer, partir des
55
+ générateurs ; pour comprendre, lire la suite.
56
+
38
57
  ## 🎯 Ce qu'on construit
39
58
 
40
59
  **En une phrase** : un salon de discussion où plusieurs personnes échangent des messages et des
@@ -134,7 +153,7 @@ le moment venu.
134
153
 
135
154
  ```bash
136
155
  # 1. Créer l'application (profil complet : http, framework, realtime, sécurité, frontend)
137
- npx nodefony create app mon-chat --preset complete
156
+ npm create nodefony@beta mon-chat -- --preset complete
138
157
  cd mon-chat
139
158
 
140
159
  # 2. Créer le module qui portera le salon
@@ -781,20 +800,23 @@ couvre le HTTP **et** le WebSocket qu'elle recouvre — c'est le défaut, et c'e
781
800
  zone protégée qui laisserait passer le WebSocket serait une porte dérobée.
782
801
 
783
802
  ```ts ignore
784
- // nodefony.config.ts (ajout de l'étape 5)
785
- use("@nodefony/security", {
786
- areas: {
787
- chat: {
788
- pattern: "^/chat",
789
- // Cookie de session (BFF) : le navigateur n'a aucun jeton à manipuler,
790
- // et le cookie voyage naturellement avec la poignée de main WebSocket.
791
- authenticators: ["session"],
803
+ // nodefony/config/security.ts — (ajout de l'étape 5)
804
+ import type { ISecurityConfigInput } from "@nodefony/security";
805
+
806
+ export const securityConfig = () =>
807
+ ({
808
+ areas: {
809
+ chat: {
810
+ pattern: "^/chat",
811
+ // Cookie de session (BFF) : le navigateur n'a aucun jeton à manipuler,
812
+ // et le cookie voyage naturellement avec la poignée de main WebSocket.
813
+ authenticators: ["session"],
814
+ },
792
815
  },
793
- },
794
- roleHierarchy: {
795
- ROLE_ADMIN: ["ROLE_USER"],
796
- },
797
- });
816
+ roleHierarchy: {
817
+ ROLE_ADMIN: ["ROLE_USER"],
818
+ },
819
+ }) satisfies ISecurityConfigInput;
798
820
  ```
799
821
 
800
822
  > [!NOTE]
@@ -1603,23 +1625,23 @@ Pour qui veut lire l'implémentation derrière chaque étape :
1603
1625
  <!-- prettier-ignore -->
1604
1626
  | Ce que tu as utilisé | Où c'est écrit |
1605
1627
  | --- | --- |
1606
- | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:191`), `RealtimeChannel()` (`realtimeDecorators.ts:240`), `RealtimeInbound()` (`realtimeDecorators.ts:278`) |
1607
- | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:344`) |
1608
- | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:321`) |
1609
- | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:464`), `RealtimeController.startChannel()` (`RealtimeController.ts:869`) |
1610
- | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:610`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:629`) |
1611
- | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:420`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:674`) |
1612
- | Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:720`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
1613
- | Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1124`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1098`) |
1614
- | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:306`) |
1615
- | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:449`), fermetures `origin not allowed` (`RealtimeController.ts:422`) et `unauthorized` (`RealtimeController.ts:444`) |
1616
- | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:815`) |
1617
- | Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:66`), fermeture `slow consumer` (`RealtimeHub.ts:64`) |
1618
- | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:981`) |
1619
- | L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
1620
- | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:847`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
1621
- | Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
1622
- | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:218`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
1623
- | Les hooks React | `useNodefony()` (`client/react/index.ts:255`), `useNodefonyState()` (`client/react/index.ts:273`), `useNodefonyIdentity()` (`client/react/index.ts:292`), `useNodefonyChannel()` (`client/react/index.ts:308`), `useNodefonyChannelData()` (`client/react/index.ts:332`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts:367`) |
1624
- | Le client navigateur | `NodefonySocket.connect()` (`NodefonySocket.ts:592`), `NodefonySocket.subscribe()` (`NodefonySocket.ts:739`), `NodefonySocket.request()` (`NodefonySocket.ts:936`), `NodefonySocket.onDenied()` (`NodefonySocket.ts:678`) |
1625
- | L'upload | `UploadedFile` (`routerDecorators.ts:1311`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:215`) |
1628
+ | Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts#RealtimeAction`), `RealtimeChannel()` (`realtimeDecorators.ts#RealtimeChannel`), `RealtimeInbound()` (`realtimeDecorators.ts#RealtimeInbound`) |
1629
+ | La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts#RealtimeController.handleRealtime`) |
1630
+ | Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts#RealtimeController.realtimeApiRequest`) |
1631
+ | L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts#RealtimeHub.subscribe`), `RealtimeController.startChannel()` (`RealtimeController.ts#RealtimeController.startChannel`) |
1632
+ | La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts#RealtimeHub.publish`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts#RealtimeHub.publishLocal`) |
1633
+ | Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts#RealtimeBroadcast`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts#RealtimeHub.markBroadcastChannel`) |
1634
+ | Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts#RealtimeHub.setBackplane`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts#registerBackplaneDriver`), cloison `resolveBackplaneOriginId()` (`originId.ts#resolveBackplaneOriginId`) |
1635
+ | Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts#IChannelPolicy`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts#RealtimeHub.registerChannelPolicy`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts#RealtimeHub.hasUnenforcedChannelPolicies`) |
1636
+ | Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts#Firewall.#wireRealtime`) |
1637
+ | Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts#IRealtimeDenied`), erreur `unauthorized` (`JsonRpcPeer.ts#JsonRpcPeer.receive`), fermetures `origin not allowed` (`RealtimeController.ts#RealtimeController.notifyClient`) et `unauthorized` (`RealtimeController.ts#RealtimeController.onHandshake`) |
1638
+ | La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts#RealtimeHub.revalidateRevocable`) |
1639
+ | Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts#SLOW_CONSUMER_BYTES`), fermeture `slow consumer` (`RealtimeHub.ts#SLOW_CONSUMER_BYTES`) |
1640
+ | Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts#RealtimeHub.maxChannelsPerConnection`) |
1641
+ | L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts#IRealtimeWelcome`) |
1642
+ | La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts#RealtimeHub.probe`), `IRealtimeProbe` (`IRealtimeProbe.ts#IRealtimeProbe`), `IRealtimeChannelStat` (`IRealtimeProbe.ts#IRealtimeChannelStat`) |
1643
+ | Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts#createRealtimeAdminApi`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts#buildRealtimeHealth`), `buildOwnHealth()` (`RealtimeAdminApi.ts#buildOwnHealth`) |
1644
+ | Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts#RealtimeService.publish`), `RealtimeService.probe()` (`RealtimeService.ts#RealtimeService.probe`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts#RealtimeService.markBroadcastChannel`) |
1645
+ | Les hooks React | `useNodefony()` (`client/react/index.ts#useNodefony`), `useNodefonyState()` (`client/react/index.ts#useNodefonyState`), `useNodefonyIdentity()` (`client/react/index.ts#useNodefonyIdentity`), `useNodefonyChannel()` (`client/react/index.ts#useNodefonyChannel`), `useNodefonyChannelData()` (`client/react/index.ts#useNodefonyChannelData`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts#useNodefonyAdaptiveChannel`) |
1646
+ | Le client navigateur | `NodefonySocket.connect()` (`NodefonySocket.ts#NodefonySocket.connect`), `NodefonySocket.subscribe()` (`NodefonySocket.ts#NodefonySocket.subscribe`), `NodefonySocket.request()` (`NodefonySocket.ts#NodefonySocket.request`), `NodefonySocket.onDenied()` (`NodefonySocket.ts#NodefonySocket.onDenied`) |
1647
+ | L'upload | `UploadedFile` (`routerDecorators.ts#UploadedFile`), `IUploadedFile` (`IUpload.ts#IUploadedFile`), `files` (`Request.ts#HttpRequest.files`) |
package/docs/index.md CHANGED
@@ -133,34 +133,34 @@ Quatre propriétés, toutes vérifiables dans le code — c'est ce qui justifie
133
133
  WebSocket brute.
134
134
 
135
135
  **Un contrôleur temps réel est un contrôleur.** `RealtimeController`
136
- (`RealtimeController.ts:203`) é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
137
137
  décorateurs de route, reçoit la même injection, passe par le même pare-feu. HTTP et WebSocket ne sont
138
138
  pas deux applications à écrire deux fois, mais deux entrées du même pipeline.
139
139
 
140
140
  **Une connexion, N canaux, dans les deux sens.** Le client s'abonne à autant de canaux qu'il veut sur
141
141
  la même socket. Trois formes de trafic coexistent : le serveur diffuse (`@RealtimeChannel`,
142
- `realtimeDecorators.ts:240`), le client appelle et attend une réponse (`@RealtimeAction`,
143
- `realtimeDecorators.ts:191`), le client pousse sans attendre (`@RealtimeInbound`,
144
- `realtimeDecorators.ts:278`). 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é.
145
145
 
146
146
  **Le travail est fait une fois par processus, pas une fois par client.** Le `RealtimeHub`
147
- (`RealtimeHub.ts:214`) 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
148
148
  dernier départ l'arrête. Chaque connexion supplémentaire n'ajoute qu'une destination de diffusion.
149
149
  Un salon suivi par mille personnes coûte un producteur, pas mille.
150
150
 
151
151
  **Le backplane est un transport, jamais un magasin.** Il relaie les publications d'un processus vers
152
152
  les autres, et rien de plus : aucun historique, aucune reprise. Les drivers vivent dans un registre
153
- (`registerBackplaneDriver`, `backplaneRegistry.ts:55`) — `loopback` (un seul processus), `cluster`
153
+ (`registerBackplaneDriver`, `backplaneRegistry.ts#registerBackplaneDriver`) — `loopback` (un seul processus), `cluster`
154
154
  (échanges entre workers d'un même pod), `redis` (entre machines) — et tu peux y inscrire le tien sans
155
155
  toucher au cœur.
156
156
 
157
157
  > [!IMPORTANT]
158
158
  > **Rien ne franchit la frontière du processus sans intention explicite.** Par défaut, un canal reste
159
159
  > local. Il faut le déclarer diffusable (`@RealtimeBroadcast`,
160
- > `realtimeDecorators.ts:420`) 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
161
161
  > d'observation qui décrit l'état d'**un** pod n'aurait aucun sens répliqué sur les autres.
162
162
 
163
- 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
164
164
  démarrage de backplane est borné dans le temps : un Redis injoignable n'empêche pas l'application de
165
165
  monter, le hub reste local, et la dégradation est annoncée dans les journaux plutôt que subie.
166
166
 
@@ -172,24 +172,35 @@ fichiers.
172
172
  ### 1. Déclarer le module
173
173
 
174
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
175
195
  // nodefony.config.ts — l'orchestrateur de l'application
196
+ import { defineConfig, use } from "nodefony";
197
+ import { realtimeConfig } from "./nodefony/config/realtime";
198
+
176
199
  export default defineConfig(() => ({
177
200
  modules: [
178
201
  "@nodefony/http",
179
202
  "@nodefony/framework",
180
- use("@nodefony/realtime", {
181
- // Un seul processus en développement : le hub distribue localement,
182
- // aucun transport réseau n'est ouvert. Passer à "redis" en production.
183
- backplane: { driver: "loopback" },
184
- // Défense CSRF à la poignée de main (RFC 6455 §10.2) : sans elle, un site
185
- // tiers peut ouvrir une socket avec les cookies de ton utilisateur.
186
- csrf: {
187
- checkOrigin: {
188
- enabled: true,
189
- allowList: ["https://127.0.0.1:5152"],
190
- },
191
- },
192
- }),
203
+ use("@nodefony/realtime", realtimeConfig()),
193
204
  ],
194
205
  }));
195
206
  ```
@@ -305,7 +316,7 @@ reste local et le dit.
305
316
  ## 🧰 Surface publique
306
317
 
307
318
  Côté serveur : `RealtimeController` (la classe à étendre), les décorateurs `RealtimeChannel`,
308
- `RealtimeAction` et `RealtimeInbound`, le service `RealtimeService` (`RealtimeService.ts:50`) pour
319
+ `RealtimeAction` et `RealtimeInbound`, le service `RealtimeService` (`RealtimeService.ts#RealtimeService`) pour
309
320
  publier depuis n'importe quel service injecté, le `RealtimeHub` et sa sonde, les trois backplanes
310
321
  natifs, et le registre de drivers pour brancher le tien.
311
322
 
@@ -318,7 +329,7 @@ jamais recopiées ici : elles divergeraient en silence.
318
329
  ## ⚙️ Configuration
319
330
 
320
331
  Un seul point d'entrée : `use("@nodefony/realtime", { … })` dans `nodefony.config.ts`, validé au boot
321
- 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 :
322
333
 
323
334
  - `backplane` — le driver de fan-out et son espace de nommage. Ce cloisonnement devient
324
335
  indispensable dès que **deux déploiements partagent le même Redis** : sans lui, leurs publications
@@ -340,7 +351,7 @@ vue de tous les workers d'un pod.
340
351
 
341
352
  Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
342
353
  diffusion, connexions, octets et frames, pression d'écriture. C'est la même donnée que celle rendue à
343
- l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:847`).
354
+ l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts#RealtimeHub.probe`).
344
355
 
345
356
  ## 🧪 Tests & couverture
346
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:847`) 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
@@ -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:836`). |
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,9 +314,9 @@ 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:1226`) : 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
321
  s'applique — `drop` par défaut, la frame est **jetée**. Sur un canal d'état c'est sans
322
322
  conséquence, le prochain instantané la remplace ; mais le rejet ne trie rien, et une réponse RPC
@@ -357,8 +357,8 @@ Trois champs n'apparaissent que lorsqu'ils ont un sens :
357
357
 
358
358
  ### La couche d'identité, au-dessus
359
359
 
360
- `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) enrichit ce snapshot pur pour produire
361
- `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
362
362
  (CPU, mémoire, boucle d'événements), et — si elles ont été branchées — la santé ORM et les compteurs
363
363
  d'erreurs du journal. Ces trois derniers champs sont **additifs et optionnels** : un consommateur qui
364
364
  ne les connaît pas les ignore.
@@ -370,22 +370,22 @@ resterait vide jusqu'au premier tick.
370
370
 
371
371
  ### L'instantané HTTP
372
372
 
373
- `GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`)
373
+ `GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts#createRealtimeAdminApi`)
374
374
  sous le namespace `realtime` du data plane admin. Sert le premier affichage, les sondes de liveness,
375
375
  et tout script qui ne veut pas ouvrir une socket.
376
376
 
377
377
  ### Le canal temps réel
378
378
 
379
379
  `nodefony:socket`. Le producteur est un minuteur qui rappelle **le même endpoint** à travers le
380
- courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts:212`,
381
- 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
382
382
  rejoué.
383
383
 
384
384
  ### La cadence vit dans le nom du canal
385
385
 
386
386
  Convention isomorphe, partagée par les deux bords : `base` nu = cadence par défaut du serveur ;
387
- `base:<ms>` = cadence explicite. Le client fabrique le nom avec `rateChannel()` (`channelRate.ts:44`),
388
- 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`).
389
389
 
390
390
  Conséquence voulue : **un canal = une cadence = un compteur de références**. `nodefony:socket:500` et
391
391
  `nodefony:socket` sont deux minuteurs distincts, jamais réconciliés — et deux consommateurs qui
@@ -394,23 +394,23 @@ veulent tous deux le défaut partagent un seul minuteur, parce que la cadence pa
394
394
 
395
395
  ### Les canaux de santé qui circulent sur la socket
396
396
 
397
- Les bornes sont déclarées dans `RATE_BOUNDS` (`StudioRealtimeController.ts:59`), les noms dans
398
- `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
399
399
  refusée.
400
400
 
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:222`) — 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: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é) |
409
409
 
410
410
  > [!WARNING]
411
411
  > **`nodefony:dashboard` n'est servi par aucun fournisseur.** Le canal de supervision s'appelle
412
412
  > `nodefony:supervision`. Le nom survit pourtant comme constante exportée, dépréciée
413
- > (`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
414
414
  > erreur** — le provider rend `null` et rien n'arrive jamais. C'est le piège numéro un de la page.
415
415
 
416
416
  `nodefony:syslog` est le seul de la liste à ne pas être cadencé : il **relaie** au lieu de sonder. Comme
@@ -441,11 +441,11 @@ après fusion et validation — la seule vue qui dit ce qui s'applique, plutôt
441
441
 
442
442
  ## ⚙️ Configuration
443
443
 
444
- | Clé | Défaut | Effet |
445
- | ------------------------ | ------ | ------------------------------------------------------------------------- |
446
- | `slowConsumer.bytes` | 1 Mio | Seuil de **comptage** de `slowConsumers` dans la sonde (`config.ts:121`). |
447
- | `cluster.probe.enabled` | `true` | Branche la sonde agrégée pod en worker de cluster (`config.ts:30`). |
448
- | `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. |
449
449
 
450
450
  > [!CAUTION]
451
451
  > `slowConsumer.bytes` ne pilote **que la métrique**. Les seuils qui jettent une frame ou ferment une
@@ -454,7 +454,7 @@ après fusion et validation — la seule vue qui dit ce qui s'applique, plutôt
454
454
  > ne change rien au comportement de la socket.
455
455
 
456
456
  Couper la sonde cluster est un **contournement total**, pas une mise en sourdine : sans elle, il n'y a
457
- 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
458
458
  l'endpoint retombe sur la vue per-instance. Aucun coût « au cas où ».
459
459
 
460
460
  ## 🏗️ En cluster — par pod ou par worker ?
@@ -464,11 +464,11 @@ instantané est exact — et ne dit rien des trois autres. Pire : deux appels co
464
464
  sur deux workers et sembler se contredire.
465
465
 
466
466
  **La réponse de Nodefony est un modèle _push_.** Chaque worker remonte périodiquement sa santé au
467
- maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts:190`) ; le maître fusionne et
467
+ maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts#ClusterProbeClient.start`) ; le maître fusionne et
468
468
  rediffuse ; chaque worker met le résultat en cache. **N'importe lequel** sert alors la vue pod en temps
469
- constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:306`).
469
+ constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts#ClusterProbeClient.getClusterHealth`).
470
470
 
471
- La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:60`), est une fonction pure — et sa règle
471
+ La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts#mergeClusterHealth`), est une fonction pure — et sa règle
472
472
  mérite d'être connue :
473
473
 
474
474
  | Grandeur | Agrégation | Pourquoi |
@@ -478,14 +478,14 @@ mérite d'être connue :
478
478
  | `backpressure.maxBufferedAmount` | **maximum** | La santé d'une flotte se juge sur son **pire** membre, pas sur sa moyenne. |
479
479
  | `slowConsumers`, `drops` | somme | Ce sont des dénombrements. |
480
480
 
481
- 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`) :
482
482
  un consommateur sait immédiatement s'il lit une vue pod ou une vue per-instance, et `instances[]`
483
483
  garde le détail par worker pour le forage.
484
484
 
485
485
  ### On paie ce qu'on regarde
486
486
 
487
487
  Les sondes coûteuses ne tournent pas en permanence. Le maître peut demander à un worker précis
488
- d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts:259`) — quand
488
+ d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts#ClusterProbeClient.requestEnrich`) — quand
489
489
  un humain ouvre le forage de ce `pid`. La sonde riche (espaces mémoire, cycles de ramasse-miettes,
490
490
  descripteurs actifs) est alors allouée ; à la fermeture de l'écran, elle est libérée et son observateur
491
491
  détaché.
@@ -506,8 +506,8 @@ l'orchestrateur finit par le tuer de force à l'expiration du délai de grâce.
506
506
 
507
507
  La règle est tenue partout dans le code, y compris là où on ne l'attendrait pas — le tick de
508
508
  revalidation d'identité du hub, qui ne démarre qu'au premier inscrit, est lui aussi détaché
509
- (`RealtimeHub.ts:443`), tout comme le report de la sonde cluster (`ClusterProbeClient.ts:170`) et le
510
- 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`).
511
511
 
512
512
  ```ts ignore
513
513
  const timer = setInterval(tick, 2000);