@nodefony/realtime 10.0.0-beta.2 → 10.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +2 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/index.js +18 -13
  4. package/dist/nodefony/config/config.js +7 -5
  5. package/dist/nodefony/config/configSchema.js +18 -0
  6. package/dist/nodefony/config/defineModuleConfig.js +43 -17
  7. package/dist/nodefony/decorators/realtimeDecorators.js +69 -19
  8. package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
  9. package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
  10. package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
  11. package/dist/nodefony/src/backplane/RedisBackplane.js +22 -6
  12. package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
  13. package/dist/nodefony/src/backplane/bootNotices.js +42 -0
  14. package/dist/nodefony/src/backplane/envelope.js +1 -2
  15. package/dist/nodefony/src/backplane/originId.js +1 -2
  16. package/dist/nodefony/src/backplane/publishQueue.js +1 -5
  17. package/dist/nodefony/src/cluster/ClusterProbeClient.js +16 -6
  18. package/dist/nodefony/src/guards.js +40 -0
  19. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +2 -6
  20. package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
  21. package/dist/nodefony/src/server/RealtimeController.js +226 -82
  22. package/dist/nodefony/src/server/RealtimeHub.js +32 -54
  23. package/dist/nodefony/src/server/ServerRealtimeSocket.js +30 -13
  24. package/dist/nodefony/src/server/deniedDetail.js +1 -2
  25. package/dist/nodefony/src/server/subscribedAck.js +29 -0
  26. package/dist/nodefony/src/server/syslogUplink.js +5 -4
  27. package/dist/nodefony/src/server/welcomeEnv.js +1 -2
  28. package/dist/nodefony/src/service/RealtimeService.js +4 -4
  29. package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
  30. package/dist/nodefony/testing/index.js +118 -5
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/nodefony/config/configSchema.d.ts +12 -0
  33. package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
  34. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
  35. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
  36. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
  37. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
  38. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
  39. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  40. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
  41. package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
  42. package/dist/types/nodefony/src/guards.d.ts +32 -0
  43. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
  44. package/dist/types/nodefony/src/server/RealtimeController.d.ts +82 -4
  45. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
  46. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
  47. package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
  48. package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
  49. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
  50. package/dist/types/nodefony/testing/index.d.ts +8 -3
  51. package/docs/actions.md +129 -45
  52. package/docs/architecture.md +143 -135
  53. package/docs/cadence-et-contre-pression.md +643 -0
  54. package/docs/configuration.md +126 -97
  55. package/docs/cookbook-chat.md +50 -43
  56. package/docs/index.md +63 -38
  57. package/docs/observabilite.md +58 -56
  58. package/docs/protocole.md +110 -76
  59. package/docs/securite.md +128 -117
  60. package/docs/vocabulaire.md +133 -129
  61. package/package.json +17 -13
  62. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
  63. package/docs/couverture-bancs.md +0 -139
@@ -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:271`) est du code isomorphe du cœur — le navigateur
128
- l'exécute dans `RealtimeClient`, le serveur l'instancie une fois par connexion dans
129
- `RealtimeController.onHandshake()` (`RealtimeController.ts:374`). Le serveur peut donc
127
+ classe `JsonRpcPeer` (`JsonRpcPeer.ts#JsonRpcPeer`) est du code isomorphe du cœur — le navigateur
128
+ l'exécute dans `NodefonySocket`, le serveur l'instancie une fois par connexion dans
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:43`), obtenu par `serverSocket()`
140
- (`ServerRealtimeSocket.ts:223`) ; côté navigateur, `RealtimeClient`. Une exception, assumée
141
- et explicite : `ServerRealtimeSocket.request()` (`ServerRealtimeSocket.ts:131`) **rejette
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:329`).
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:462`) : le
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:604`) — un choix de sûreté détaillé plus bas.
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
 
@@ -260,7 +266,7 @@ export default ChatController;
260
266
 
261
267
  ```ts
262
268
  // frontend/src/chat.ts
263
- import { RealtimeClient } from "nodefony/client";
269
+ import { NodefonySocket } from "nodefony/client";
264
270
 
265
271
  interface ChatMessage {
266
272
  from: string;
@@ -271,7 +277,7 @@ interface ChatMessage {
271
277
  const scheme = location.protocol === "https:" ? "wss" : "ws";
272
278
  // `shared` = une seule socket par URL pour toute la page, même si dix
273
279
  // composants la demandent (les options ne comptent qu'à la création).
274
- const socket = RealtimeClient.shared({
280
+ const socket = NodefonySocket.shared({
275
281
  url: `${scheme}://${location.host}/chat/realtime`,
276
282
  });
277
283
 
