@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.
- package/README.md +29 -10
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +4 -6
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/configSchema.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/decorators/realtimeDecorators.js +1 -2
- package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
- package/dist/nodefony/src/backplane/RedisBackplane.js +1 -2
- package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
- package/dist/nodefony/src/backplane/bootNotices.js +1 -2
- package/dist/nodefony/src/backplane/envelope.js +1 -2
- package/dist/nodefony/src/backplane/originId.js +1 -2
- package/dist/nodefony/src/backplane/publishQueue.js +0 -1
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -2
- package/dist/nodefony/src/guards.js +1 -2
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -2
- package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
- package/dist/nodefony/src/server/RealtimeController.js +158 -84
- package/dist/nodefony/src/server/RealtimeHub.js +3 -3
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +18 -9
- package/dist/nodefony/src/server/deniedDetail.js +1 -2
- package/dist/nodefony/src/server/subscribedAck.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +1 -2
- package/dist/nodefony/src/server/welcomeEnv.js +1 -2
- package/dist/nodefony/src/service/RealtimeService.js +1 -2
- package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
- package/dist/nodefony/testing/index.js +77 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +60 -2
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
- package/dist/types/nodefony/testing/index.d.ts +11 -4
- package/docs/actions.md +67 -44
- package/docs/architecture.md +125 -119
- package/docs/cadence-et-contre-pression.md +31 -22
- package/docs/configuration.md +106 -82
- package/docs/cookbook-chat.md +56 -34
- package/docs/index.md +35 -24
- package/docs/observabilite.md +49 -49
- package/docs/protocole.md +91 -68
- package/docs/securite.md +135 -115
- package/docs/vocabulaire.md +117 -114
- package/package.json +10 -10
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/cookbook-chat.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
795
|
-
|
|
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
|
|
1607
|
-
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts
|
|
1608
|
-
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts
|
|
1609
|
-
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts
|
|
1610
|
-
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts
|
|
1611
|
-
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts
|
|
1612
|
-
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts
|
|
1613
|
-
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts
|
|
1614
|
-
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts
|
|
1615
|
-
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts
|
|
1616
|
-
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts
|
|
1617
|
-
| Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts
|
|
1618
|
-
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts
|
|
1619
|
-
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts
|
|
1620
|
-
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts
|
|
1621
|
-
| Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts
|
|
1622
|
-
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts
|
|
1623
|
-
| Les hooks React | `useNodefony()` (`client/react/index.ts
|
|
1624
|
-
| Le client navigateur | `NodefonySocket.connect()` (`NodefonySocket.ts
|
|
1625
|
-
| L'upload | `UploadedFile` (`routerDecorators.ts
|
|
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
|
|
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
|
|
143
|
-
`realtimeDecorators.ts
|
|
144
|
-
`realtimeDecorators.ts
|
|
142
|
+
`realtimeDecorators.ts#RealtimeChannel`), le client appelle et attend une réponse (`@RealtimeAction`,
|
|
143
|
+
`realtimeDecorators.ts#RealtimeAction`), le client pousse sans attendre (`@RealtimeInbound`,
|
|
144
|
+
`realtimeDecorators.ts#RealtimeInbound`). Rien n'est ouvert qui n'ait été déclaré.
|
|
145
145
|
|
|
146
146
|
**Le travail est fait une fois par processus, pas une fois par client.** Le `RealtimeHub`
|
|
147
|
-
(`RealtimeHub.ts
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
354
|
+
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts#RealtimeHub.probe`).
|
|
344
355
|
|
|
345
356
|
## 🧪 Tests & couverture
|
|
346
357
|
|
package/docs/observabilite.md
CHANGED
|
@@ -96,7 +96,7 @@ temps de réponse — elle mesure **ce qui n'est pas encore parti**.
|
|
|
96
96
|
Trois partis pris, tous lisibles dans le code.
|
|
97
97
|
|
|
98
98
|
**1. La sonde est une lecture pure, jamais un collecteur.** `RealtimeHub.probe()`
|
|
99
|
-
(`RealtimeHub.ts
|
|
99
|
+
(`RealtimeHub.ts#RealtimeHub.probe`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
|
|
100
100
|
Aucune entrée-sortie, aucune exception possible, aucun état de lecture conservé. Appeler la sonde
|
|
101
101
|
mille fois par seconde ne changerait rien à ce que le hub fait par ailleurs.
|
|
102
102
|
|
|
@@ -107,7 +107,7 @@ divergence entre deux consommateurs, et un rafraîchissement qui se choisit libr
|
|
|
107
107
|
|
|
108
108
|
**3. La mesure voyage sur la socket qu'elle décrit.** Le même producteur alimente l'endpoint HTTP
|
|
109
109
|
(premier affichage, sans attendre un tick) et le canal temps réel (le suivi). Un seul code, deux
|
|
110
|
-
portes — `buildRealtimeHealth()` (`RealtimeAdminApi.ts
|
|
110
|
+
portes — `buildRealtimeHealth()` (`RealtimeAdminApi.ts#buildRealtimeHealth`).
|
|
111
111
|
|
|
112
112
|
Le compromis assumé : cette sonde est **per-instance par nature**. Un pod à quatre workers rend quatre
|
|
113
113
|
vérités partielles. Nodefony ne les moyenne pas au hasard de la requête — il pousse une vue agrégée
|
|
@@ -247,26 +247,26 @@ socket.subscribe("shop:health");
|
|
|
247
247
|
|
|
248
248
|
## 🧰 Ce que la sonde expose, champ par champ
|
|
249
249
|
|
|
250
|
-
Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts
|
|
250
|
+
Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts#IRealtimeProbe`). Quatre familles, un discriminant.
|
|
251
251
|
|
|
252
252
|
### Canaux — qui vit, qui écoute
|
|
253
253
|
|
|
254
|
-
| Champ | Ce qu'il dit
|
|
255
|
-
| ------------------------ |
|
|
256
|
-
| `channels[]` | Une entrée par canal **actif** (au moins un abonné), cf `IRealtimeChannelStat` (`IRealtimeProbe.ts
|
|
257
|
-
| `channels[].channel` | Le nom exact souscrit, **suffixe de cadence compris** (`nodefony:orm:health:2000`).
|
|
258
|
-
| `channels[].subscribers` | Abonnés locaux vivants. Instantané, pas un cumul.
|
|
259
|
-
| `channels[].messages` | Publications cumulées sur ce canal.
|
|
260
|
-
| `channelCount` | Nombre de canaux actifs sur ce processus.
|
|
254
|
+
| Champ | Ce qu'il dit |
|
|
255
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| `channels[]` | Une entrée par canal **actif** (au moins un abonné), cf `IRealtimeChannelStat` (`IRealtimeProbe.ts#IRealtimeChannelStat`). |
|
|
257
|
+
| `channels[].channel` | Le nom exact souscrit, **suffixe de cadence compris** (`nodefony:orm:health:2000`). |
|
|
258
|
+
| `channels[].subscribers` | Abonnés locaux vivants. Instantané, pas un cumul. |
|
|
259
|
+
| `channels[].messages` | Publications cumulées sur ce canal. |
|
|
260
|
+
| `channelCount` | Nombre de canaux actifs sur ce processus. |
|
|
261
261
|
|
|
262
262
|
### Fan-out — l'amplification réelle du broker
|
|
263
263
|
|
|
264
|
-
| Champ | Ce qu'il dit
|
|
265
|
-
| ---------------------- |
|
|
266
|
-
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.**
|
|
267
|
-
| `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.**
|
|
268
|
-
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts
|
|
269
|
-
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable.
|
|
264
|
+
| Champ | Ce qu'il dit |
|
|
265
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
266
|
+
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
|
|
267
|
+
| `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
|
|
268
|
+
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts#RealtimeHub.recordInbound`). |
|
|
269
|
+
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
|
|
270
270
|
|
|
271
271
|
Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
|
|
272
272
|
moyenne d'un salon**. S'il vaut 1, chaque publication ne touche qu'une personne — un canal par client,
|
|
@@ -314,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
|
|
317
|
+
La source par connexion est `IRealtimeConnProbe` (`IRealtimeProbe.ts#IRealtimeConnProbe`), implémentée par le
|
|
318
318
|
transport. Les seuils d'ACTION sont distincts du seuil de comptage, et ils sont
|
|
319
|
-
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts
|
|
319
|
+
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts#httpConfigSchema`) : au-delà de
|
|
320
320
|
`websocket.maxBackpressure` (4 MiB par défaut) la politique `websocket.backpressurePolicy`
|
|
321
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
|
|
361
|
-
`IRealtimeHealth` (`IRealtimeProbe.ts
|
|
360
|
+
`buildOwnHealth()` (`RealtimeAdminApi.ts#buildOwnHealth`) enrichit ce snapshot pur pour produire
|
|
361
|
+
`IRealtimeHealth` (`IRealtimeProbe.ts#IRealtimeHealth`) : `instanceId`, la santé **process** du worker
|
|
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
|
|
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
|
|
381
|
-
via `createBrokerTicker()` (`providers.ts
|
|
380
|
+
courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts#StudioRealtimeController.createRealtimeChannel`,
|
|
381
|
+
via `createBrokerTicker()` (`providers.ts#createBrokerTicker`)). Rien n'est dupliqué : le canal est un endpoint
|
|
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
|
|
388
|
-
le serveur le résout et le **borne** avec `parseRate()` (`channelRate.ts
|
|
387
|
+
`base:<ms>` = cadence explicite. Le client fabrique le nom avec `rateChannel()` (`channelRate.ts#rateChannel`),
|
|
388
|
+
le serveur le résout et le **borne** avec `parseRate()` (`channelRate.ts#parseRate`).
|
|
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
|
|
398
|
-
`CHANNELS` (`providers.ts
|
|
397
|
+
Les bornes sont déclarées dans `RATE_BOUNDS` (`StudioRealtimeController.ts#RATE_BOUNDS`), les noms dans
|
|
398
|
+
`CHANNELS` (`providers.ts#CHANNELS`). Une cadence hors bornes est **ramenée dans l'intervalle**, jamais
|
|
399
399
|
refusée.
|
|
400
400
|
|
|
401
|
-
| Canal | Qui le produit
|
|
402
|
-
| ---------------------- |
|
|
403
|
-
| `nodefony:socket` | La sonde du hub, rejouée depuis l'endpoint admin `realtime`
|
|
404
|
-
| `nodefony:supervision` | `createStatsTicker()` (`providers.ts
|
|
405
|
-
| `nodefony:debugbar` | Le même producteur, canal séparé pour la barre de débogage
|
|
406
|
-
| `nodefony:orm:health` | Endpoint admin `orm/connection/health`, rejoué
|
|
407
|
-
| `nodefony:orm:flow` | Endpoint admin `orm/flow` (débit de requêtes)
|
|
408
|
-
| `nodefony:syslog` | `createSyslogBridge()` (`providers.ts
|
|
401
|
+
| Canal | Qui le produit | Cadence par défaut | Bornes |
|
|
402
|
+
| ---------------------- | -------------------------------------------------------------------------------- | ------------------ | ------------- |
|
|
403
|
+
| `nodefony:socket` | La sonde du hub, rejouée depuis l'endpoint admin `realtime` | 2 s | 500 ms – 60 s |
|
|
404
|
+
| `nodefony:supervision` | `createStatsTicker()` (`providers.ts#createStatsTicker`) — CPU, mémoire, GC, ELU | 1 s | 250 ms – 60 s |
|
|
405
|
+
| `nodefony:debugbar` | Le même producteur, canal séparé pour la barre de débogage | 1 s | 250 ms – 60 s |
|
|
406
|
+
| `nodefony:orm:health` | Endpoint admin `orm/connection/health`, rejoué | 5 s | 1 s – 60 s |
|
|
407
|
+
| `nodefony:orm:flow` | Endpoint admin `orm/flow` (débit de requêtes) | 2 s | 500 ms – 60 s |
|
|
408
|
+
| `nodefony:syslog` | `createSyslogBridge()` (`providers.ts#createSyslogBridge`) | **événementiel** | — (coalescé) |
|
|
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
|
|
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
|
|
447
|
-
| `cluster.probe.enabled` | `true` | Branche la sonde agrégée pod en worker de cluster (`config.ts
|
|
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
|
|
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
|
|
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
|
|
469
|
+
constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts#ClusterProbeClient.getClusterHealth`).
|
|
470
470
|
|
|
471
|
-
La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts
|
|
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
|
|
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
|
|
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
|
|
510
|
-
minuteur générique des canaux (`providers.ts
|
|
509
|
+
(`RealtimeHub.ts#RealtimeHub.subscribeClient`), tout comme le report de la sonde cluster (`ClusterProbeClient.ts#ClusterProbeClient.#ormRich`) et le
|
|
510
|
+
minuteur générique des canaux (`providers.ts#createBrokerTicker`).
|
|
511
511
|
|
|
512
512
|
```ts ignore
|
|
513
513
|
const timer = setInterval(tick, 2000);
|