@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/architecture.md
CHANGED
|
@@ -124,9 +124,9 @@ Nodefony étiquette avec **JSON-RPC 2.0**, une norme publique plutôt qu'un form
|
|
|
124
124
|
| **notification** | non | non | pousser un événement, s'abonner, se désabonner |
|
|
125
125
|
|
|
126
126
|
Le point remarquable : le **même** moteur de protocole tourne des deux côtés du fil. La
|
|
127
|
-
classe `JsonRpcPeer` (`JsonRpcPeer.ts
|
|
127
|
+
classe `JsonRpcPeer` (`JsonRpcPeer.ts#JsonRpcPeer`) est du code isomorphe du cœur — le navigateur
|
|
128
128
|
l'exécute dans `NodefonySocket`, le serveur l'instancie une fois par connexion dans
|
|
129
|
-
`RealtimeController.onHandshake()` (`RealtimeController.ts
|
|
129
|
+
`RealtimeController.onHandshake()` (`RealtimeController.ts#RealtimeController.onHandshake`). Le serveur peut donc
|
|
130
130
|
appeler le client, pas seulement l'inverse : c'est du vrai duplex, pas un aller-retour
|
|
131
131
|
déguisé.
|
|
132
132
|
|
|
@@ -136,16 +136,16 @@ Trois partis pris distinguent cette pile d'un simple « serveur WebSocket ».
|
|
|
136
136
|
|
|
137
137
|
**Un seul contrat de socket, des deux côtés.** Un service back qui pousse des événements
|
|
138
138
|
écrit le même code qu'une page front : `publish` / `subscribe` / `on`. Côté serveur c'est
|
|
139
|
-
`ServerRealtimeSocket` (`ServerRealtimeSocket.ts
|
|
140
|
-
(`ServerRealtimeSocket.ts
|
|
141
|
-
et explicite : `ServerRealtimeSocket.request()` (`ServerRealtimeSocket.ts
|
|
139
|
+
`ServerRealtimeSocket` (`ServerRealtimeSocket.ts#ServerRealtimeSocket`), obtenu par `serverSocket()`
|
|
140
|
+
(`ServerRealtimeSocket.ts#serverSocket`) ; côté navigateur, `NodefonySocket`. Une exception, assumée
|
|
141
|
+
et explicite : `ServerRealtimeSocket.request()` (`ServerRealtimeSocket.ts#ServerRealtimeSocket.request`) **rejette
|
|
142
142
|
toujours** — un handle posé sur le hub n'a pas d'interlocuteur unique, puisque le hub est
|
|
143
143
|
multi-clients. Pour un appel serveur→client ciblé, on passe par la connexion :
|
|
144
|
-
`RealtimeController.requestClient()` (`RealtimeController.ts
|
|
144
|
+
`RealtimeController.requestClient()` (`RealtimeController.ts#RealtimeController.requestClient`).
|
|
145
145
|
|
|
146
146
|
**Un provider par canal, pas un par client.** Si mille onglets s'abonnent au même canal de
|
|
147
147
|
santé, le calcul ne doit tourner qu'une fois. Le hub crée le producteur au **premier**
|
|
148
|
-
abonné et le détruit au **dernier** (`RealtimeHub.subscribe()`, `RealtimeHub.ts
|
|
148
|
+
abonné et le détruit au **dernier** (`RealtimeHub.subscribe()`, `RealtimeHub.ts#RealtimeHub.subscribe`) : le
|
|
149
149
|
coût suit le nombre de canaux, pas le nombre de clients.
|
|
150
150
|
|
|
151
151
|
**Le cluster est un détail de configuration.** Ni ton contrôleur ni ton client ne savent
|
|
@@ -154,7 +154,7 @@ chaîne de caractères. Passer de mono-process à multi-pods ne touche aucune li
|
|
|
154
154
|
|
|
155
155
|
**Le compromis, dit franchement** : un canal **ne franchit PAS** la frontière du process
|
|
156
156
|
tant que tu ne l'as pas déclaré. Le défaut est l'isolement (`RealtimeHub.publish()`,
|
|
157
|
-
`RealtimeHub.ts
|
|
157
|
+
`RealtimeHub.ts#RealtimeHub.publish`) — un choix de sûreté détaillé plus bas.
|
|
158
158
|
|
|
159
159
|
## 🚀 Démarrage rapide
|
|
160
160
|
|
|
@@ -164,25 +164,31 @@ trois fichiers, et il fonctionne en mono-process comme en cluster.
|
|
|
164
164
|
### 1. Charger le module et choisir le fond de panier
|
|
165
165
|
|
|
166
166
|
```ts
|
|
167
|
+
// nodefony/config/realtime.ts — le fragment du module, importé par le manifeste
|
|
168
|
+
import type { IRealtimeConfigInput } from "@nodefony/realtime";
|
|
169
|
+
|
|
170
|
+
export const realtimeConfig = () =>
|
|
171
|
+
({
|
|
172
|
+
// "loopback" = un seul process : le hub suffit, aucun transport réseau.
|
|
173
|
+
// "cluster" (workers IPC) ou "redis" (multi-pods) sans changer une ligne de code.
|
|
174
|
+
backplane: { driver: "loopback" },
|
|
175
|
+
// Défense CSRF de l'upgrade WebSocket : SameSite ne protège pas un handshake.
|
|
176
|
+
csrf: {
|
|
177
|
+
checkOrigin: {
|
|
178
|
+
enabled: true,
|
|
179
|
+
allowList: ["http://localhost:5151"],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
}) satisfies IRealtimeConfigInput;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```ts ignore
|
|
167
186
|
// nodefony.config.ts
|
|
168
187
|
import { defineConfig, use } from "nodefony";
|
|
188
|
+
import { realtimeConfig } from "./nodefony/config/realtime";
|
|
169
189
|
|
|
170
190
|
export default defineConfig(() => ({
|
|
171
|
-
modules: [
|
|
172
|
-
"@nodefony/framework",
|
|
173
|
-
use("@nodefony/realtime", {
|
|
174
|
-
// "loopback" = un seul process : le hub suffit, aucun transport réseau.
|
|
175
|
-
// "cluster" (workers IPC) ou "redis" (multi-pods) sans changer une ligne de code.
|
|
176
|
-
backplane: { driver: "loopback" },
|
|
177
|
-
// Défense CSRF de l'upgrade WebSocket : SameSite ne protège pas un handshake.
|
|
178
|
-
csrf: {
|
|
179
|
-
checkOrigin: {
|
|
180
|
-
enabled: true,
|
|
181
|
-
allowList: ["http://localhost:5151"],
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
}),
|
|
185
|
-
],
|
|
191
|
+
modules: ["@nodefony/framework", use("@nodefony/realtime", realtimeConfig())],
|
|
186
192
|
}));
|
|
187
193
|
```
|
|
188
194
|
|
|
@@ -291,7 +297,7 @@ export async function start(): Promise<void> {
|
|
|
291
297
|
### Ce qu'on observe
|
|
292
298
|
|
|
293
299
|
Au démarrage du serveur, le module annonce son fond de panier — une ligne, toujours la
|
|
294
|
-
même forme, quel que soit le driver (`src/packages/@nodefony/realtime/index.ts
|
|
300
|
+
même forme, quel que soit le driver (`src/packages/@nodefony/realtime/index.ts#Realtime.#wireBackplane`) :
|
|
295
301
|
|
|
296
302
|
```
|
|
297
303
|
realtime backplane driver=loopback kind=local cross-pod=no (hub local)
|
|
@@ -319,13 +325,13 @@ entre les deux **est** le facteur d'amplification du canal.
|
|
|
319
325
|
|
|
320
326
|
La pile se lit de haut en bas ; chaque étage ne connaît que son voisin du dessous.
|
|
321
327
|
|
|
322
|
-
| Étage | Qui
|
|
323
|
-
| ---------- |
|
|
324
|
-
| Applicatif | ton contrôleur, tes services
|
|
325
|
-
| Protocole | `JsonRpcPeer` (`JsonRpcPeer.ts
|
|
326
|
-
| Transport | `WsConnectionTransport` (`WsConnectionTransport.ts
|
|
327
|
-
| Hub | `RealtimeHub` (`RealtimeHub.ts
|
|
328
|
-
| Backplane | `IBackplane` (`IBackplane.ts
|
|
328
|
+
| Étage | Qui | Sa seule responsabilité | Ce qu'il ignore |
|
|
329
|
+
| ---------- | -------------------------------------------------------------------------- | --------------------------------------- | ----------------------------- |
|
|
330
|
+
| Applicatif | ton contrôleur, tes services | le métier : quoi publier, quoi accepter | tout le reste |
|
|
331
|
+
| Protocole | `JsonRpcPeer` (`JsonRpcPeer.ts#JsonRpcPeer`) | étiqueter, corréler, refuser une frame | par où passent les octets |
|
|
332
|
+
| Transport | `WsConnectionTransport` (`WsConnectionTransport.ts#WsConnectionTransport`) | déplacer des octets, mesurer la file | ce que veut dire un message |
|
|
333
|
+
| Hub | `RealtimeHub` (`RealtimeHub.ts#RealtimeHub`) | table des abonnés locaux + fan-out | qu'il existe d'autres process |
|
|
334
|
+
| Backplane | `IBackplane` (`IBackplane.ts#IBackplane`) | porter un message aux autres process | ce qu'est un abonné |
|
|
329
335
|
|
|
330
336
|
> [!NOTE]
|
|
331
337
|
> Une part de la documentation historique décrit cette pile en « 5 étages » en comptant
|
|
@@ -338,11 +344,11 @@ La pile se lit de haut en bas ; chaque étage ne connaît que son voisin du dess
|
|
|
338
344
|
dispatch des notifications, gestion des erreurs, `dispose()` propre. Deux points
|
|
339
345
|
d'interception y sont posés :
|
|
340
346
|
|
|
341
|
-
- `beforeDispatch` (`JsonRpcPeer.ts
|
|
347
|
+
- `beforeDispatch` (`JsonRpcPeer.ts#JsonRpcPeerOptions.beforeDispatch`) — un verrou **synchrone** appelé avant tout
|
|
342
348
|
traitement de frame. Il rend `true`/`false`. Un refus sur une requête produit
|
|
343
349
|
`-32001 unauthorized` ; sur une notification, la frame est jetée
|
|
344
|
-
(`JsonRpcPeer.ts
|
|
345
|
-
- `onFrameAudit` (`JsonRpcPeer.ts
|
|
350
|
+
(`JsonRpcPeer.ts#JsonRpcPeer.receive`).
|
|
351
|
+
- `onFrameAudit` (`JsonRpcPeer.ts#JsonRpcPeerOptions.onFrameAudit`) — la trace des événements protocolaires notables
|
|
346
352
|
(frame invalide, refusée, méthode inconnue, erreur interne).
|
|
347
353
|
|
|
348
354
|
La contrainte de synchronisme n'est pas un oubli : un `await` par frame coûterait une
|
|
@@ -355,14 +361,14 @@ au handshake, puis lue en O(1). C'est le compromis assumé, documenté dans
|
|
|
355
361
|
Particularité contre-intuitive : côté serveur, le transport **n'écoute pas** d'événement
|
|
356
362
|
socket. C'est le pipeline HTTP de `@nodefony/http` qui appelle la route WebSocket du
|
|
357
363
|
contrôleur à chaque message reçu, laquelle pousse la charge dans
|
|
358
|
-
`WsConnectionTransport.feed()` (`WsConnectionTransport.ts
|
|
364
|
+
`WsConnectionTransport.feed()` (`WsConnectionTransport.ts#WsConnectionTransport.feed`). La fermeture arrive de la
|
|
359
365
|
même façon, par le hook `onFinish` du contexte, qui déclenche `fireClose()`
|
|
360
|
-
(`WsConnectionTransport.ts
|
|
366
|
+
(`WsConnectionTransport.ts#WsConnectionTransport.fireClose`).
|
|
361
367
|
|
|
362
368
|
Ce transport porte aussi la **back-pressure**. Elle n'est pas câblée en dur : les trois
|
|
363
369
|
leviers sont des clés de configuration du serveur WebSocket, lues par le transport
|
|
364
|
-
(`WsConnectionTransport.ts
|
|
365
|
-
(`http/nodefony/config/config.ts
|
|
370
|
+
(`WsConnectionTransport.ts#WsConnectionTransport`) et documentées dans `@nodefony/http`
|
|
371
|
+
(`http/nodefony/config/config.ts#httpConfigSchema`).
|
|
366
372
|
|
|
367
373
|
| File non drainée (`bufferedAmount`) | Décision | Réglage (défaut) |
|
|
368
374
|
| ----------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
@@ -378,7 +384,7 @@ un canal d'état, où le prochain instantané remplace celui qu'on a sauté ; ma
|
|
|
378
384
|
rien, et une réponse RPC ou un événement jetés sont perdus. Détail et courbes :
|
|
379
385
|
[Cadence et contre-pression](./cadence-et-contre-pression.md).
|
|
380
386
|
|
|
381
|
-
À ne pas confondre avec `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts
|
|
387
|
+
À ne pas confondre avec `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts#SLOW_CONSUMER_BYTES`, 1 MiB) : ce seuil-là
|
|
382
388
|
ne jette rien, il **compte** — c'est celui à partir duquel la sonde marque une connexion
|
|
383
389
|
`slowConsumer`.
|
|
384
390
|
|
|
@@ -390,7 +396,7 @@ Le schéma ci-dessous rend ces étages vivants : active le temps réel et il res
|
|
|
390
396
|
|
|
391
397
|
## 🔌 Le cycle de vie d'une connexion
|
|
392
398
|
|
|
393
|
-
Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts
|
|
399
|
+
Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts#RealtimeController.onHandshake`), appelé
|
|
394
400
|
une seule fois par connexion, en chemin froid.
|
|
395
401
|
|
|
396
402
|
```mermaid
|
|
@@ -421,19 +427,19 @@ sequenceDiagram
|
|
|
421
427
|
|
|
422
428
|
Les étapes, dans l'ordre exact du code :
|
|
423
429
|
|
|
424
|
-
1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts
|
|
430
|
+
1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts#RealtimeHub.checkOrigin`). Refus →
|
|
425
431
|
fermeture `4003`. La politique vient de la configuration ; sans politique, tout passe.
|
|
426
432
|
2. **Résolution de l'identité** — `RealtimeHub.resolveAuthenticator()`
|
|
427
|
-
(`RealtimeHub.ts
|
|
433
|
+
(`RealtimeHub.ts#RealtimeHub.resolveAuthenticator`) parcourt les authentificateurs enregistrés : **le premier motif
|
|
428
434
|
qui correspond capture**. Aucun ne correspond ? Le jeton anonyme gelé est posé
|
|
429
435
|
(`ANONYMOUS_REALTIME_TOKEN`) — la lecture d'identité ne rend donc jamais `null`. Un
|
|
430
436
|
échec d'authentification ferme en `4001`.
|
|
431
437
|
3. **Création du peer et du transport**, puis association `peer → jeton`
|
|
432
|
-
(`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts
|
|
438
|
+
(`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts#RealtimeHub.setTokenForPeer`), stockée dans une `WeakMap` : le
|
|
433
439
|
jeton disparaît avec le peer, sans fuite.
|
|
434
440
|
4. **Enregistrement des actions** — celles des décorateurs `@RealtimeAction`, puis celles
|
|
435
441
|
de la surcharge `realtimeActions()`, qui gagne en cas de conflit. Le pont API
|
|
436
|
-
`api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts
|
|
442
|
+
`api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts#RealtimeController.realtimeApiRequest`)
|
|
437
443
|
rend `true`.
|
|
438
444
|
5. **Inscription aux registres** : sonde de connexion, révocation périodique si le jeton
|
|
439
445
|
est révocable, préfixes de canaux broadcast, canaux entrants.
|
|
@@ -441,7 +447,7 @@ Les étapes, dans l'ordre exact du code :
|
|
|
441
447
|
l'identité résolue (type, authentifié ou non, rôles, portées). Le client sait **qui il
|
|
442
448
|
est** sans appeler la moindre route.
|
|
443
449
|
|
|
444
|
-
À la fermeture, un unique `onFinish` (`RealtimeController.ts
|
|
450
|
+
À la fermeture, un unique `onFinish` (`RealtimeController.ts#RealtimeController.onHandshake`) fait le ménage complet :
|
|
445
451
|
désabonnement de chaque canal tenu, retrait des deux registres, `fireClose()` du transport,
|
|
446
452
|
`dispose()` du peer. C'est ce qui garantit qu'aucun minuteur ni écouteur ne survit à une
|
|
447
453
|
déconnexion.
|
|
@@ -453,37 +459,37 @@ déconnexion.
|
|
|
453
459
|
|
|
454
460
|
### Deux gardes qui ferment la connexion
|
|
455
461
|
|
|
456
|
-
| Garde | Déclencheur | Effet | Ancrage
|
|
457
|
-
| --------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- |
|
|
458
|
-
| Plafond de canaux | une connexion dépasse son quota d'abonnements (256 par défaut) | abonnement refusé + `realtime:denied` motif `limit` | `RealtimeController.ts
|
|
459
|
-
| Révocation d'identité | tick périodique ; `token.isValid()` rend `false` ou lève une exception | fermeture `4001` « session revoked » | `RealtimeHub.ts
|
|
462
|
+
| Garde | Déclencheur | Effet | Ancrage |
|
|
463
|
+
| --------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------- |
|
|
464
|
+
| Plafond de canaux | une connexion dépasse son quota d'abonnements (256 par défaut) | abonnement refusé + `realtime:denied` motif `limit` | `RealtimeController.ts#RealtimeController.onRealtimeNotification` |
|
|
465
|
+
| Révocation d'identité | tick périodique ; `token.isValid()` rend `false` ou lève une exception | fermeture `4001` « session revoked » | `RealtimeHub.ts#FrameAuthorizer` |
|
|
460
466
|
|
|
461
467
|
La seconde mérite une explication. Le verrou de frame est synchrone et lit une identité
|
|
462
468
|
figée au handshake : il ne peut donc pas voir une session qui meurt en cours de route (une
|
|
463
469
|
déconnexion HTTP, par exemple). Un minuteur — démarré au premier inscrit, arrêté dès que le
|
|
464
|
-
registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts
|
|
470
|
+
registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts#RealtimeHub.registerRevocable`) — relit
|
|
465
471
|
périodiquement ces identités et coupe les sockets orphelines. Seules les identités
|
|
466
472
|
**révocables** y entrent : un visiteur anonyme ne coûte rien.
|
|
467
473
|
|
|
468
474
|
## Le hub — canaux partagés et fan-out
|
|
469
475
|
|
|
470
|
-
Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts
|
|
476
|
+
Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts#getRealtimeHub`). Il ne
|
|
471
477
|
dépend de rien : ce sont les fabriques fournies par les contrôleurs qui portent les
|
|
472
478
|
dépendances.
|
|
473
479
|
|
|
474
480
|
### Le provider partagé — un ticker, pas mille
|
|
475
481
|
|
|
476
|
-
`RealtimeHub.subscribe()` (`RealtimeHub.ts
|
|
482
|
+
`RealtimeHub.subscribe()` (`RealtimeHub.ts#RealtimeHub.subscribe`) applique une mécanique en trois temps :
|
|
477
483
|
|
|
478
484
|
1. Le canal existe déjà ? On ajoute simplement le sink de cette connexion. Fin.
|
|
479
485
|
2. Sinon, on inscrit le sink **avant** d'appeler la fabrique — de sorte que le tout premier
|
|
480
486
|
paquet du producteur atteigne bien ce premier abonné.
|
|
481
487
|
3. La fabrique du contrôleur rend `null` (canal inconnu de lui) ? Dernier recours : le
|
|
482
488
|
registre des **canaux système** (`RealtimeHub.registerSystemChannel()`,
|
|
483
|
-
`RealtimeHub.ts
|
|
489
|
+
`RealtimeHub.ts#RealtimeHub.registerSystemChannel`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
|
|
484
490
|
l'abonnement est refusé et rien n'est alloué.
|
|
485
491
|
|
|
486
|
-
Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts
|
|
492
|
+
Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts#RealtimeHub.unsubscribe`) appelle le
|
|
487
493
|
`dispose` du producteur et retire le canal. Un producteur fautif qui lève une exception ne
|
|
488
494
|
bloque pas le nettoyage.
|
|
489
495
|
|
|
@@ -496,24 +502,24 @@ bloque pas le nettoyage.
|
|
|
496
502
|
|
|
497
503
|
Deux méthodes qui se ressemblent et ne font pas du tout la même chose :
|
|
498
504
|
|
|
499
|
-
| Méthode
|
|
500
|
-
|
|
|
501
|
-
| `publish()` (`RealtimeHub.ts
|
|
502
|
-
| `publishLocal()` (`RealtimeHub.ts
|
|
505
|
+
| Méthode | Fan-out local | Propagation aux autres process | Qui l'appelle |
|
|
506
|
+
| ------------------------------------------------------------ | :-----------: | :--------------------------------: | ----------------------------------- |
|
|
507
|
+
| `publish()` (`RealtimeHub.ts#RealtimeHub.publish`) | oui | oui, **si** le canal est broadcast | producteurs, contrôleurs, services |
|
|
508
|
+
| `publishLocal()` (`RealtimeHub.ts#RealtimeHub.publishLocal`) | oui | **jamais** | l'arrivée d'un message du backplane |
|
|
503
509
|
|
|
504
510
|
C'est **la** règle qui empêche la tempête : un message reçu d'un pair est réinjecté
|
|
505
511
|
localement et **ne repart pas**. Le câblage se fait une fois pour toutes dans
|
|
506
|
-
`RealtimeHub.setBackplane()` (`RealtimeHub.ts
|
|
512
|
+
`RealtimeHub.setBackplane()` (`RealtimeHub.ts#RealtimeHub.setBackplane`), qui branche l'arrivée du backplane
|
|
507
513
|
directement sur `publishLocal`.
|
|
508
514
|
|
|
509
|
-
Le fan-out lui-même (`RealtimeHub.ts
|
|
515
|
+
Le fan-out lui-même (`RealtimeHub.ts#RealtimeHub.#channelPolicyPatterns`) est **isolé** : chaque livraison est protégée,
|
|
510
516
|
une connexion fautive n'interrompt pas la diffusion aux autres.
|
|
511
517
|
|
|
512
518
|
### Le forward est OPT-IN — le défaut est l'isolement
|
|
513
519
|
|
|
514
520
|
Par défaut, **aucun canal ne traverse le backplane**. Il faut déclarer un préfixe, via
|
|
515
|
-
`@RealtimeBroadcast` sur ton contrôleur (`realtimeDecorators.ts
|
|
516
|
-
directement `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts
|
|
521
|
+
`@RealtimeBroadcast` sur ton contrôleur (`realtimeDecorators.ts#RealtimeBroadcast`) ou
|
|
522
|
+
directement `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts#RealtimeHub.markBroadcastChannel`).
|
|
517
523
|
|
|
518
524
|
Trois raisons à ce choix, qui prend à contre-pied la plupart des bibliothèques temps réel :
|
|
519
525
|
|
|
@@ -549,21 +555,21 @@ coûte chaque abonné supplémentaire.
|
|
|
549
555
|
|
|
550
556
|
### Le contrat, en cinq méthodes
|
|
551
557
|
|
|
552
|
-
`IBackplane` (`IBackplane.ts
|
|
558
|
+
`IBackplane` (`IBackplane.ts#IBackplane`) est délibérément minuscule : le backplane ne connaît ni
|
|
553
559
|
les abonnés ni les canaux logiques. Tout l'état vit dans le hub.
|
|
554
560
|
|
|
555
|
-
| Membre
|
|
556
|
-
|
|
|
557
|
-
| `originId` (`IBackplane.ts
|
|
558
|
-
| `start()` (`IBackplane.ts
|
|
559
|
-
| `publish(channel, payload)` (`IBackplane.ts
|
|
560
|
-
| `onMessage(handler)` (`IBackplane.ts
|
|
561
|
-
| `stop()` (`IBackplane.ts
|
|
562
|
-
| `describe()` (`IBackplane.ts
|
|
561
|
+
| Membre | Rôle |
|
|
562
|
+
| ---------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
563
|
+
| `originId` (`IBackplane.ts#IBackplane.originId`) | l'étiquette de CE process, lue par l'anti-écho |
|
|
564
|
+
| `start()` (`IBackplane.ts#IBackplane.start`) | ouvrir le transport. Idempotent, synchrone ou asynchrone |
|
|
565
|
+
| `publish(channel, payload)` (`IBackplane.ts#IBackplane.publish`) | propager aux **autres** pairs. Ne refait pas le fan-out local |
|
|
566
|
+
| `onMessage(handler)` (`IBackplane.ts#IBackplane.onMessage`) | recevoir des pairs. Un seul gestionnaire à la fois |
|
|
567
|
+
| `stop()` (`IBackplane.ts#IBackplane.stop`) | libérer connexions et écouteurs. Idempotent |
|
|
568
|
+
| `describe()` (`IBackplane.ts#IBackplane.describe`) | la carte d'identité : driver, nature, origine, cross-pod, canal |
|
|
563
569
|
|
|
564
570
|
Deux absences volontaires. Il n'y a **pas** de `subscribe`/`unsubscribe` par canal : le
|
|
565
571
|
transport porte **un seul** canal physique, et les canaux logiques voyagent **dans**
|
|
566
|
-
l'enveloppe (`IBackplaneMessage`, `IBackplane.ts
|
|
572
|
+
l'enveloppe (`IBackplaneMessage`, `IBackplane.ts#IBackplaneMessage`). Un seul abonnement au démarrage,
|
|
567
573
|
zéro (dés)abonnement dynamique — et le hub reste seul maître de la table des canaux.
|
|
568
574
|
|
|
569
575
|
`describe()`, lui, alimente **trois sorties cohérentes** depuis une source unique : la ligne
|
|
@@ -576,11 +582,11 @@ resynchronise. Ne construis pas au-dessus une fiabilité que le support n'offre
|
|
|
576
582
|
|
|
577
583
|
### Les drivers natifs
|
|
578
584
|
|
|
579
|
-
| Driver | Classe
|
|
580
|
-
| ---------- |
|
|
581
|
-
| `loopback` | `LoopbackBackplane` (`LoopbackBackplane.ts
|
|
582
|
-
| `cluster` | `ClusterBackplane` (`ClusterBackplane.ts
|
|
583
|
-
| `redis` | `RedisBackplane` (`RedisBackplane.ts
|
|
585
|
+
| Driver | Classe | Nature | Pairs | Traverse les pods |
|
|
586
|
+
| ---------- | -------------------------------------------------------------- | -------------- | ---------------------------------- | :---------------: |
|
|
587
|
+
| `loopback` | `LoopbackBackplane` (`LoopbackBackplane.ts#LoopbackBackplane`) | `local` | aucun (no-op complet) | non |
|
|
588
|
+
| `cluster` | `ClusterBackplane` (`ClusterBackplane.ts#ClusterBackplane`) | `ipc` | les workers du même process maître | non |
|
|
589
|
+
| `redis` | `RedisBackplane` (`RedisBackplane.ts#RedisBackplane`) | `redis-pubsub` | tous les pods abonnés au canal | **oui** |
|
|
584
590
|
|
|
585
591
|
**`loopback`** est le défaut. En réalité, le hub garde son backplane à `null` en
|
|
586
592
|
mono-process : le coût est un test de nullité, pas un appel. La classe existe pour
|
|
@@ -588,30 +594,30 @@ matérialiser le contrat et prouver, en test, que brancher un backplane sans pai
|
|
|
588
594
|
rien.
|
|
589
595
|
|
|
590
596
|
**`cluster`** relie les workers d'un même hôte. Un worker Node ne peut parler qu'au maître :
|
|
591
|
-
il émet vers lui (`ClusterBackplane.publish()`, `ClusterBackplane.ts
|
|
597
|
+
il émet vers lui (`ClusterBackplane.publish()`, `ClusterBackplane.ts#ClusterBackplane.publish`), le maître
|
|
592
598
|
rediffuse aux autres. Le driver ne s'active qu'en rôle worker avec la variable
|
|
593
|
-
`NF_CLUSTER=1` (`src/packages/@nodefony/realtime/index.ts
|
|
599
|
+
`NF_CLUSTER=1` (`src/packages/@nodefony/realtime/index.ts`) ; ailleurs il rend
|
|
594
600
|
`null`. Aucune infrastructure requise — c'est le banc d'essai qui stabilise l'architecture
|
|
595
601
|
multi-process avant d'ajouter du réseau.
|
|
596
602
|
|
|
597
603
|
**`redis`** franchit la frontière de l'hôte, sans dépendre de la bibliothèque `redis` : il
|
|
598
604
|
consomme deux connexions du module `@nodefony/redis` via un adaptateur purement structurel,
|
|
599
|
-
`createRedisServiceTransport()` (`RedisBackplane.ts
|
|
605
|
+
`createRedisServiceTransport()` (`RedisBackplane.ts#createRedisServiceTransport`). Deux connexions et non une, parce
|
|
600
606
|
qu'un client Redis abonné ne peut plus émettre de commandes ordinaires. Module absent ou
|
|
601
607
|
connexions indisponibles → avertissement, `null`, hub local, démarrage poursuivi
|
|
602
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
608
|
+
(`src/packages/@nodefony/realtime/index.ts`).
|
|
603
609
|
|
|
604
610
|
### Le registre de drivers — zéro nom en dur
|
|
605
611
|
|
|
606
612
|
Choisir le driver par une cascade de `if (driver === "redis")` trahirait la promesse de
|
|
607
613
|
brancherie. La sélection passe donc par un registre — `registerBackplaneDriver()`
|
|
608
|
-
(`backplaneRegistry.ts
|
|
614
|
+
(`backplaneRegistry.ts#registerBackplaneDriver`) et `getBackplaneDriver()` (`backplaneRegistry.ts#getBackplaneDriver`) : chaque
|
|
609
615
|
driver porte son nom en membre statique, les natifs s'inscrivent au chargement du module, et
|
|
610
616
|
le câblage résout une chaîne vers une fabrique sans connaître aucun littéral
|
|
611
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
617
|
+
(`src/packages/@nodefony/realtime/index.ts#Realtime.onKernelBoot`).
|
|
612
618
|
|
|
613
619
|
Une fabrique reçoit module, `originId`, rôle dans la topologie et configuration validée
|
|
614
|
-
(`IBackplaneFactoryContext`, `backplaneRegistry.ts
|
|
620
|
+
(`IBackplaneFactoryContext`, `backplaneRegistry.ts#IBackplaneFactoryContext`), puis rend une instance ou `null`
|
|
615
621
|
(« inactif ici »). Elle ne démarre rien : c'est le câblage qui appelle `start()`.
|
|
616
622
|
|
|
617
623
|
### L'anti-écho et l'`originId`
|
|
@@ -622,9 +628,9 @@ et non une :
|
|
|
622
628
|
|
|
623
629
|
1. **Côté hub** : l'arrivée passe par `publishLocal`, jamais par `publish`. Rien ne repart.
|
|
624
630
|
2. **Côté backplane** : à la réception, on compare l'`originId` de l'enveloppe au sien et
|
|
625
|
-
on jette si c'est le même (`RedisBackplane.ts
|
|
631
|
+
on jette si c'est le même (`RedisBackplane.ts#RedisBackplane.originId`, `ClusterBackplane.ts#ClusterBackplane.#ingress`).
|
|
626
632
|
|
|
627
|
-
L'étiquette elle-même est calculée par `resolveBackplaneOriginId()` (`originId.ts
|
|
633
|
+
L'étiquette elle-même est calculée par `resolveBackplaneOriginId()` (`originId.ts#resolveBackplaneOriginId`), et
|
|
628
634
|
sa recette mérite qu'on s'y arrête :
|
|
629
635
|
|
|
630
636
|
| Étape | Valeur | Pourquoi |
|
|
@@ -643,14 +649,14 @@ sa recette mérite qu'on s'y arrête :
|
|
|
643
649
|
|
|
644
650
|
### Le cloisonnement du canal Redis
|
|
645
651
|
|
|
646
|
-
Le canal pub/sub de base est `nodefony:realtime` (`RedisBackplane.ts
|
|
652
|
+
Le canal pub/sub de base est `nodefony:realtime` (`RedisBackplane.ts#REDIS_RT_CHANNEL`). Problème : le
|
|
647
653
|
numéro de base Redis **ne cloisonne pas** le pub/sub, qui est global au serveur. Deux
|
|
648
654
|
applications sur un Redis mutualisé se parleraient.
|
|
649
655
|
|
|
650
|
-
D'où le suffixe de cloison, calculé par `resolveRedisChannel()` (`RedisBackplane.ts
|
|
656
|
+
D'où le suffixe de cloison, calculé par `resolveRedisChannel()` (`RedisBackplane.ts#resolveRedisChannel`) :
|
|
651
657
|
le canal effectif devient `nodefony:realtime:<namespace>`. Le namespace vient de la
|
|
652
658
|
configuration ; à défaut, il est dérivé du nom de l'application
|
|
653
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
659
|
+
(`src/packages/@nodefony/realtime/index.ts`).
|
|
654
660
|
|
|
655
661
|
> [!TIP]
|
|
656
662
|
> Deux déploiements de la **même** application sur un Redis partagé (préproduction et
|
|
@@ -658,19 +664,19 @@ configuration ; à défaut, il est dérivé du nom de l'application
|
|
|
658
664
|
> namespace **explicite et distinct** dans ces cas-là.
|
|
659
665
|
|
|
660
666
|
L'entrée est par ailleurs blindée : un JSON malformé publié par un tiers sur le même canal
|
|
661
|
-
est ignoré, pas propagé (`RedisBackplane.ts
|
|
667
|
+
est ignoré, pas propagé (`RedisBackplane.ts#RedisBackplane`).
|
|
662
668
|
|
|
663
669
|
### Le démarrage, et ce qui se passe quand ça rate
|
|
664
670
|
|
|
665
671
|
Le module valide sa configuration par Zod à l'enregistrement
|
|
666
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
667
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
672
|
+
(`src/packages/@nodefony/realtime/index.ts#Realtime.onKernelRegister`), puis câble tout au démarrage du noyau
|
|
673
|
+
(`src/packages/@nodefony/realtime/index.ts#Realtime.onKernelRegister`) — après l'initialisation des services (le
|
|
668
674
|
service Redis a donc ouvert ses connexions) et avant le trafic.
|
|
669
675
|
|
|
670
676
|
Le `start()` du backplane est **attendu explicitement avant** d'être branché : un driver
|
|
671
677
|
asynchrone brancherait sinon un abonnement incomplet et perdrait ses premiers messages. Et
|
|
672
678
|
il est **borné par un délai** de cinq secondes
|
|
673
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
679
|
+
(`src/packages/@nodefony/realtime/index.ts#BACKPLANE_START_TIMEOUT_MS`), pour une raison très concrète : un
|
|
674
680
|
transport réseau qui reste pendu gèlerait la phase de démarrage, et donc la montée des
|
|
675
681
|
serveurs HTTP eux-mêmes.
|
|
676
682
|
|
|
@@ -682,7 +688,7 @@ Trois issues, toutes annoncées, jamais silencieuses :
|
|
|
682
688
|
| driver inactif ici (loopback, mauvais rôle) | hub local | `INFO` |
|
|
683
689
|
| driver inconnu, ou démarrage en échec/expiré | hub local, **le démarrage continue** | `WARNING` |
|
|
684
690
|
|
|
685
|
-
Le module se déclare non critique (`src/packages/@nodefony/realtime/index.ts
|
|
691
|
+
Le module se déclare non critique (`src/packages/@nodefony/realtime/index.ts`) : son
|
|
686
692
|
échec ne tue jamais le process. C'est de la résilience **sans dégradation silencieuse** —
|
|
687
693
|
tout repli est annoncé. Enfin, `enabled: false` rend le module totalement inerte : ni API
|
|
688
694
|
d'administration, ni backplane, ni sonde.
|
|
@@ -712,18 +718,18 @@ registerBackplaneDriver(
|
|
|
712
718
|
|
|
713
719
|
**2. Déclarer un service** nommé `realtimeBackplane` dans le conteneur d'injection —
|
|
714
720
|
**la voie « instance » pour une application**. `RealtimeService.init()`
|
|
715
|
-
(`RealtimeService.ts
|
|
721
|
+
(`RealtimeService.ts#RealtimeService.init`) le résout et le branche **avant** le registre : une instance
|
|
716
722
|
déjà fournie court-circuite la sélection par nom.
|
|
717
723
|
|
|
718
724
|
**3. Passer une instance** en second argument de `defineRealtimeConfig()` — réservé au
|
|
719
725
|
code qui **appelle le builder lui-même** (un `Module` sur mesure, une intégration
|
|
720
726
|
programmatique). Une application qui configure via `use("@nodefony/realtime", …)` **ne
|
|
721
727
|
l'atteint pas** : elle ne construit pas la config (le module appelle
|
|
722
|
-
`defineRealtimeConfig(this.options)` sans second argument, `index.ts
|
|
728
|
+
`defineRealtimeConfig(this.options)` sans second argument, `index.ts#createRealtimeHarness`), et une
|
|
723
729
|
instance posée dans l'objet de config est refusée par la validation Zod (clé inconnue du
|
|
724
730
|
schéma strict). Pour une app, la voie « instance » est l'option 2 ci-dessus.
|
|
725
731
|
|
|
726
|
-
Le registre s'introspecte : `listBackplaneDrivers()` (`backplaneRegistry.ts
|
|
732
|
+
Le registre s'introspecte : `listBackplaneDrivers()` (`backplaneRegistry.ts#listBackplaneDrivers`) rend les
|
|
727
733
|
noms disponibles — c'est ce que le message d'erreur affiche quand un driver déclaré est
|
|
728
734
|
introuvable.
|
|
729
735
|
|
|
@@ -733,31 +739,31 @@ L'architecture prévoit des points de greffe pour que `@nodefony/security` s'y b
|
|
|
733
739
|
refonte. Ici, seulement **où** ils sont ; le **quoi** et le **comment** sont dans
|
|
734
740
|
[la page sécurité](./securite.md).
|
|
735
741
|
|
|
736
|
-
| Point de greffe | Étage | Moment | Ancrage
|
|
737
|
-
| ---------------------------- | --------- | -------------- |
|
|
738
|
-
| Contrôle d'origine | hub | à l'upgrade | `RealtimeHub.ts
|
|
739
|
-
| Authentificateur réseau | hub | au handshake | `RealtimeHub.ts
|
|
740
|
-
| Verrou de frame | protocole | à chaque frame | `RealtimeHub.ts
|
|
741
|
-
| Politique déclarée par canal | hub | au handshake | `RealtimeHub.ts
|
|
742
|
-
| Audit de frame | protocole | sur refus | `RealtimeController.ts
|
|
742
|
+
| Point de greffe | Étage | Moment | Ancrage |
|
|
743
|
+
| ---------------------------- | --------- | -------------- | ------------------------------------------------------ |
|
|
744
|
+
| Contrôle d'origine | hub | à l'upgrade | `RealtimeHub.ts#RealtimeHub.publishLocal` |
|
|
745
|
+
| Authentificateur réseau | hub | au handshake | `RealtimeHub.ts#RealtimeHub.unsubscribe` |
|
|
746
|
+
| Verrou de frame | protocole | à chaque frame | `RealtimeHub.ts#RealtimeHub.markBroadcastChannel` |
|
|
747
|
+
| Politique déclarée par canal | hub | au handshake | `RealtimeHub.ts#RealtimeHub.setBackplane` |
|
|
748
|
+
| Audit de frame | protocole | sur refus | `RealtimeController.ts#RealtimeController.onHandshake` |
|
|
743
749
|
|
|
744
750
|
Deux propriétés architecturales méritent d'être notées ici, parce qu'elles expliquent des
|
|
745
751
|
choix de conception visibles partout dans le module :
|
|
746
752
|
|
|
747
753
|
**Coût nul quand la sécurité est absente.** Le verrou de frame n'est branché sur le peer que
|
|
748
|
-
si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts
|
|
754
|
+
si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts#RealtimeHub.hasFrameAuthorizer`, testé une
|
|
749
755
|
fois au handshake). Sans module de sécurité, `beforeDispatch` reste indéfini et le chemin
|
|
750
756
|
chaud ne paie **rien** du tout.
|
|
751
757
|
|
|
752
758
|
**Échec bruyant plutôt que faux sentiment de sécurité.** Si des canaux déclarent une
|
|
753
759
|
politique sans qu'aucun décideur ne soit câblé, `hasUnenforcedChannelPolicies()`
|
|
754
|
-
(`RealtimeHub.ts
|
|
755
|
-
(`RealtimeController.ts
|
|
760
|
+
(`RealtimeHub.ts#RealtimeHub.hasUnenforcedChannelPolicies`) le détecte et un avertissement est émis une fois par process
|
|
761
|
+
(`RealtimeController.ts#RealtimeController.onHandshake`). Un canal qui **se croit** gardé alors qu'il est ouvert est
|
|
756
762
|
bien plus dangereux qu'un canal ouvertement public.
|
|
757
763
|
|
|
758
764
|
## 📡 Observabilité — la sonde et Studio
|
|
759
765
|
|
|
760
|
-
`RealtimeHub.probe()` (`RealtimeHub.ts
|
|
766
|
+
`RealtimeHub.probe()` (`RealtimeHub.ts#RealtimeHub.probe`) rend un instantané en **lecture pure** : aucune
|
|
761
767
|
allocation sur le chemin chaud, jamais d'exception. Les compteurs sont des primitives
|
|
762
768
|
incrémentées en O(1) ; ils sont **monotones**, ce qui laisse au lecteur le soin de dériver
|
|
763
769
|
un débit.
|
|
@@ -770,35 +776,35 @@ un débit.
|
|
|
770
776
|
| Back-pressure | `maxBufferedAmount`, `totalBufferedAmount`, `slowConsumers`, `drops` | le risque mémoire numéro un |
|
|
771
777
|
| Fond de panier | `backplane` (la carte d'identité de `describe()`) | ce qui est réellement branché |
|
|
772
778
|
|
|
773
|
-
Le contrat complet est `IRealtimeProbe` (`IRealtimeProbe.ts
|
|
774
|
-
`buildOwnHealth()` (`RealtimeAdminApi.ts
|
|
779
|
+
Le contrat complet est `IRealtimeProbe` (`IRealtimeProbe.ts#IRealtimeProbe`). La version enrichie
|
|
780
|
+
`buildOwnHealth()` (`RealtimeAdminApi.ts#buildOwnHealth`) y ajoute l'identifiant d'instance et la sonde
|
|
775
781
|
process, plus — si elles sont disponibles — les sondes ORM et les compteurs d'erreurs. Deux
|
|
776
782
|
chemins de lecture, un seul producteur :
|
|
777
783
|
|
|
778
784
|
- **HTTP** : `GET /nodefony/realtime/api/health`, exposé par `createRealtimeAdminApi()`
|
|
779
|
-
(`RealtimeAdminApi.ts
|
|
785
|
+
(`RealtimeAdminApi.ts#createRealtimeAdminApi`) ;
|
|
780
786
|
- **Canal temps réel** : `nodefony:socket`, poussé par un ticker.
|
|
781
787
|
|
|
782
788
|
### En cluster : la vue « pod » plutôt que « worker »
|
|
783
789
|
|
|
784
790
|
Interroger la santé en cluster pose un problème sournois : la requête atterrit sur **un**
|
|
785
791
|
worker au hasard, dont l'instantané ne dit rien des autres. D'où `ClusterProbeClient`
|
|
786
|
-
(`ClusterProbeClient.ts
|
|
787
|
-
périodiquement sa santé au maître (`ClusterProbeClient.ts
|
|
792
|
+
(`ClusterProbeClient.ts#ClusterProbeClient`), qui applique un modèle **push** — chaque worker remonte
|
|
793
|
+
périodiquement sa santé au maître (`ClusterProbeClient.ts#ClusterProbeClient.constructor`), le maître agrège et
|
|
788
794
|
rediffuse, chaque worker met en cache. **N'importe lequel** sert alors la vue pod en O(1),
|
|
789
795
|
sans latence de requête.
|
|
790
796
|
|
|
791
|
-
L'agrégation, `mergeClusterHealth()` (`ClusterProbeClient.ts
|
|
797
|
+
L'agrégation, `mergeClusterHealth()` (`ClusterProbeClient.ts#mergeClusterHealth`), est une fonction pure :
|
|
792
798
|
elle somme les scalaires, mais prend le **maximum** de `maxBufferedAmount` — la santé d'une
|
|
793
799
|
flotte se juge sur son pire membre, pas sur sa moyenne. `buildRealtimeHealth()`
|
|
794
|
-
(`RealtimeAdminApi.ts
|
|
800
|
+
(`RealtimeAdminApi.ts#buildRealtimeHealth`) choisit ensuite la vue agrégée si elle existe, la per-instance
|
|
795
801
|
sinon.
|
|
796
802
|
|
|
797
803
|
Sonde désactivée ? Elle n'est pas muette : **elle n'existe pas**. Aucun client, donc aucun
|
|
798
804
|
minuteur, aucun écouteur, aucun message IPC
|
|
799
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
805
|
+
(`src/packages/@nodefony/realtime/index.ts#Realtime.#startWithTimeout`) — coupable par la configuration ou par une
|
|
800
806
|
variable d'environnement. Enfin, le maître peut demander à un worker précis d'enrichir sa
|
|
801
|
-
remontée (`requestEnrich()`, `ClusterProbeClient.ts
|
|
807
|
+
remontée (`requestEnrich()`, `ClusterProbeClient.ts#ClusterProbeClient.requestEnrich`) : la sonde riche n'est allouée que
|
|
802
808
|
pendant l'inspection, puis libérée — on paie ce qu'on regarde.
|
|
803
809
|
|
|
804
810
|
## ⚡ Performance & mémoire
|
|
@@ -807,10 +813,10 @@ Le module suit une discipline stricte, lisible dans presque chaque champ privé
|
|
|
807
813
|
|
|
808
814
|
**Allocation paresseuse partout.** Table des canaux, registre des connexions, registre des
|
|
809
815
|
révocables, authentificateurs, politiques de canal, association peer→jeton : tout démarre à
|
|
810
|
-
`null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts
|
|
816
|
+
`null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts#IRevocableConnection` et suivants).
|
|
811
817
|
|
|
812
818
|
**Aucun minuteur au repos.** Le tick de révocation démarre au premier inscrit et s'arrête
|
|
813
|
-
dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts
|
|
819
|
+
dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts#RealtimeHub.unregisterRevocable`).
|
|
814
820
|
Les minuteurs sont détachés de la boucle d'événements : ils ne retiennent jamais l'arrêt du
|
|
815
821
|
process.
|
|
816
822
|
|