@@ -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:303`) :
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 | Sa seule responsabilité | Ce qu'il ignore |
323
- | ---------- | ------------------------------------------------------- | --------------------------------------- | ----------------------------- |
324
- | Applicatif | ton contrôleur, tes services | le métier : quoi publier, quoi accepter | tout le reste |
325
- | Protocole | `JsonRpcPeer` (`JsonRpcPeer.ts:271`) | étiqueter, corréler, refuser une frame | par où passent les octets |
326
- | Transport | `WsConnectionTransport` (`WsConnectionTransport.ts:46`) | déplacer des octets, mesurer la file | ce que veut dire un message |
327
- | Hub | `RealtimeHub` (`RealtimeHub.ts:213`) | table des abonnés locaux + fan-out | qu'il existe d'autres process |
328
- | Backplane | `IBackplane` (`IBackplane.ts:107`) | porter un message aux autres process | ce qu'est un abonné |
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:192`) — un verrou **synchrone** appelé avant tout
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:413`).
345
- - `onFrameAudit` (`JsonRpcPeer.ts:210`) — la trace des événements protocolaires notables
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,28 +361,30 @@ 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:150`). La fermeture arrive de la
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:155`).
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:63-75`) et documentées dans `@nodefony/http`
365
- (`http/nodefony/config/config.ts:1044`).
370
+ (`WsConnectionTransport.ts#WsConnectionTransport`) et documentées dans `@nodefony/http`
371
+ (`http/nodefony/config/config.ts#httpConfigSchema`).
366
372
 
367
- | File non drainée (`bufferedAmount`) | Décision | Réglage (défaut) |
368
- | --------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- |
369
- | sous le seuil | envoi normal | — |
370
- | au-dessus du seuil | politique appliquée : frame **jetée**, ou fermeture | `websocket.maxBackpressure` (**4 MiB**) · `websocket.backpressurePolicy` (**`drop`**) |
371
- | drops **consécutifs** au-delà du compte | fermeture `1013` (_Try Again Later_) | `websocket.backpressureCloseAfterDrops` (**1000**) |
373
+ | File non drainée (`bufferedAmount`) | Décision | Réglage (défaut) |
374
+ | ----------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- |
375
+ | sous le seuil | envoi normal | — |
376
+ | au-dessus du seuil | politique appliquée : frame **jetée**, ou fermeture | `websocket.maxBackpressure` (**4 MiB**) · `websocket.backpressurePolicy` (**`drop`**) |
377
+ | **solde** de refus atteint | fermeture `1013` (_Try Again Later_) | `websocket.backpressureCloseAfterDrops` (**1000**) |
372
378
 
373
379
  Deux points que la formulation « deux seuils d'octets » faisait manquer : la fermeture ne
374
- se déclenche pas sur un volume mais sur une **suite de frames jetées** — une seule frame
375
- qui repart remet le compteur à zéro — et `drop` est un choix, `close` fermant dès le
376
- premier dépassement. Jeter est acceptable parce que les canaux d'état sont « le dernier
377
- gagne » : le prochain instantané remplace celui qu'on a sauté.
378
-
379
- À ne pas confondre avec `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:63`, 1 MiB) : ce seuil-là
380
+ se déclenche pas sur un volume mais sur un **solde de frames jetées** — +1 par refus, −1 par
381
+ envoi, si bien qu'un pic passager redescend et qu'un client qui n'absorbe plus finit coupé — et
382
+ `drop` est un choix, `close` fermant dès le premier dépassement. Jeter est sans conséquence sur
383
+ un canal d'état, où le prochain instantané remplace celui qu'on a sauté ; mais le rejet ne trie
384
+ rien, et une réponse RPC ou un événement jetés sont perdus. Détail et courbes :
385
+ [Cadence et contre-pression](./cadence-et-contre-pression.md).
386
+
387
+ À ne pas confondre avec `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts#SLOW_CONSUMER_BYTES`, 1 MiB) : ce seuil-là
380
388
  ne jette rien, il **compte** — c'est celui à partir duquel la sonde marque une connexion
381
389
  `slowConsumer`.
382
390
 
@@ -388,7 +396,7 @@ Le schéma ci-dessous rend ces étages vivants : active le temps réel et il res
388
396
 
389
397
  ## 🔌 Le cycle de vie d'une connexion
390
398
 
391
- Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:374`), appelé
399
+ Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts#RealtimeController.onHandshake`), appelé
392
400
  une seule fois par connexion, en chemin froid.
393
401
 
394
402
  ```mermaid
@@ -419,19 +427,19 @@ sequenceDiagram
419
427
 
420
428
  Les étapes, dans l'ordre exact du code :
421
429
 
422
- 1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:986`). Refus →
430
+ 1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts#RealtimeHub.checkOrigin`). Refus →
423
431
  fermeture `4003`. La politique vient de la configuration ; sans politique, tout passe.
424
432
  2. **Résolution de l'identité** — `RealtimeHub.resolveAuthenticator()`
425
- (`RealtimeHub.ts:961`) parcourt les authentificateurs enregistrés : **le premier motif
433
+ (`RealtimeHub.ts#RealtimeHub.resolveAuthenticator`) parcourt les authentificateurs enregistrés : **le premier motif
426
434
  qui correspond capture**. Aucun ne correspond ? Le jeton anonyme gelé est posé
