@nodefony/realtime 10.0.0-beta.2 → 10.0.0-beta.3
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 +2 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +17 -10
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/config/configSchema.js +19 -0
- package/dist/nodefony/config/defineModuleConfig.js +42 -15
- package/dist/nodefony/decorators/realtimeDecorators.js +68 -17
- package/dist/nodefony/src/backplane/RedisBackplane.js +21 -4
- package/dist/nodefony/src/backplane/bootNotices.js +43 -0
- package/dist/nodefony/src/backplane/publishQueue.js +1 -4
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +15 -4
- package/dist/nodefony/src/guards.js +41 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -4
- package/dist/nodefony/src/server/RealtimeController.js +120 -50
- package/dist/nodefony/src/server/RealtimeHub.js +29 -51
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +13 -5
- package/dist/nodefony/src/server/syslogUplink.js +4 -2
- package/dist/nodefony/src/service/RealtimeService.js +3 -2
- package/dist/nodefony/testing/index.js +41 -1
- package/dist/types/nodefony/config/configSchema.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
- package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
- package/dist/types/nodefony/src/guards.d.ts +32 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +24 -4
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
- package/dist/types/nodefony/testing/index.d.ts +2 -2
- package/docs/actions.md +111 -28
- package/docs/architecture.md +77 -75
- package/docs/cadence-et-contre-pression.md +634 -0
- package/docs/configuration.md +39 -34
- package/docs/cookbook-chat.md +31 -27
- package/docs/index.md +35 -21
- package/docs/observabilite.md +17 -15
- package/docs/protocole.md +74 -63
- package/docs/securite.md +49 -49
- package/docs/vocabulaire.md +76 -75
- package/package.json +16 -12
- package/docs/couverture-bancs.md +0 -139
package/docs/vocabulaire.md
CHANGED
|
@@ -113,7 +113,7 @@ Trois mots du lexique ne sont pas des synonymes de ce que proposent les autres b
|
|
|
113
113
|
temps réel. Ce sont les différenciateurs du framework.
|
|
114
114
|
|
|
115
115
|
**`socket` est isomorphe.** Le même contrat `IRealtimeSocket` (`IRealtimeSocket.ts:122`) décrit la
|
|
116
|
-
prise **côté navigateur** (`
|
|
116
|
+
prise **côté navigateur** (`NodefonySocket`, `NodefonySocket.ts:195`) **et côté serveur**
|
|
117
117
|
(`ServerRealtimeSocket`, `ServerRealtimeSocket.ts:44`). Un service back publie exactement comme une
|
|
118
118
|
page front : `publish("chat:room-42", payload)`. Il n'y a pas une API cliente et une API serveur à
|
|
119
119
|
apprendre, il y en a **une**.
|
|
@@ -125,9 +125,9 @@ vers un autre service — et le consommateur ne voit jamais la différence
|
|
|
125
125
|
(`IRealtimeChannel`, `IRealtimeSocket.ts:87`).
|
|
126
126
|
|
|
127
127
|
**`contrôleur` est le même mot qu'en HTTP.** Un endpoint temps réel est un contrôleur qui étend
|
|
128
|
-
`RealtimeController` (`RealtimeController.ts:
|
|
128
|
+
`RealtimeController` (`RealtimeController.ts:203`) et porte une route WebSocket : HTTP et WebSocket
|
|
129
129
|
vivent dans le **même contexte de contrôleur**. C'est ce qui permet au pont `api.request`
|
|
130
|
-
(`RealtimeController.realtimeApiRequest()`, `RealtimeController.ts:
|
|
130
|
+
(`RealtimeController.realtimeApiRequest()`, `RealtimeController.ts:321`) de rejouer sur la socket
|
|
131
131
|
**la même action** que celle servie en REST.
|
|
132
132
|
|
|
133
133
|
## 🚀 Démarrage rapide — les mots en situation
|
|
@@ -179,10 +179,10 @@ export class ChatController extends RealtimeController {
|
|
|
179
179
|
Côté client — la même **socket**, les quatre verbes :
|
|
180
180
|
|
|
181
181
|
```ts
|
|
182
|
-
import {
|
|
182
|
+
import { NodefonySocket } from "nodefony/client";
|
|
183
183
|
|
|
184
184
|
export async function joinChat(): Promise<void> {
|
|
185
|
-
const socket = new
|
|
185
|
+
const socket = new NodefonySocket({ url: "wss://127.0.0.1:5152/chat" });
|
|
186
186
|
await socket.connect();
|
|
187
187
|
|
|
188
188
|
socket.subscribe("chat:room-42"); // DEMANDE le flux au pair
|
|
@@ -218,7 +218,7 @@ Le **handle unique** manipulé par le code applicatif : quatre verbes (`subscrib
|
|
|
218
218
|
`request`) et une vue par canal. Le mot vient de la prise murale : on branche, on ignore le câblage.
|
|
219
219
|
|
|
220
220
|
Contrat isomorphe `IRealtimeSocket` (`IRealtimeSocket.ts:122`), implémenté côté navigateur par
|
|
221
|
-
`
|
|
221
|
+
`NodefonySocket` (`NodefonySocket.ts:394`) et côté serveur par `ServerRealtimeSocket`
|
|
222
222
|
(`ServerRealtimeSocket.ts:44`). ⚠️ La socket **n'est pas** le [transport](#transport--la-couche-octets).
|
|
223
223
|
|
|
224
224
|
→ [Architecture](./architecture.md) pour la pile complète.
|
|
@@ -229,7 +229,7 @@ Un **tuyau bidirectionnel nommé**, multiplexé sur la connexion : `"chat:room-4
|
|
|
229
229
|
`"nodefony:orm:health"`, `"nodefony:socket"`. Convention de nommage par `:` (espace de noms, puis
|
|
230
230
|
précision). Ce n'est pas une classe — c'est une **chaîne de caractères indexée par le hub**.
|
|
231
231
|
|
|
232
|
-
`RealtimeHub.subscribe()` (`RealtimeHub.ts:
|
|
232
|
+
`RealtimeHub.subscribe()` (`RealtimeHub.ts:464`) l'indexe ; `IRealtimeSocket.subscribe()`
|
|
233
233
|
(`IRealtimeSocket.ts:132`) le demande. Plusieurs canaux cohabitent sur **une** connexion : c'est le
|
|
234
234
|
multiplexage.
|
|
235
235
|
|
|
@@ -255,22 +255,22 @@ que personne ne lit ; écouter sans s'abonner n'en reçoit aucune.
|
|
|
255
255
|
client, la charge part vers le serveur (un seul pair) ; depuis le serveur, elle part en
|
|
256
256
|
[fan-out](#fan-out--une-publication-n-livraisons) vers tous les abonnés.
|
|
257
257
|
|
|
258
|
-
`IRealtimeSocket.publish()` (`IRealtimeSocket.ts:160`), `RealtimeHub.publish()` (`RealtimeHub.ts:
|
|
258
|
+
`IRealtimeSocket.publish()` (`IRealtimeSocket.ts:160`), `RealtimeHub.publish()` (`RealtimeHub.ts:610`).
|
|
259
259
|
|
|
260
260
|
### `action RPC` (request) — l'appel corrélé
|
|
261
261
|
|
|
262
262
|
L'appel qui **attend une réponse** : une `Promise` résolue avec le `result`, rejetée sur `error` ou
|
|
263
|
-
sur expiration. Déclarée côté serveur par `@RealtimeAction` (`realtimeDecorators.ts:
|
|
263
|
+
sur expiration. Déclarée côté serveur par `@RealtimeAction` (`realtimeDecorators.ts:191`), appelée
|
|
264
264
|
côté client par `request()` (`IRealtimeSocket.ts:166`).
|
|
265
265
|
|
|
266
266
|
Un handler qui lève une erreur rend un `-32603` **générique** ; seule une `RpcError`
|
|
267
|
-
(`JsonRpcPeer.ts:
|
|
267
|
+
(`JsonRpcPeer.ts:101`) choisit ce qu'elle expose au pair.
|
|
268
268
|
|
|
269
269
|
### `canal entrant` (inbound) — le client pousse
|
|
270
270
|
|
|
271
271
|
Un canal où le **client a le droit d'émettre** vers le serveur. Défaut sûr : **aucun**. Un canal
|
|
272
|
-
n'accepte d'entrée que déclaré explicitement, par `@RealtimeInbound` (`realtimeDecorators.ts:
|
|
273
|
-
par l'override `realtimeInbound()` (`RealtimeController.ts:
|
|
272
|
+
n'accepte d'entrée que déclaré explicitement, par `@RealtimeInbound` (`realtimeDecorators.ts:278`) ou
|
|
273
|
+
par l'override `realtimeInbound()` (`RealtimeController.ts:307`).
|
|
274
274
|
|
|
275
275
|
Le handler reçoit `(params, reply)` — `params` vient du réseau, donc **jamais fiable** :
|
|
276
276
|
`RealtimeInboundHandler` (`IRealtimeController.ts:16`).
|
|
@@ -285,7 +285,7 @@ point d'accroche des canaux à état — un appel SIP, une connexion pontée.
|
|
|
285
285
|
|
|
286
286
|
### `client` — la socket côté navigateur
|
|
287
287
|
|
|
288
|
-
`
|
|
288
|
+
`NodefonySocket` (`NodefonySocket.ts:162`) : reconnexion automatique, réémission des abonnements,
|
|
289
289
|
compteurs par canal, heartbeat. Publié dans le sous-chemin `nodefony/client` du cœur — donc
|
|
290
290
|
importable **sans** aucune dépendance serveur, ce qui est la condition de l'isomorphisme.
|
|
291
291
|
|
|
@@ -294,10 +294,10 @@ importable **sans** aucune dépendance serveur, ce qui est la condition de l'iso
|
|
|
294
294
|
### `façade serveur` — la même socket, côté back
|
|
295
295
|
|
|
296
296
|
`ServerRealtimeSocket` (`ServerRealtimeSocket.ts:44`), obtenue par `serverSocket()`
|
|
297
|
-
(`ServerRealtimeSocket.ts:
|
|
297
|
+
(`ServerRealtimeSocket.ts:243`) : un service métier tient un handle et publie **comme une page
|
|
298
298
|
front**. Une différence assumée : `request()` n'y est pas supporté — au-dessus du hub il n'y a pas
|
|
299
299
|
**un** pair mais N clients. Pour un appel serveur → un client précis, c'est `requestClient()`
|
|
300
|
-
(`RealtimeController.ts:
|
|
300
|
+
(`RealtimeController.ts:395`).
|
|
301
301
|
|
|
302
302
|
### `accueil` (welcome) — la première frame
|
|
303
303
|
|
|
@@ -335,10 +335,10 @@ handshake, jamais renégociée par frame.
|
|
|
335
335
|
### `hub` — le broker du process
|
|
336
336
|
|
|
337
337
|
Le **standard téléphonique** du pod : il tient la table « canal → abonnés locaux » et diffuse. Un
|
|
338
|
-
process = un hub, obtenu par `getRealtimeHub()` (`RealtimeHub.ts:
|
|
338
|
+
process = un hub, obtenu par `getRealtimeHub()` (`RealtimeHub.ts:1432`). Il ne connaît **ni** les
|
|
339
339
|
contrôleurs, **ni** le métier : ce sont les providers qui portent les dépendances.
|
|
340
340
|
|
|
341
|
-
`RealtimeHub` (`RealtimeHub.ts:
|
|
341
|
+
`RealtimeHub` (`RealtimeHub.ts:214`). ⚠️ « hub » désigne **toujours** le serveur ; ce que tient le
|
|
342
342
|
code applicatif s'appelle une [socket](#socket--la-prise-que-tient-ton-code).
|
|
343
343
|
|
|
344
344
|
### `fan-out` — une publication, N livraisons
|
|
@@ -346,15 +346,15 @@ code applicatif s'appelle une [socket](#socket--la-prise-que-tient-ton-code).
|
|
|
346
346
|
L'action de diffuser : une charge publiée sur un canal part vers **tous** les sinks abonnés
|
|
347
347
|
localement. Une connexion fautive ne casse pas la diffusion aux autres — chaque livraison est isolée.
|
|
348
348
|
|
|
349
|
-
`RealtimeHub.publish()` (`RealtimeHub.ts:
|
|
350
|
-
(`RealtimeHub.ts:
|
|
349
|
+
`RealtimeHub.publish()` (`RealtimeHub.ts:610`) pour le chemin complet, `publishLocal()`
|
|
350
|
+
(`RealtimeHub.ts:629`) pour la diffusion **strictement locale** (voie d'entrée du backplane).
|
|
351
351
|
|
|
352
352
|
### `sink` — le point de livraison d'une connexion
|
|
353
353
|
|
|
354
354
|
Un `sink` = **une** connexion sur **un** canal : la fonction qui pousse la charge vers son pair. Le
|
|
355
355
|
mot vient du couple source/puits : le provider est la source, le sink le puits.
|
|
356
356
|
|
|
357
|
-
`ChannelSink` (`RealtimeHub.ts:
|
|
357
|
+
`ChannelSink` (`RealtimeHub.ts:156`). Le nombre de sinks d'un canal est son nombre d'abonnés locaux.
|
|
358
358
|
|
|
359
359
|
### `provider` — ce qui produit un canal
|
|
360
360
|
|
|
@@ -362,7 +362,7 @@ La fabrique appelée **au premier abonné** d'un canal, qui démarre ce qui prod
|
|
|
362
362
|
ticker, un écouteur) et rend son `dispose`, appelé **au dernier désabonné**. Zéro abonné = zéro
|
|
363
363
|
timer.
|
|
364
364
|
|
|
365
|
-
`ChannelFactory` (`RealtimeHub.ts:
|
|
365
|
+
`ChannelFactory` (`RealtimeHub.ts:171`). ⚠️ Un provider est **partagé** : il survit à la connexion qui
|
|
366
366
|
l'a créé — n'y capturer que des dépendances à longue vie, jamais le contexte d'une connexion.
|
|
367
367
|
|
|
368
368
|
### `canal partagé` — un provider par canal et par pod
|
|
@@ -371,7 +371,7 @@ Le modèle de coût du module : là où une implémentation naïve crée un tick
|
|
|
371
371
|
Nodefony en crée **un par canal et par pod** et se contente d'ajouter un sink par abonné. Mille
|
|
372
372
|
spectateurs d'un tableau de bord coûtent un timer, pas mille.
|
|
373
373
|
|
|
374
|
-
`RealtimeHub.subscribe()` (`RealtimeHub.ts:
|
|
374
|
+
`RealtimeHub.subscribe()` (`RealtimeHub.ts:464`).
|
|
375
375
|
|
|
376
376
|
### `canal passif` — écouter n'est pas posséder
|
|
377
377
|
|
|
@@ -385,8 +385,8 @@ reçoit du vrai producteur ; sinon l'abonnement est refusé, et l'écoute en pla
|
|
|
385
385
|
Sans cette distinction, le premier service qui prononce un nom de canal l'ouvrirait pour tout le
|
|
386
386
|
monde — et les clients suivants s'abonneraient à un flux que rien ne produit, sans jamais l'apprendre.
|
|
387
387
|
|
|
388
|
-
`RealtimeHub.listen()` (`RealtimeHub.ts:
|
|
389
|
-
(`RealtimeHub.ts:
|
|
388
|
+
`RealtimeHub.listen()` (`RealtimeHub.ts:531`) pour l'écoute, `isChannelOwned()`
|
|
389
|
+
(`RealtimeHub.ts:552`) pour lire l'état. ⚠️ Un canal passif **n'est pas** un canal fermé : il n'a
|
|
390
390
|
simplement pas encore de propriétaire.
|
|
391
391
|
|
|
392
392
|
### `canal système` — servi par la plateforme
|
|
@@ -395,7 +395,7 @@ Un canal dont la fabrique est déclarée par un module bas niveau (le journal d'
|
|
|
395
395
|
qui devient servable par **n'importe quel** endpoint, présent ou futur, sans qu'aucun contrôleur ne
|
|
396
396
|
le connaisse. Consulté seulement quand la fabrique du contrôleur a dit « inconnu ».
|
|
397
397
|
|
|
398
|
-
`RealtimeHub.registerSystemChannel()` (`RealtimeHub.ts:
|
|
398
|
+
`RealtimeHub.registerSystemChannel()` (`RealtimeHub.ts:1278`).
|
|
399
399
|
|
|
400
400
|
### `canal broadcast` — celui qui traverse le backplane
|
|
401
401
|
|
|
@@ -403,8 +403,8 @@ Un canal **déclaré** comme franchissant la frontière du process : chat, prés
|
|
|
403
403
|
déclaration se fait par **préfixe**, ce qui couvre les variantes de cadence (`chat:` couvre
|
|
404
404
|
`chat:room-42:1000`).
|
|
405
405
|
|
|
406
|
-
`RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:
|
|
407
|
-
`@RealtimeBroadcast` (`realtimeDecorators.ts:
|
|
406
|
+
`RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:674`), déclaré côté contrôleur par
|
|
407
|
+
`@RealtimeBroadcast` (`realtimeDecorators.ts:420`).
|
|
408
408
|
|
|
409
409
|
### `canal instance-local` — le défaut
|
|
410
410
|
|
|
@@ -412,15 +412,15 @@ Tout canal **non déclaré** broadcast reste dans son process. C'est voulu : les
|
|
|
412
412
|
d'observabilité (journaux, sondes, état interne) décrivent **ce pod**, et les agréger silencieusement
|
|
413
413
|
serait à la fois faux et une fuite. Traverser le process est une capacité qu'un canal **demande**.
|
|
414
414
|
|
|
415
|
-
Politique de forward du hub — `#broadcastPrefixes` (`RealtimeHub.ts:
|
|
415
|
+
Politique de forward du hub — `#broadcastPrefixes` (`RealtimeHub.ts:395`).
|
|
416
416
|
|
|
417
417
|
### `contrôleur temps réel` — l'endpoint WebSocket
|
|
418
418
|
|
|
419
419
|
La classe de base d'un endpoint : elle porte tout le protocole (handshake, accueil, discrimination
|
|
420
420
|
des frames, cycle de vie des canaux) et ne laisse au métier que ses canaux et ses actions.
|
|
421
421
|
|
|
422
|
-
`RealtimeController` (`RealtimeController.ts:
|
|
423
|
-
(`RealtimeController.ts:
|
|
422
|
+
`RealtimeController` (`RealtimeController.ts:203`), point d'entrée `handleRealtime()`
|
|
423
|
+
(`RealtimeController.ts:344`). C'est un **contrôleur** au sens habituel de Nodefony : la même classe
|
|
424
424
|
peut porter des routes HTTP.
|
|
425
425
|
|
|
426
426
|
### `service realtime` — la façade d'injection
|
|
@@ -437,31 +437,32 @@ L'option qui expose la méthode `api.request { path }` : la connexion rejoue **l
|
|
|
437
437
|
contrôleur** que celle servie en REST, avec **la même garde**. Le pont n'atteint que les routes qui
|
|
438
438
|
déclarent explicitement le transport WebSocket — aucun contournement possible.
|
|
439
439
|
|
|
440
|
-
`realtimeApiRequest()` (`RealtimeController.ts:
|
|
441
|
-
(`RealtimeController.ts:
|
|
440
|
+
`realtimeApiRequest()` (`RealtimeController.ts:321`), mise en œuvre `invokeApiRequest()`
|
|
441
|
+
(`RealtimeController.ts:1070`). Désactivé par défaut.
|
|
442
442
|
|
|
443
443
|
## 🔌 Le protocole et le transport — ce qui passe sur le fil
|
|
444
444
|
|
|
445
|
-
| Terme | En une ligne
|
|
446
|
-
| -------------- |
|
|
447
|
-
| `frame` | l'unité atomique qui passe sur le fil
|
|
448
|
-
| `pair` | le moteur de protocole, identique client et serveur
|
|
449
|
-
| `requête` | frame avec `method` **et** `id` — appelle une réponse
|
|
450
|
-
| `notification` | frame avec `method` seul — sans réponse
|
|
451
|
-
| `réponse` |
|
|
452
|
-
| `dispatch` | classer une frame entrante puis la router vers son handler
|
|
453
|
-
| `transport` | la couche octets, interchangeable
|
|
454
|
-
| `enveloppe` | joindre des métadonnées serveur à côté du résultat
|
|
455
|
-
| `cadence` | la fréquence d'un canal d'état, portée par son nom
|
|
456
|
-
| `AIMD` | l'auto-ajustement de cadence face à un client lent
|
|
445
|
+
| Terme | En une ligne |
|
|
446
|
+
| -------------- | ----------------------------------------------------------- |
|
|
447
|
+
| `frame` | l'unité atomique qui passe sur le fil |
|
|
448
|
+
| `pair` | le moteur de protocole, identique client et serveur |
|
|
449
|
+
| `requête` | frame avec `method` **et** `id` — appelle une réponse |
|
|
450
|
+
| `notification` | frame avec `method` seul — sans réponse |
|
|
451
|
+
| `réponse` | `id` + `result` ou `error`, sans `method` — résout un appel |
|
|
452
|
+
| `dispatch` | classer une frame entrante puis la router vers son handler |
|
|
453
|
+
| `transport` | la couche octets, interchangeable |
|
|
454
|
+
| `enveloppe` | joindre des métadonnées serveur à côté du résultat |
|
|
455
|
+
| `cadence` | la fréquence d'un canal d'état, portée par son nom |
|
|
456
|
+
| `AIMD` | l'auto-ajustement de cadence face à un client lent |
|
|
457
457
|
|
|
458
458
|
### `frame` — l'unité qui passe sur le fil
|
|
459
459
|
|
|
460
460
|
Une enveloppe JSON-RPC 2.0. Sa **nature se lit sur `method`**, pas sur `id` : `method` + `id` =
|
|
461
|
-
requête, `method` seul = notification, `id`
|
|
461
|
+
requête, `method` seul = notification, `id` + `result` ou `error` = réponse. Tout le reste est
|
|
462
|
+
invalide.
|
|
462
463
|
|
|
463
|
-
`JsonRpcFrameKind` (`
|
|
464
|
-
(`JsonRpcPeer.ts:
|
|
464
|
+
`JsonRpcFrameKind` (`jsonrpc/index.ts:138`). Le classement est fait par `JsonRpcPeer.receive()`
|
|
465
|
+
(`JsonRpcPeer.ts:419`), qui **ne lève jamais**.
|
|
465
466
|
|
|
466
467
|
### `pair` (peer) — le moteur de protocole
|
|
467
468
|
|
|
@@ -469,14 +470,14 @@ Client et serveur sont des **pairs** : classer une frame, router, corréler les
|
|
|
469
470
|
travail des deux côtés. Nodefony l'écrit **une seule fois** ; chaque côté l'entoure de son transport
|
|
470
471
|
et de ses handlers. Aucune dépendance Node — le même fichier tourne dans le navigateur.
|
|
471
472
|
|
|
472
|
-
`JsonRpcPeer` (`JsonRpcPeer.ts:
|
|
473
|
+
`JsonRpcPeer` (`JsonRpcPeer.ts:311`), contrat `IRealtimePeer` (`JsonRpcPeer.ts:261`).
|
|
473
474
|
|
|
474
475
|
### `dispatch` — classer puis router
|
|
475
476
|
|
|
476
477
|
Le geste du pair sur une frame entrante : décider de sa nature, trouver le handler, l'appeler,
|
|
477
478
|
renvoyer la réponse s'il en faut une. Ce n'est pas une méthode publique — c'est un **moment**, et
|
|
478
479
|
c'est précisément là que se greffe le [verrou de frame](#verrou-de-frame--la-décision-par-frame),
|
|
479
|
-
par le crochet `beforeDispatch` (`JsonRpcPeer.ts:
|
|
480
|
+
par le crochet `beforeDispatch` (`JsonRpcPeer.ts:219`).
|
|
480
481
|
|
|
481
482
|
### `transport` — la couche octets
|
|
482
483
|
|
|
@@ -494,7 +495,7 @@ Quand un handler veut joindre des métadonnées serveur à sa réponse, il rend
|
|
|
494
495
|
`result` reste **exactement** ce que rendrait la même route en REST, et la méta voyage dans un champ
|
|
495
496
|
frère. Un pair qui n'en connaît pas les clés les ignore.
|
|
496
497
|
|
|
497
|
-
`RpcEnvelope` (`JsonRpcPeer.ts:
|
|
498
|
+
`RpcEnvelope` (`JsonRpcPeer.ts:139`), `RpcMeta` (`JsonRpcPeer.ts:125`).
|
|
498
499
|
|
|
499
500
|
### `cadence` — la fréquence portée par le nom
|
|
500
501
|
|
|
@@ -535,7 +536,7 @@ publications d'un process aux autres. Un contrat volontairement minuscule — `p
|
|
|
535
536
|
`start`, `stop`, `describe` — parce que tout ce qui est riche appartient au hub.
|
|
536
537
|
|
|
537
538
|
`IBackplane` (`IBackplane.ts:107`), message `IBackplaneMessage` (`IBackplane.ts:51`), branchement
|
|
538
|
-
`RealtimeHub.setBackplane()` (`RealtimeHub.ts:
|
|
539
|
+
`RealtimeHub.setBackplane()` (`RealtimeHub.ts:720`).
|
|
539
540
|
|
|
540
541
|
> [!TIP]
|
|
541
542
|
> C'est la pièce qui tient la promesse « une ligne de configuration change tout » : le hub **ne sait
|
|
@@ -587,7 +588,7 @@ La règle qui empêche les tempêtes : un message **reçu** du backplane est ré
|
|
|
587
588
|
**locale seulement**, jamais renvoyé sur le bus ; et un message qui porte sa propre origine est
|
|
588
589
|
ignoré à l'arrivée. Sans l'origine, l'anti-écho ferait taire le voisin au lieu de soi-même.
|
|
589
590
|
|
|
590
|
-
`RealtimeHub.publishLocal()` (`RealtimeHub.ts:
|
|
591
|
+
`RealtimeHub.publishLocal()` (`RealtimeHub.ts:629`).
|
|
591
592
|
|
|
592
593
|
### `cloison` (namespace) — deux applications, un même bus
|
|
593
594
|
|
|
@@ -630,7 +631,7 @@ l'authenticator tourne et que l'identité est figée. Tout ce qui coûte cher se
|
|
|
630
631
|
frame.
|
|
631
632
|
|
|
632
633
|
DTO neutre `IRealtimeHandshake` (`IRealtimeHandshake.ts:14`) — en-têtes, cookies, URL, origine,
|
|
633
|
-
sous-protocoles. Traitement dans `onHandshake()` (`RealtimeController.ts:
|
|
634
|
+
sous-protocoles. Traitement dans `onHandshake()` (`RealtimeController.ts:450`).
|
|
634
635
|
|
|
635
636
|
### `authenticator` — du handshake à l'identité
|
|
636
637
|
|
|
@@ -639,7 +640,7 @@ La stratégie qui transforme une poignée de main en identité : `supports` (est
|
|
|
639
640
|
HTTP — un seul modèle mental pour les deux transports.
|
|
640
641
|
|
|
641
642
|
`IRealtimeAuthenticator` (`IRealtimeAuthenticator.ts:24`), enregistrement
|
|
642
|
-
`RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:
|
|
643
|
+
`RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:925`).
|
|
643
644
|
|
|
644
645
|
### `matcher` — qui capture quoi
|
|
645
646
|
|
|
@@ -647,7 +648,7 @@ Le sélecteur (motif d'URL, hôte optionnel) qui décide **quel** authenticator
|
|
|
647
648
|
**premier** qui capture gagne : on enregistre donc du plus spécifique au plus général.
|
|
648
649
|
|
|
649
650
|
`IRealtimeAuthenticatorMatcher` (`IRealtimeAuthenticatorMatcher.ts:25`), résolution
|
|
650
|
-
`resolveAuthenticator()` (`RealtimeHub.ts:
|
|
651
|
+
`resolveAuthenticator()` (`RealtimeHub.ts:943`).
|
|
651
652
|
|
|
652
653
|
### `jeton` (token) — l'identité figée
|
|
653
654
|
|
|
@@ -664,7 +665,7 @@ Les exigences déclarées d'un canal : authentifié, rôles, scopes. Elles s'att
|
|
|
664
665
|
canal, pas à la méthode qui l'implémente — parce que c'est le nom que le pare-feu résout.
|
|
665
666
|
|
|
666
667
|
`IChannelPolicy` (`IChannelPolicy.ts:20`), déclaration par `@RealtimeChannel`
|
|
667
|
-
(`realtimeDecorators.ts:
|
|
668
|
+
(`realtimeDecorators.ts:240`), résolution `resolveChannelPolicy()` (`RealtimeHub.ts:1209`).
|
|
668
669
|
|
|
669
670
|
### `verrou de frame` — la décision par frame
|
|
670
671
|
|
|
@@ -672,8 +673,8 @@ La fonction qui répond « cette frame passe-t-elle ? » à partir du jeton **d
|
|
|
672
673
|
**Synchrone par doctrine** : attendre une réponse distante à chaque frame sérialiserait le flux d'une
|
|
673
674
|
connexion. Un contrôle qui doit interroger le réseau se fait au handshake, pas ici.
|
|
674
675
|
|
|
675
|
-
`FrameAuthorizer` (`RealtimeHub.ts:
|
|
676
|
-
`runAuthorizer()` (`RealtimeHub.ts:
|
|
676
|
+
`FrameAuthorizer` (`RealtimeHub.ts:40`), pose `setFrameAuthorizer()` (`RealtimeHub.ts:1020`), appel
|
|
677
|
+
`runAuthorizer()` (`RealtimeHub.ts:1309`).
|
|
677
678
|
|
|
678
679
|
### `contrôle d'origine` — la défense d'ouverture
|
|
679
680
|
|
|
@@ -681,7 +682,7 @@ Le contrôle de l'en-tête `Origin` à l'ouverture (RFC 6455 §10.2) : un site t
|
|
|
681
682
|
ouvrir une connexion authentifiée dans le dos de l'utilisateur. Correspondance **exacte**, aucun
|
|
682
683
|
caractère générique.
|
|
683
684
|
|
|
684
|
-
`OriginGuard` (`RealtimeHub.ts:
|
|
685
|
+
`OriginGuard` (`RealtimeHub.ts:34`), configuration `csrf.checkOrigin` (`config.ts:255`).
|
|
685
686
|
|
|
686
687
|
### `révocation` — la connexion qui survit à sa session
|
|
687
688
|
|
|
@@ -689,8 +690,8 @@ Le problème que ce mot nomme : une connexion ouverte pourrait survivre à la se
|
|
|
689
690
|
autorisée, puisque le verrou de frame ne relit rien. Un contrôle périodique referme l'écart en
|
|
690
691
|
re-validant les identités révocables et en fermant celles qui ne valent plus.
|
|
691
692
|
|
|
692
|
-
`registerRevocable()` (`RealtimeHub.ts:
|
|
693
|
-
`REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:
|
|
693
|
+
`registerRevocable()` (`RealtimeHub.ts:796`), `revalidateRevocable()` (`RealtimeHub.ts:815`), période
|
|
694
|
+
`REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:112`). Une re-validation en erreur **ferme** — jamais
|
|
694
695
|
l'inverse.
|
|
695
696
|
|
|
696
697
|
### `refus` (denied) — rendre le « non » visible
|
|
@@ -700,7 +701,7 @@ donc `realtime:denied`. Le motif est **générique** : jamais le rôle ou le sco
|
|
|
700
701
|
transformerait le refus en oracle d'autorisation.
|
|
701
702
|
|
|
702
703
|
`IRealtimeDenied` (`RealtimeEventMap.ts:269`), conversion en message d'interface `deniedToNotice()`
|
|
703
|
-
(`notice.ts:
|
|
704
|
+
(`notice.ts:46`).
|
|
704
705
|
|
|
705
706
|
### `audit de frame` — tracer le notable
|
|
706
707
|
|
|
@@ -708,7 +709,7 @@ Le signal émis sur les événements protocolaires qui méritent une trace : fra
|
|
|
708
709
|
refusée, méthode inconnue, erreur interne. Émis sans attente, avec le pair — ce qui permet de
|
|
709
710
|
retrouver **qui** a été refusé, pas seulement d'où venait le paquet.
|
|
710
711
|
|
|
711
|
-
`FrameAuditReason` (`JsonRpcPeer.ts:
|
|
712
|
+
`FrameAuditReason` (`JsonRpcPeer.ts:182`), crochet `onFrameAudit` (`JsonRpcPeer.ts:236`).
|
|
712
713
|
|
|
713
714
|
### `seam` — le point de greffe
|
|
714
715
|
|
|
@@ -716,8 +717,8 @@ Littéralement une **couture** : un point prévu dans une couche basse pour qu'u
|
|
|
716
717
|
greffe du comportement **sans modifier la couche basse**. Le module en expose cinq, et c'est ce qui
|
|
717
718
|
permet à la couche sécurité de se brancher sans qu'aucune ligne de realtime ne la connaisse.
|
|
718
719
|
|
|
719
|
-
Les deux du protocole sont `beforeDispatch` (`JsonRpcPeer.ts:
|
|
720
|
-
(`JsonRpcPeer.ts:
|
|
720
|
+
Les deux du protocole sont `beforeDispatch` (`JsonRpcPeer.ts:219`) et `onFrameAudit`
|
|
721
|
+
(`JsonRpcPeer.ts:236`) ; les trois du hub sont l'authenticator, le matcher et le contrôle d'origine.
|
|
721
722
|
|
|
722
723
|
→ [Sécurité](./securite.md) pour le détail de chacun.
|
|
723
724
|
|
|
@@ -728,7 +729,7 @@ ressources ; sans borne, une seule connexion peut les épuiser. Au-delà, l'abon
|
|
|
728
729
|
client en est informé.
|
|
729
730
|
|
|
730
731
|
`limits.maxChannelsPerConnection` (`config.ts:142`), pose `setMaxChannelsPerConnection()`
|
|
731
|
-
(`RealtimeHub.ts:
|
|
732
|
+
(`RealtimeHub.ts:976`).
|
|
732
733
|
|
|
733
734
|
## 📡 L'observabilité — ce que la socket dit d'elle-même
|
|
734
735
|
|
|
@@ -747,7 +748,7 @@ L'oscilloscope du module : « voici mon état, maintenant ». Canaux et abonnés
|
|
|
747
748
|
livraisons, connexions, octets, back-pressure, carte d'identité du backplane. Lecture **pure**, sans
|
|
748
749
|
allocation sur le chemin chaud.
|
|
749
750
|
|
|
750
|
-
`RealtimeHub.probe()` (`RealtimeHub.ts:
|
|
751
|
+
`RealtimeHub.probe()` (`RealtimeHub.ts:847`) rend un `IRealtimeProbe` (`IRealtimeProbe.ts:61`).
|
|
751
752
|
|
|
752
753
|
> [!WARNING]
|
|
753
754
|
> `IRealtimeProbe` est la **forme des données**, pas une interface à implémenter avec une méthode
|
|
@@ -759,10 +760,10 @@ La quantité d'octets déjà remise au transport mais pas encore partie. C'est l
|
|
|
759
760
|
n° 1 : il monte quand le client n'absorbe plus. Deux seuils distincts existent — un pour **compter**
|
|
760
761
|
(la sonde), un pour **agir** (abandonner des frames, puis fermer).
|
|
761
762
|
|
|
762
|
-
Seuil de comptage `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:
|
|
763
|
+
Seuil de comptage `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:64`) — une constante, car il ne
|
|
763
764
|
décide de rien. Les seuils d'ACTION, eux, sont des réglages : `websocket.maxBackpressure`,
|
|
764
765
|
`websocket.backpressurePolicy` et `websocket.backpressureCloseAfterDrops` (`http`,
|
|
765
|
-
`http/nodefony/config/config.ts:
|
|
766
|
+
`http/nodefony/config/config.ts:1226`).
|
|
766
767
|
|
|
767
768
|
### `consommateur lent` — la connexion qui décroche
|
|
768
769
|
|
|
@@ -785,7 +786,7 @@ point d'arrivée des frames, donc réutilisables par n'importe quelle applicatio
|
|
|
785
786
|
En multi-worker, chaque process a **sa** sonde. La vue du pod est leur fusion : cumuls additionnés,
|
|
786
787
|
maxima repris au plus haut, instances listées une à une.
|
|
787
788
|
|
|
788
|
-
`mergeClusterHealth()` (`ClusterProbeClient.ts:
|
|
789
|
+
`mergeClusterHealth()` (`ClusterProbeClient.ts:60`), forme `IRealtimeClusterHealth`
|
|
789
790
|
(`IRealtimeProbe.ts:178`).
|
|
790
791
|
|
|
791
792
|
### `plan de données admin` — la sonde servie à Studio
|
|
@@ -802,14 +803,14 @@ elle-même par le même chemin que les autres modules.
|
|
|
802
803
|
| Symptôme | Cause — le mot pris pour un autre | Correction |
|
|
803
804
|
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
804
805
|
| « Je suis abonné mais je ne reçois rien » | `subscribe` confondu avec `on` : le flux est demandé, aucun écouteur ne le lit | Les deux : `subscribe(canal)` **et** `on(canal, handler)` (`IRealtimeSocket.ts:131`) |
|
|
805
|
-
| « Ça marche en local, plus rien dès qu'on passe à plusieurs pods » | canal resté **instance-local** — le défaut. Traverser le process est une capacité qu'on **demande** | Déclarer le préfixe dans `@RealtimeBroadcast` (`realtimeDecorators.ts:
|
|
806
|
-
| « Le client publie, le serveur ignore » | canal non déclaré **entrant**. Un client ne peut rien pousser tant qu'aucun handler n'existe | `@RealtimeInbound("mon:canal")` (`realtimeDecorators.ts:
|
|
806
|
+
| « Ça marche en local, plus rien dès qu'on passe à plusieurs pods » | canal resté **instance-local** — le défaut. Traverser le process est une capacité qu'on **demande** | Déclarer le préfixe dans `@RealtimeBroadcast` (`realtimeDecorators.ts:420`) |
|
|
807
|
+
| « Le client publie, le serveur ignore » | canal non déclaré **entrant**. Un client ne peut rien pousser tant qu'aucun handler n'existe | `@RealtimeInbound("mon:canal")` (`realtimeDecorators.ts:278`) |
|
|
807
808
|
| Deux déploiements se mélangent sur un même serveur Redis | pas de **cloison** — le numéro de base ne cloisonne pas le pub/sub | Poser `backplane.namespace` (`config.ts:59`) |
|
|
808
809
|
| Le fan-out disparaît entre conteneurs identiques | **origine** dérivée du seul identifiant de processus : deux conteneurs sont tous deux le n° 1, l'anti-écho avale tout | `resolveBackplaneOriginId()` (`originId.ts:24`) dérive du pod ou de l'hôte |
|
|
809
810
|
| Deux tickers pour le même tableau de bord | **cadence** différente = **canal** différent, jamais réconcilié | Fabriquer le nom via `rateChannel()` (`channelRate.ts:44`) des deux côtés |
|
|
810
|
-
| Un canal se croit protégé mais laisse tout passer | **politique** déclarée sans **verrou de frame** posé pour la faire respecter | Le hub le détecte et avertit : `hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:
|
|
811
|
-
| Une connexion garde ses flux après une déconnexion applicative | le **verrou de frame** ne relit rien : par construction, il ne voit pas la session mourir | Inscrire l'identité au registre de **révocation** (`RealtimeHub.ts:
|
|
812
|
-
| « Le provider a planté après la fermeture d'un onglet » | **provider** confondu avec **sink** : le provider est partagé, il survit à la connexion créatrice | N'y capturer que des dépendances à longue vie (`ChannelFactory`, `RealtimeHub.ts:
|
|
811
|
+
| Un canal se croit protégé mais laisse tout passer | **politique** déclarée sans **verrou de frame** posé pour la faire respecter | Le hub le détecte et avertit : `hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1098`) |
|
|
812
|
+
| Une connexion garde ses flux après une déconnexion applicative | le **verrou de frame** ne relit rien : par construction, il ne voit pas la session mourir | Inscrire l'identité au registre de **révocation** (`RealtimeHub.ts:439`) |
|
|
813
|
+
| « Le provider a planté après la fermeture d'un onglet » | **provider** confondu avec **sink** : le provider est partagé, il survit à la connexion créatrice | N'y capturer que des dépendances à longue vie (`ChannelFactory`, `RealtimeHub.ts:171`) |
|
|
813
814
|
| « Notre store realtime est indisponible » | **store** employé pour **driver** : le backplane ne conserve rien, il fait passer | Dire « le driver de backplane » — les données ont des stores, les flux ont des drivers |
|
|
814
815
|
|
|
815
816
|
> [!CAUTION]
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/realtime",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.3",
|
|
4
|
+
"nodefony": {
|
|
5
|
+
"configSchema": "./dist/nodefony/config/configSchema.js"
|
|
6
|
+
},
|
|
4
7
|
"description": "Couche temps réel serveur de Nodefony : canaux, appels bidirectionnels JSON-RPC 2.0, contre-pression, et diffusion entre plusieurs instances (boucle locale, IPC de grappe, Redis)",
|
|
5
8
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
6
9
|
"type": "module",
|
|
@@ -41,20 +44,20 @@
|
|
|
41
44
|
"esm"
|
|
42
45
|
],
|
|
43
46
|
"peerDependencies": {
|
|
44
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
45
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
46
|
-
"nodefony": "^10.0.0-beta.
|
|
47
|
+
"@nodefony/framework": "^10.0.0-beta.3",
|
|
48
|
+
"@nodefony/http": "^10.0.0-beta.3",
|
|
49
|
+
"nodefony": "^10.0.0-beta.3",
|
|
47
50
|
"reflect-metadata": "0.2.2",
|
|
48
|
-
"zod": "^4.6.
|
|
51
|
+
"zod": "^4.6.7"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
51
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
52
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
53
|
-
"@types/node": "26.6.
|
|
54
|
-
"@vitest/coverage-v8": "5.0.
|
|
55
|
-
"nodefony": "^10.0.0-beta.
|
|
54
|
+
"@nodefony/framework": "^10.0.0-beta.3",
|
|
55
|
+
"@nodefony/http": "^10.0.0-beta.3",
|
|
56
|
+
"@types/node": "26.6.5",
|
|
57
|
+
"@vitest/coverage-v8": "5.0.4",
|
|
58
|
+
"nodefony": "^10.0.0-beta.3",
|
|
56
59
|
"reflect-metadata": "0.2.2",
|
|
57
|
-
"vitest": "5.0.
|
|
60
|
+
"vitest": "5.0.4"
|
|
58
61
|
},
|
|
59
62
|
"private": false,
|
|
60
63
|
"engines": {
|
|
@@ -66,7 +69,8 @@
|
|
|
66
69
|
},
|
|
67
70
|
"files": [
|
|
68
71
|
"dist",
|
|
69
|
-
"docs"
|
|
72
|
+
"docs",
|
|
73
|
+
"!docs/couverture-bancs.md"
|
|
70
74
|
],
|
|
71
75
|
"publishConfig": {
|
|
72
76
|
"access": "public"
|