427
435
  (`ANONYMOUS_REALTIME_TOKEN`) — la lecture d'identité ne rend donc jamais `null`. Un
428
436
  échec d'authentification ferme en `4001`.
429
437
  3. **Création du peer et du transport**, puis association `peer → jeton`
430
- (`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:1018`), stockée dans une `WeakMap` : le
438
+ (`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts#RealtimeHub.setTokenForPeer`), stockée dans une `WeakMap` : le
431
439
  jeton disparaît avec le peer, sans fuite.
432
440
  4. **Enregistrement des actions** — celles des décorateurs `@RealtimeAction`, puis celles
433
441
  de la surcharge `realtimeActions()`, qui gagne en cas de conflit. Le pont API
434
- `api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts:276`)
442
+ `api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts#RealtimeController.realtimeApiRequest`)
435
443
  rend `true`.
436
444
  5. **Inscription aux registres** : sonde de connexion, révocation périodique si le jeton
437
445
  est révocable, préfixes de canaux broadcast, canaux entrants.
@@ -439,7 +447,7 @@ Les étapes, dans l'ordre exact du code :
439
447
  l'identité résolue (type, authentifié ou non, rôles, portées). Le client sait **qui il
440
448
  est** sans appeler la moindre route.
441
449
 
442
- À la fermeture, un unique `onFinish` (`RealtimeController.ts:715`) fait le ménage complet :
450
+ À la fermeture, un unique `onFinish` (`RealtimeController.ts#RealtimeController.onHandshake`) fait le ménage complet :
443
451
  désabonnement de chaque canal tenu, retrait des deux registres, `fireClose()` du transport,
444
452
  `dispose()` du peer. C'est ce qui garantit qu'aucun minuteur ni écouteur ne survit à une
445
453
  déconnexion.
@@ -447,41 +455,41 @@ déconnexion.
447
455
  > [!IMPORTANT]
448
456
  > Les frames reçues **pendant** l'authentification sont jetées silencieusement : le
449
457
  > transport n'est pas encore branché. C'est au client d'attendre `realtime:welcome` avant
450
- > de pousser — ce que `RealtimeClient` fait nativement.
458
+ > de pousser — ce que `NodefonySocket` fait nativement.
451
459
 
452
460
  ### Deux gardes qui ferment la connexion
453
461
 
454
- | Garde | Déclencheur | Effet | Ancrage |
455
- | --------------------- | ---------------------------------------------------------------------- | --------------------------------------------------- | --------------------------- |
456
- | Plafond de canaux | une connexion dépasse son quota d'abonnements (256 par défaut) | abonnement refusé + `realtime:denied` motif `limit` | `RealtimeController.ts:618` |
457
- | Révocation d'identité | tick périodique ; `token.isValid()` rend `false` ou lève une exception | fermeture `4001` « session revoked » | `RealtimeHub.ts:48` |
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` |
458
466
 
459
467
  La seconde mérite une explication. Le verrou de frame est synchrone et lit une identité
460
468
  figée au handshake : il ne peut donc pas voir une session qui meurt en cours de route (une
461
469
  déconnexion HTTP, par exemple). Un minuteur — démarré au premier inscrit, arrêté dès que le
462
- registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:794`) — relit
470
+ registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts#RealtimeHub.registerRevocable`) — relit
463
471
  périodiquement ces identités et coupe les sockets orphelines. Seules les identités
464
472
  **révocables** y entrent : un visiteur anonyme ne coûte rien.
465
473
 
466
474
  ## Le hub — canaux partagés et fan-out
467
475
 
468
- Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1451`). Il ne
476
+ Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts#getRealtimeHub`). Il ne
469
477
  dépend de rien : ce sont les fabriques fournies par les contrôleurs qui portent les
470
478
  dépendances.
471
479
 
472
480
  ### Le provider partagé — un ticker, pas mille
473
481
 
474
- `RealtimeHub.subscribe()` (`RealtimeHub.ts:462`) applique une mécanique en trois temps :
482
+ `RealtimeHub.subscribe()` (`RealtimeHub.ts#RealtimeHub.subscribe`) applique une mécanique en trois temps :
475
483
 
476
484
  1. Le canal existe déjà ? On ajoute simplement le sink de cette connexion. Fin.
477
485
  2. Sinon, on inscrit le sink **avant** d'appeler la fabrique — de sorte que le tout premier
478
486
  paquet du producteur atteigne bien ce premier abonné.
479
487
  3. La fabrique du contrôleur rend `null` (canal inconnu de lui) ? Dernier recours : le
480
488
  registre des **canaux système** (`RealtimeHub.registerSystemChannel()`,
481
- `RealtimeHub.ts:1297`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
489
+ `RealtimeHub.ts#RealtimeHub.registerSystemChannel`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
482
490
  l'abonnement est refusé et rien n'est alloué.
483
491
 
484
- Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts:576`) appelle le
492
+ Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts#RealtimeHub.unsubscribe`) appelle le
485
493
  `dispose` du producteur et retire le canal. Un producteur fautif qui lève une exception ne
486
494
  bloque pas le nettoyage.
487
495
 
@@ -494,24 +502,24 @@ bloque pas le nettoyage.
494
502
 
495
503
  Deux méthodes qui se ressemblent et ne font pas du tout la même chose :
496
504
 
497
- | Méthode | Fan-out local | Propagation aux autres process | Qui l'appelle |
498
- | --------------------------------------- | :-----------: | :--------------------------------: | ----------------------------------- |
499
- | `publish()` (`RealtimeHub.ts:604`) | oui | oui, **si** le canal est broadcast | producteurs, contrôleurs, services |
500
- | `publishLocal()` (`RealtimeHub.ts:623`) | oui | **jamais** | l'arrivée d'un message du backplane |
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 |
501
509
 
502
510
  C'est **la** règle qui empêche la tempête : un message reçu d'un pair est réinjecté
503
511
  localement et **ne repart pas**. Le câblage se fait une fois pour toutes dans
504
- `RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`), qui branche l'arrivée du backplane
512
+ `RealtimeHub.setBackplane()` (`RealtimeHub.ts#RealtimeHub.setBackplane`), qui branche l'arrivée du backplane
505
513
  directement sur `publishLocal`.
506
514
 
507
- Le fan-out lui-même (`RealtimeHub.ts:340`) est **isolé** : chaque livraison est protégée,
515
+ Le fan-out lui-même (`RealtimeHub.ts#RealtimeHub.#channelPolicyPatterns`) est **isolé** : chaque livraison est protégée,
508
516
  une connexion fautive n'interrompt pas la diffusion aux autres.
509
517
 
510
518
  ### Le forward est OPT-IN — le défaut est l'isolement
511
519
 
512
520
  Par défaut, **aucun canal ne traverse le backplane**. Il faut déclarer un préfixe, via
513
- `@RealtimeBroadcast` sur ton contrôleur (`realtimeDecorators.ts:342`) ou
514
- directement `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:668`).
521
+ `@RealtimeBroadcast` sur ton contrôleur (`realtimeDecorators.ts#RealtimeBroadcast`) ou
522
+ directement `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts#RealtimeHub.markBroadcastChannel`).
515
523
 
516
524
  Trois raisons à ce choix, qui prend à contre-pied la plupart des bibliothèques temps réel :
517
525
 
@@ -547,21 +555,21 @@ coûte chaque abonné supplémentaire.
547
555
 
548
556
  ### Le contrat, en cinq méthodes
549
557
 
550
- `IBackplane` (`IBackplane.ts:107`) est délibérément minuscule : le backplane ne connaît ni
558
+ `IBackplane` (`IBackplane.ts#IBackplane`) est délibérément minuscule : le backplane ne connaît ni
551
559
  les abonnés ni les canaux logiques. Tout l'état vit dans le hub.
552
560
 
553
- | Membre | Rôle |
554
- | ------------------------------------------------- | --------------------------------------------------------------- |
555
- | `originId` (`IBackplane.ts:109`) | l'étiquette de CE process, lue par l'anti-écho |
556
- | `start()` (`IBackplane.ts:115`) | ouvrir le transport. Idempotent, synchrone ou asynchrone |
557
- | `publish(channel, payload)` (`IBackplane.ts:121`) | propager aux **autres** pairs. Ne refait pas le fan-out local |
558
- | `onMessage(handler)` (`IBackplane.ts:127`) | recevoir des pairs. Un seul gestionnaire à la fois |
559
- | `stop()` (`IBackplane.ts:130`) | libérer connexions et écouteurs. Idempotent |
560
- | `describe()` (`IBackplane.ts:136`) | la carte d'identité : driver, nature, origine, cross-pod, canal |
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 |
561
569
 
562
570
  Deux absences volontaires. Il n'y a **pas** de `subscribe`/`unsubscribe` par canal : le
563
571
  transport porte **un seul** canal physique, et les canaux logiques voyagent **dans**
564
- l'enveloppe (`IBackplaneMessage`, `IBackplane.ts:51`). Un seul abonnement au démarrage,
572
+ l'enveloppe (`IBackplaneMessage`, `IBackplane.ts#IBackplaneMessage`). Un seul abonnement au démarrage,
565
573
  zéro (dés)abonnement dynamique — et le hub reste seul maître de la table des canaux.
566
574
 
567
575
  `describe()`, lui, alimente **trois sorties cohérentes** depuis une source unique : la ligne
@@ -574,11 +582,11 @@ resynchronise. Ne construis pas au-dessus une fiabilité que le support n'offre
574
582
 
575
583
  ### Les drivers natifs
576
584
 
577
- | Driver | Classe | Nature | Pairs | Traverse les pods |
578
- | ---------- | ----------------------------------------------- | -------------- | ---------------------------------- | :---------------: |
579
- | `loopback` | `LoopbackBackplane` (`LoopbackBackplane.ts:24`) | `local` | aucun (no-op complet) | non |
580
- | `cluster` | `ClusterBackplane` (`ClusterBackplane.ts:89`) | `ipc` | les workers du même process maître | non |
581
- | `redis` | `RedisBackplane` (`RedisBackplane.ts:161`) | `redis-pubsub` | tous les pods abonnés au canal | **oui** |
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** |
582
590
 
583
591
  **`loopback`** est le défaut. En réalité, le hub garde son backplane à `null` en
584
592
  mono-process : le coût est un test de nullité, pas un appel. La classe existe pour
@@ -586,30 +594,30 @@ matérialiser le contrat et prouver, en test, que brancher un backplane sans pai
586
594
  rien.
587
595
 
588
596
  **`cluster`** relie les workers d'un même hôte. Un worker Node ne peut parler qu'au maître :
589
- il émet vers lui (`ClusterBackplane.publish()`, `ClusterBackplane.ts:117`), le maître
597
+ il émet vers lui (`ClusterBackplane.publish()`, `ClusterBackplane.ts#ClusterBackplane.publish`), le maître
590
598
  rediffuse aux autres. Le driver ne s'active qu'en rôle worker avec la variable
591
- `NF_CLUSTER=1` (`src/packages/@nodefony/realtime/index.ts:99`) ; ailleurs il rend
599
+ `NF_CLUSTER=1` (`src/packages/@nodefony/realtime/index.ts`) ; ailleurs il rend
592
600
  `null`. Aucune infrastructure requise — c'est le banc d'essai qui stabilise l'architecture
593
601
  multi-process avant d'ajouter du réseau.
594
602
 
595
603
  **`redis`** franchit la frontière de l'hôte, sans dépendre de la bibliothèque `redis` : il
596
604
  consomme deux connexions du module `@nodefony/redis` via un adaptateur purement structurel,
597
- `createRedisServiceTransport()` (`RedisBackplane.ts:100`). Deux connexions et non une, parce
605
+ `createRedisServiceTransport()` (`RedisBackplane.ts#createRedisServiceTransport`). Deux connexions et non une, parce
598
606
  qu'un client Redis abonné ne peut plus émettre de commandes ordinaires. Module absent ou
599
607
  connexions indisponibles → avertissement, `null`, hub local, démarrage poursuivi
600
- (`src/packages/@nodefony/realtime/index.ts:108`).
608
+ (`src/packages/@nodefony/realtime/index.ts`).
601
609
 
602
610
  ### Le registre de drivers — zéro nom en dur
603
611
 
604
612
  Choisir le driver par une cascade de `if (driver === "redis")` trahirait la promesse de
605
613
  brancherie. La sélection passe donc par un registre — `registerBackplaneDriver()`
606
- (`backplaneRegistry.ts:55`) et `getBackplaneDriver()` (`backplaneRegistry.ts:63`) : chaque
614
+ (`backplaneRegistry.ts#registerBackplaneDriver`) et `getBackplaneDriver()` (`backplaneRegistry.ts#getBackplaneDriver`) : chaque
607
615
  driver porte son nom en membre statique, les natifs s'inscrivent au chargement du module, et
608
616
  le câblage résout une chaîne vers une fabrique sans connaître aucun littéral
609
- (`src/packages/@nodefony/realtime/index.ts:253`).
617
+ (`src/packages/@nodefony/realtime/index.ts#Realtime.onKernelBoot`).
610
618
 
611
619
  Une fabrique reçoit module, `originId`, rôle dans la topologie et configuration validée
612
- (`IBackplaneFactoryContext`, `backplaneRegistry.ts:27`), puis rend une instance ou `null`
620
+ (`IBackplaneFactoryContext`, `backplaneRegistry.ts#IBackplaneFactoryContext`), puis rend une instance ou `null`
613
621
  (« inactif ici »). Elle ne démarre rien : c'est le câblage qui appelle `start()`.
614
622
 
615
623
  ### L'anti-écho et l'`originId`
@@ -620,9 +628,9 @@ et non une :
620
628
 
621
629
  1. **Côté hub** : l'arrivée passe par `publishLocal`, jamais par `publish`. Rien ne repart.
622
630
  2. **Côté backplane** : à la réception, on compare l'`originId` de l'enveloppe au sien et
623
- on jette si c'est le même (`RedisBackplane.ts:209`, `ClusterBackplane.ts:134`).
631
+ on jette si c'est le même (`RedisBackplane.ts#RedisBackplane.originId`, `ClusterBackplane.ts#ClusterBackplane.#ingress`).
624
632
 
625
- L'étiquette elle-même est calculée par `resolveBackplaneOriginId()` (`originId.ts:24`), et
633
+ L'étiquette elle-même est calculée par `resolveBackplaneOriginId()` (`originId.ts#resolveBackplaneOriginId`), et
626
634
  sa recette mérite qu'on s'y arrête :
627
635
 
628
636
  | Étape | Valeur | Pourquoi |
@@ -641,14 +649,14 @@ sa recette mérite qu'on s'y arrête :
641
649
 
642
650
  ### Le cloisonnement du canal Redis
643
651
 
644
- Le canal pub/sub de base est `nodefony:realtime` (`RedisBackplane.ts:20`). Problème : le
652
+ Le canal pub/sub de base est `nodefony:realtime` (`RedisBackplane.ts#REDIS_RT_CHANNEL`). Problème : le
645
653
  numéro de base Redis **ne cloisonne pas** le pub/sub, qui est global au serveur. Deux
646
654
  applications sur un Redis mutualisé se parleraient.
647
655
 
648
- D'où le suffixe de cloison, calculé par `resolveRedisChannel()` (`RedisBackplane.ts:31`) :
656
+ D'où le suffixe de cloison, calculé par `resolveRedisChannel()` (`RedisBackplane.ts#resolveRedisChannel`) :
649
657
  le canal effectif devient `nodefony:realtime:<namespace>`. Le namespace vient de la
650
658
  configuration ; à défaut, il est dérivé du nom de l'application
651
- (`src/packages/@nodefony/realtime/index.ts:132`).
659
+ (`src/packages/@nodefony/realtime/index.ts`).
652
660
 
653
661
  > [!TIP]
654
662
  > Deux déploiements de la **même** application sur un Redis partagé (préproduction et
@@ -656,19 +664,19 @@ configuration ; à défaut, il est dérivé du nom de l'application
656
664
  > namespace **explicite et distinct** dans ces cas-là.
657
665
 
658
666
  L'entrée est par ailleurs blindée : un JSON malformé publié par un tiers sur le même canal
659
- est ignoré, pas propagé (`RedisBackplane.ts:212`).
667
+ est ignoré, pas propagé (`RedisBackplane.ts#RedisBackplane`).
660
668
 
661
669
  ### Le démarrage, et ce qui se passe quand ça rate
662
670
 
663
671
  Le module valide sa configuration par Zod à l'enregistrement
664
- (`src/packages/@nodefony/realtime/index.ts:219`), puis câble tout au démarrage du noyau
665
- (`src/packages/@nodefony/realtime/index.ts:213`) — après l'initialisation des services (le
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
666
674
  service Redis a donc ouvert ses connexions) et avant le trafic.
667
675
 
668
676
  Le `start()` du backplane est **attendu explicitement avant** d'être branché : un driver
669
677
  asynchrone brancherait sinon un abonnement incomplet et perdrait ses premiers messages. Et
670
678
  il est **borné par un délai** de cinq secondes
671
- (`src/packages/@nodefony/realtime/index.ts:83`), pour une raison très concrète : un
679
+ (`src/packages/@nodefony/realtime/index.ts#BACKPLANE_START_TIMEOUT_MS`), pour une raison très concrète : un
672
680
  transport réseau qui reste pendu gèlerait la phase de démarrage, et donc la montée des
673
681
  serveurs HTTP eux-mêmes.
674
682
 
@@ -680,7 +688,7 @@ Trois issues, toutes annoncées, jamais silencieuses :
680
688
  | driver inactif ici (loopback, mauvais rôle) | hub local | `INFO` |
681
689
  | driver inconnu, ou démarrage en échec/expiré | hub local, **le démarrage continue** | `WARNING` |
682
690
 
683
- Le module se déclare non critique (`src/packages/@nodefony/realtime/index.ts:144`) : son
691
+ Le module se déclare non critique (`src/packages/@nodefony/realtime/index.ts`) : son
684
692
  échec ne tue jamais le process. C'est de la résilience **sans dégradation silencieuse** —
685
693
  tout repli est annoncé. Enfin, `enabled: false` rend le module totalement inerte : ni API
686
694
  d'administration, ni backplane, ni sonde.
@@ -710,18 +718,18 @@ registerBackplaneDriver(
710
718
 
711
719
  **2. Déclarer un service** nommé `realtimeBackplane` dans le conteneur d'injection —
712
720
  **la voie « instance » pour une application**. `RealtimeService.init()`
713
- (`RealtimeService.ts:79`) le résout et le branche **avant** le registre : une instance
721
+ (`RealtimeService.ts#RealtimeService.init`) le résout et le branche **avant** le registre : une instance
714
722
  déjà fournie court-circuite la sélection par nom.
715
723
 
716
724
  **3. Passer une instance** en second argument de `defineRealtimeConfig()` — réservé au
717
725
  code qui **appelle le builder lui-même** (un `Module` sur mesure, une intégration
718
726
  programmatique). Une application qui configure via `use("@nodefony/realtime", …)` **ne
719
727
  l'atteint pas** : elle ne construit pas la config (le module appelle
720
- `defineRealtimeConfig(this.options)` sans second argument, `index.ts:216`), et une
728
+ `defineRealtimeConfig(this.options)` sans second argument, `index.ts#createRealtimeHarness`), et une
721
729
  instance posée dans l'objet de config est refusée par la validation Zod (clé inconnue du
722
730
  schéma strict). Pour une app, la voie « instance » est l'option 2 ci-dessus.
723
731
 
724
- Le registre s'introspecte : `listBackplaneDrivers()` (`backplaneRegistry.ts:68`) rend les
732
+ Le registre s'introspecte : `listBackplaneDrivers()` (`backplaneRegistry.ts#listBackplaneDrivers`) rend les
725
733
  noms disponibles — c'est ce que le message d'erreur affiche quand un driver déclaré est
726
734
  introuvable.
727
735
 
@@ -731,31 +739,31 @@ L'architecture prévoit des points de greffe pour que `@nodefony/security` s'y b
731
739
  refonte. Ici, seulement **où** ils sont ; le **quoi** et le **comment** sont dans
732
740
  [la page sécurité](./securite.md).
733
741
 
734
- | Point de greffe | Étage | Moment | Ancrage |
735
- | ---------------------------- | --------- | -------------- | --------------------------- |
736
- | Contrôle d'origine | hub | à l'upgrade | `RealtimeHub.ts:614` |
737
- | Authentificateur réseau | hub | au handshake | `RealtimeHub.ts:579` |
738
- | Verrou de frame | protocole | à chaque frame | `RealtimeHub.ts:675` |
739
- | Politique déclarée par canal | hub | au handshake | `RealtimeHub.ts:713` |
740
- | Audit de frame | protocole | sur refus | `RealtimeController.ts:404` |
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` |
741
749
 
742
750
  Deux propriétés architecturales méritent d'être notées ici, parce qu'elles expliquent des
743
751
  choix de conception visibles partout dans le module :
744
752
 
745
753
  **Coût nul quand la sécurité est absente.** Le verrou de frame n'est branché sur le peer que
746
- si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:1058`, testé une
754
+ si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts#RealtimeHub.hasFrameAuthorizer`, testé une
747
755
  fois au handshake). Sans module de sécurité, `beforeDispatch` reste indéfini et le chemin
748
756
  chaud ne paie **rien** du tout.
749
757
 
750
758
  **Échec bruyant plutôt que faux sentiment de sécurité.** Si des canaux déclarent une
751
759
  politique sans qu'aucun décideur ne soit câblé, `hasUnenforcedChannelPolicies()`
752
- (`RealtimeHub.ts:1117`) le détecte et un avertissement est émis une fois par process
753
- (`RealtimeController.ts:487`). Un canal qui **se croit** gardé alors qu'il est ouvert est
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
754
762
  bien plus dangereux qu'un canal ouvertement public.
755
763
 
756
764
  ## 📡 Observabilité — la sonde et Studio
757
765
 
758
- `RealtimeHub.probe()` (`RealtimeHub.ts:865`) rend un instantané en **lecture pure** : aucune
766
+ `RealtimeHub.probe()` (`RealtimeHub.ts#RealtimeHub.probe`) rend un instantané en **lecture pure** : aucune
759
767
  allocation sur le chemin chaud, jamais d'exception. Les compteurs sont des primitives
760
768
  incrémentées en O(1) ; ils sont **monotones**, ce qui laisse au lecteur le soin de dériver
761
769
  un débit.
@@ -768,35 +776,35 @@ un débit.
768
776
  | Back-pressure | `maxBufferedAmount`, `totalBufferedAmount`, `slowConsumers`, `drops` | le risque mémoire numéro un |
769
777
  | Fond de panier | `backplane` (la carte d'identité de `describe()`) | ce qui est réellement branché |
770
778
 
771
- Le contrat complet est `IRealtimeProbe` (`IRealtimeProbe.ts:61`). La version enrichie
772
- `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) y ajoute l'identifiant d'instance et la sonde
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
773
781
  process, plus — si elles sont disponibles — les sondes ORM et les compteurs d'erreurs. Deux
774
782
  chemins de lecture, un seul producteur :
775
783
 
776
784
  - **HTTP** : `GET /nodefony/realtime/api/health`, exposé par `createRealtimeAdminApi()`
777
- (`RealtimeAdminApi.ts:98`) ;
785
+ (`RealtimeAdminApi.ts#createRealtimeAdminApi`) ;
778
786
  - **Canal temps réel** : `nodefony:socket`, poussé par un ticker.
779
787
 
780
788
  ### En cluster : la vue « pod » plutôt que « worker »
781
789
 
782
790
  Interroger la santé en cluster pose un problème sournois : la requête atterrit sur **un**
783
791
  worker au hasard, dont l'instantané ne dit rien des autres. D'où `ClusterProbeClient`
784
- (`ClusterProbeClient.ts:138`), qui applique un modèle **push** — chaque worker remonte
785
- périodiquement sa santé au maître (`ClusterProbeClient.ts:179`), le maître agrège et
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
786
794
  rediffuse, chaque worker met en cache. **N'importe lequel** sert alors la vue pod en O(1),
787
795
  sans latence de requête.
788
796
 
789
- L'agrégation, `mergeClusterHealth()` (`ClusterProbeClient.ts:46`), est une fonction pure :
797
+ L'agrégation, `mergeClusterHealth()` (`ClusterProbeClient.ts#mergeClusterHealth`), est une fonction pure :
790
798
  elle somme les scalaires, mais prend le **maximum** de `maxBufferedAmount` — la santé d'une
791
799
  flotte se juge sur son pire membre, pas sur sa moyenne. `buildRealtimeHealth()`
792
- (`RealtimeAdminApi.ts:74`) choisit ensuite la vue agrégée si elle existe, la per-instance
800
+ (`RealtimeAdminApi.ts#buildRealtimeHealth`) choisit ensuite la vue agrégée si elle existe, la per-instance
793
801
  sinon.
794
802
 
795
803
  Sonde désactivée ? Elle n'est pas muette : **elle n'existe pas**. Aucun client, donc aucun
796
804
  minuteur, aucun écouteur, aucun message IPC
797
- (`src/packages/@nodefony/realtime/index.ts:351`) — coupable par la configuration ou par une
805
+ (`src/packages/@nodefony/realtime/index.ts#Realtime.#startWithTimeout`) — coupable par la configuration ou par une
798
806
  variable d'environnement. Enfin, le maître peut demander à un worker précis d'enrichir sa
799
- remontée (`requestEnrich()`, `ClusterProbeClient.ts:239`) : la sonde riche n'est allouée que
807
+ remontée (`requestEnrich()`, `ClusterProbeClient.ts#ClusterProbeClient.requestEnrich`) : la sonde riche n'est allouée que
800
808
  pendant l'inspection, puis libérée — on paie ce qu'on regarde.
801
809
 
802
810
  ## ⚡ Performance & mémoire
@@ -805,10 +813,10 @@ Le module suit une discipline stricte, lisible dans presque chaque champ privé
805
813
 
806
814
  **Allocation paresseuse partout.** Table des canaux, registre des connexions, registre des
807
815
  révocables, authentificateurs, politiques de canal, association peer→jeton : tout démarre à
808
- `null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts:141` et suivants).
816
+ `null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts#IRevocableConnection` et suivants).
809
817
 
810
818
  **Aucun minuteur au repos.** Le tick de révocation démarre au premier inscrit et s'arrête
811
- dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:809`).
819
+ dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts#RealtimeHub.unregisterRevocable`).
812
820
  Les minuteurs sont détachés de la boucle d'événements : ils ne retiennent jamais l'arrêt du
813
821
  process.
814
822
 
@@ -839,7 +847,7 @@ dans la sonde, et le plafond de canaux par connexion.
839
847
  | Abonnement refusé avec `realtime:denied` motif `limit` | plafond de canaux par connexion atteint (256 par défaut) | regrouper les canaux, ou relever le plafond en connaissance de cause |
840
848
  | Fermeture `1013` sur un client lent | file au-delà de `websocket.maxBackpressure` (4 MiB) pendant 1000 frames jetées d'affilée | attendu ; le client se reconnecte et se resynchronise |
841
849
  | Fermeture `4001` en cours de session | identité révoquée, détectée par le tick de re-validation | se réauthentifier ; le comportement est voulu |
842
- | Frames envoyées juste après `connect()` perdues | le transport n'est pas branché tant que le handshake n'est pas fini | attendre `realtime:welcome` (`RealtimeClient` le fait déjà) |
850
+ | Frames envoyées juste après `connect()` perdues | le transport n'est pas branché tant que le handshake n'est pas fini | attendre `realtime:welcome` (`NodefonySocket` le fait déjà) |
843
851
  | Avertissement « channel policies … NOT enforced » au démarrage | des canaux déclarent une politique sans décideur câblé | charger `@nodefony/security` avec une zone realtime |
844
852
  | `ServerRealtimeSocket.request()` rejette systématiquement | un handle posé sur le hub n'a pas d'interlocuteur unique | utiliser `RealtimeController.requestClient()` pour un appel ciblé |
845
853
  | `TS2550: RegExp.escape does not exist` à la compilation | `lib` du tsconfig sous ES2025 (les paquets exposent leurs types en source) | `"lib": ["ESNext", …]`, comme le tsconfig généré par `nodefony create app` |
@@ -868,7 +876,7 @@ l'aperçu, régénérée depuis les résultats réels — jamais figés ici.
868
876
  > [!CAUTION]
869
877
  > Plusieurs de ces suites exigent une infrastructure (Redis, cluster). **Un test qui se
870
878
  > saute compte comme vert.** Avant de conclure « tout passe », vérifie que les variables
871
- > d'environnement d'infrastructure sont bien posées — la source unique est `vitest.gates.ts`
879
+ > d'environnement d'infrastructure sont bien posées — la source unique est `scripts/test/vitest/gates.ts`
872
880
  > à la racine, dont le rapport s'affiche en fin d'exécution.
873
881
 
874
882
  Couverture : `npm run coverage` dans `@nodefony/realtime`. Pour la charge et la mémoire, les