@nodefony/realtime 10.0.0-alpha.3 → 10.0.0-alpha.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/LICENSE +201 -543
- package/README.md +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/src/server/RealtimeController.js +3 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/nodefony/config/config.d.ts +2 -2
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +3 -3
- package/docs/actions.md +2 -2
- package/docs/architecture.md +17 -17
- package/docs/configuration.md +2 -2
- package/docs/cookbook-chat.md +7 -7
- package/docs/observabilite.md +1 -1
- package/docs/protocole.md +1 -1
- package/docs/securite.md +26 -26
- package/docs/vocabulaire.md +27 -27
- package/package.json +10 -10
package/docs/vocabulaire.md
CHANGED
|
@@ -255,7 +255,7 @@ 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:604`).
|
|
259
259
|
|
|
260
260
|
### `action RPC` (request) — l'appel corrélé
|
|
261
261
|
|
|
@@ -297,7 +297,7 @@ importable **sans** aucune dépendance serveur, ce qui est la condition de l'iso
|
|
|
297
297
|
(`ServerRealtimeSocket.ts:223`) : 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:279`).
|
|
301
301
|
|
|
302
302
|
### `accueil` (welcome) — la première frame
|
|
303
303
|
|
|
@@ -305,7 +305,7 @@ La notification `realtime:welcome`, poussée par le serveur juste après le hand
|
|
|
305
305
|
protocole, les canaux et actions **découvrables** de l'endpoint, et l'identité résolue. Un client
|
|
306
306
|
attend cette frame avant de pousser quoi que ce soit.
|
|
307
307
|
|
|
308
|
-
`IRealtimeWelcome` (`RealtimeEventMap.ts:
|
|
308
|
+
`IRealtimeWelcome` (`RealtimeEventMap.ts:231`).
|
|
309
309
|
|
|
310
310
|
### `identité` — qui est cette connexion
|
|
311
311
|
|
|
@@ -313,7 +313,7 @@ La vue « sur soi » d'une connexion : type de jeton, authentifié ou non, ident
|
|
|
313
313
|
Aucun secret — seulement ce que le porteur sait déjà de lui-même. Elle est **résolue une fois** au
|
|
314
314
|
handshake, jamais renégociée par frame.
|
|
315
315
|
|
|
316
|
-
`RealtimeIdentity` (`RealtimeEventMap.ts:
|
|
316
|
+
`RealtimeIdentity` (`RealtimeEventMap.ts:212`).
|
|
317
317
|
|
|
318
318
|
## 🏗️ Le serveur — ce qui aiguille derrière la prise
|
|
319
319
|
|
|
@@ -335,7 +335,7 @@ 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:1435`). 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
341
|
`RealtimeHub` (`RealtimeHub.ts:213`). ⚠️ « hub » désigne **toujours** le serveur ; ce que tient le
|
|
@@ -346,7 +346,7 @@ 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:
|
|
349
|
+
`RealtimeHub.publish()` (`RealtimeHub.ts:604`) pour le chemin complet, `publishLocal()`
|
|
350
350
|
(`RealtimeHub.ts:549`) pour la diffusion **strictement locale** (voie d'entrée du backplane).
|
|
351
351
|
|
|
352
352
|
### `sink` — le point de livraison d'une connexion
|
|
@@ -354,7 +354,7 @@ localement. Une connexion fautive ne casse pas la diffusion aux autres — chaqu
|
|
|
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:155`). 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:170`). ⚠️ 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
|
|
@@ -386,7 +386,7 @@ Sans cette distinction, le premier service qui prononce un nom de canal l'ouvrir
|
|
|
386
386
|
monde — et les clients suivants s'abonneraient à un flux que rien ne produit, sans jamais l'apprendre.
|
|
387
387
|
|
|
388
388
|
`RealtimeHub.listen()` (`RealtimeHub.ts:455`) pour l'écoute, `isChannelOwned()`
|
|
389
|
-
(`RealtimeHub.ts:
|
|
389
|
+
(`RealtimeHub.ts:550`) 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:1281`).
|
|
399
399
|
|
|
400
400
|
### `canal broadcast` — celui qui traverse le backplane
|
|
401
401
|
|
|
@@ -412,7 +412,7 @@ 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:393`).
|
|
416
416
|
|
|
417
417
|
### `contrôleur temps réel` — l'endpoint WebSocket
|
|
418
418
|
|
|
@@ -420,7 +420,7 @@ La classe de base d'un endpoint : elle porte tout le protocole (handshake, accue
|
|
|
420
420
|
des frames, cycle de vie des canaux) et ne laisse au métier que ses canaux et ses actions.
|
|
421
421
|
|
|
422
422
|
`RealtimeController` (`RealtimeController.ts:144`), point d'entrée `handleRealtime()`
|
|
423
|
-
(`RealtimeController.ts:
|
|
423
|
+
(`RealtimeController.ts:249`). 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
|
|
@@ -438,7 +438,7 @@ contrôleur** que celle servie en REST, avec **la même garde**. Le pont n'attei
|
|
|
438
438
|
déclarent explicitement le transport WebSocket — aucun contournement possible.
|
|
439
439
|
|
|
440
440
|
`realtimeApiRequest()` (`RealtimeController.ts:219`), mise en œuvre `invokeApiRequest()`
|
|
441
|
-
(`RealtimeController.ts:
|
|
441
|
+
(`RealtimeController.ts:878`). Désactivé par défaut.
|
|
442
442
|
|
|
443
443
|
## 🔌 Le protocole et le transport — ce qui passe sur le fil
|
|
444
444
|
|
|
@@ -535,7 +535,7 @@ publications d'un process aux autres. Un contrat volontairement minuscule — `p
|
|
|
535
535
|
`start`, `stop`, `describe` — parce que tout ce qui est riche appartient au hub.
|
|
536
536
|
|
|
537
537
|
`IBackplane` (`IBackplane.ts:75`), message `IBackplaneMessage` (`IBackplane.ts:51`), branchement
|
|
538
|
-
`RealtimeHub.setBackplane()` (`RealtimeHub.ts:
|
|
538
|
+
`RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`).
|
|
539
539
|
|
|
540
540
|
> [!TIP]
|
|
541
541
|
> C'est la pièce qui tient la promesse « une ligne de configuration change tout » : le hub **ne sait
|
|
@@ -587,7 +587,7 @@ La règle qui empêche les tempêtes : un message **reçu** du backplane est ré
|
|
|
587
587
|
**locale seulement**, jamais renvoyé sur le bus ; et un message qui porte sa propre origine est
|
|
588
588
|
ignoré à l'arrivée. Sans l'origine, l'anti-écho ferait taire le voisin au lieu de soi-même.
|
|
589
589
|
|
|
590
|
-
`RealtimeHub.publishLocal()` (`RealtimeHub.ts:
|
|
590
|
+
`RealtimeHub.publishLocal()` (`RealtimeHub.ts:623`).
|
|
591
591
|
|
|
592
592
|
### `cloison` (namespace) — deux applications, un même bus
|
|
593
593
|
|
|
@@ -630,7 +630,7 @@ l'authenticator tourne et que l'identité est figée. Tout ce qui coûte cher se
|
|
|
630
630
|
frame.
|
|
631
631
|
|
|
632
632
|
DTO neutre `IRealtimeHandshake` (`IRealtimeHandshake.ts:14`) — en-têtes, cookies, URL, origine,
|
|
633
|
-
sous-protocoles. Traitement dans `onHandshake()` (`RealtimeController.ts:
|
|
633
|
+
sous-protocoles. Traitement dans `onHandshake()` (`RealtimeController.ts:328`).
|
|
634
634
|
|
|
635
635
|
### `authenticator` — du handshake à l'identité
|
|
636
636
|
|
|
@@ -639,7 +639,7 @@ La stratégie qui transforme une poignée de main en identité : `supports` (est
|
|
|
639
639
|
HTTP — un seul modèle mental pour les deux transports.
|
|
640
640
|
|
|
641
641
|
`IRealtimeAuthenticator` (`IRealtimeAuthenticator.ts:24`), enregistrement
|
|
642
|
-
`RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:
|
|
642
|
+
`RealtimeHub.useAuthenticator()` (`RealtimeHub.ts:927`).
|
|
643
643
|
|
|
644
644
|
### `matcher` — qui capture quoi
|
|
645
645
|
|
|
@@ -647,7 +647,7 @@ Le sélecteur (motif d'URL, hôte optionnel) qui décide **quel** authenticator
|
|
|
647
647
|
**premier** qui capture gagne : on enregistre donc du plus spécifique au plus général.
|
|
648
648
|
|
|
649
649
|
`IRealtimeAuthenticatorMatcher` (`IRealtimeAuthenticatorMatcher.ts:25`), résolution
|
|
650
|
-
`resolveAuthenticator()` (`RealtimeHub.ts:
|
|
650
|
+
`resolveAuthenticator()` (`RealtimeHub.ts:945`).
|
|
651
651
|
|
|
652
652
|
### `jeton` (token) — l'identité figée
|
|
653
653
|
|
|
@@ -664,7 +664,7 @@ Les exigences déclarées d'un canal : authentifié, rôles, scopes. Elles s'att
|
|
|
664
664
|
canal, pas à la méthode qui l'implémente — parce que c'est le nom que le pare-feu résout.
|
|
665
665
|
|
|
666
666
|
`IChannelPolicy` (`IChannelPolicy.ts:20`), déclaration par `@RealtimeChannel`
|
|
667
|
-
(`realtimeDecorators.ts:192`), résolution `resolveChannelPolicy()` (`RealtimeHub.ts:
|
|
667
|
+
(`realtimeDecorators.ts:192`), résolution `resolveChannelPolicy()` (`RealtimeHub.ts:1212`).
|
|
668
668
|
|
|
669
669
|
### `verrou de frame` — la décision par frame
|
|
670
670
|
|
|
@@ -672,8 +672,8 @@ La fonction qui répond « cette frame passe-t-elle ? » à partir du jeton **d
|
|
|
672
672
|
**Synchrone par doctrine** : attendre une réponse distante à chaque frame sérialiserait le flux d'une
|
|
673
673
|
connexion. Un contrôle qui doit interroger le réseau se fait au handshake, pas ici.
|
|
674
674
|
|
|
675
|
-
`FrameAuthorizer` (`RealtimeHub.ts:39`), pose `setFrameAuthorizer()` (`RealtimeHub.ts:
|
|
676
|
-
`runAuthorizer()` (`RealtimeHub.ts:
|
|
675
|
+
`FrameAuthorizer` (`RealtimeHub.ts:39`), pose `setFrameAuthorizer()` (`RealtimeHub.ts:1023`), appel
|
|
676
|
+
`runAuthorizer()` (`RealtimeHub.ts:1312`).
|
|
677
677
|
|
|
678
678
|
### `contrôle d'origine` — la défense d'ouverture
|
|
679
679
|
|
|
@@ -689,7 +689,7 @@ Le problème que ce mot nomme : une connexion ouverte pourrait survivre à la se
|
|
|
689
689
|
autorisée, puisque le verrou de frame ne relit rien. Un contrôle périodique referme l'écart en
|
|
690
690
|
re-validant les identités révocables et en fermant celles qui ne valent plus.
|
|
691
691
|
|
|
692
|
-
`registerRevocable()` (`RealtimeHub.ts:
|
|
692
|
+
`registerRevocable()` (`RealtimeHub.ts:778`), `revalidateRevocable()` (`RealtimeHub.ts:810`), période
|
|
693
693
|
`REVOCATION_REVALIDATE_MS` (`RealtimeHub.ts:111`). Une re-validation en erreur **ferme** — jamais
|
|
694
694
|
l'inverse.
|
|
695
695
|
|
|
@@ -699,7 +699,7 @@ Une notification refusée serait abandonnée en silence, laissant le client aveu
|
|
|
699
699
|
donc `realtime:denied`. Le motif est **générique** : jamais le rôle ou le scope manquant, qui
|
|
700
700
|
transformerait le refus en oracle d'autorisation.
|
|
701
701
|
|
|
702
|
-
`IRealtimeDenied` (`RealtimeEventMap.ts:
|
|
702
|
+
`IRealtimeDenied` (`RealtimeEventMap.ts:269`), conversion en message d'interface `deniedToNotice()`
|
|
703
703
|
(`notice.ts:45`).
|
|
704
704
|
|
|
705
705
|
### `audit de frame` — tracer le notable
|
|
@@ -728,7 +728,7 @@ ressources ; sans borne, une seule connexion peut les épuiser. Au-delà, l'abon
|
|
|
728
728
|
client en est informé.
|
|
729
729
|
|
|
730
730
|
`limits.maxChannelsPerConnection` (`config.ts:142`), pose `setMaxChannelsPerConnection()`
|
|
731
|
-
(`RealtimeHub.ts:
|
|
731
|
+
(`RealtimeHub.ts:979`).
|
|
732
732
|
|
|
733
733
|
## 📡 L'observabilité — ce que la socket dit d'elle-même
|
|
734
734
|
|
|
@@ -762,7 +762,7 @@ n° 1 : il monte quand le client n'absorbe plus. Deux seuils distincts existent
|
|
|
762
762
|
Seuil de comptage `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:63`) — une constante, car il ne
|
|
763
763
|
décide de rien. Les seuils d'ACTION, eux, sont des réglages : `websocket.maxBackpressure`,
|
|
764
764
|
`websocket.backpressurePolicy` et `websocket.backpressureCloseAfterDrops` (`http`,
|
|
765
|
-
`http/nodefony/config/config.ts:
|
|
765
|
+
`http/nodefony/config/config.ts:1044`).
|
|
766
766
|
|
|
767
767
|
### `consommateur lent` — la connexion qui décroche
|
|
768
768
|
|
|
@@ -807,9 +807,9 @@ elle-même par le même chemin que les autres modules.
|
|
|
807
807
|
| 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
808
|
| 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
809
|
| 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:
|
|
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:1101`) |
|
|
811
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:433`) |
|
|
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:
|
|
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:170`) |
|
|
813
813
|
| « 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
814
|
|
|
815
815
|
> [!CAUTION]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/realtime",
|
|
3
|
-
"version": "10.0.0-alpha.
|
|
3
|
+
"version": "10.0.0-alpha.5",
|
|
4
4
|
"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
5
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
6
6
|
"type": "module",
|
|
@@ -40,18 +40,18 @@
|
|
|
40
40
|
"esm"
|
|
41
41
|
],
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@nodefony/framework": "^10.0.0-alpha.
|
|
44
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
45
|
-
"nodefony": "^10.0.0-alpha.
|
|
43
|
+
"@nodefony/framework": "^10.0.0-alpha.5",
|
|
44
|
+
"@nodefony/http": "^10.0.0-alpha.5",
|
|
45
|
+
"nodefony": "^10.0.0-alpha.5",
|
|
46
46
|
"reflect-metadata": "0.2.2",
|
|
47
|
-
"zod": "^4.
|
|
47
|
+
"zod": "^4.6.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@nodefony/framework": "^10.0.0-alpha.
|
|
51
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
52
|
-
"@types/node": "26.
|
|
50
|
+
"@nodefony/framework": "^10.0.0-alpha.5",
|
|
51
|
+
"@nodefony/http": "^10.0.0-alpha.5",
|
|
52
|
+
"@types/node": "26.5.1",
|
|
53
53
|
"@vitest/coverage-v8": "5.0.0",
|
|
54
|
-
"nodefony": "^10.0.0-alpha.
|
|
54
|
+
"nodefony": "^10.0.0-alpha.5",
|
|
55
55
|
"reflect-metadata": "0.2.2",
|
|
56
56
|
"rimraf": "6.1.3",
|
|
57
57
|
"vitest": "5.0.0"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"engines": {
|
|
61
61
|
"node": ">=24.0.0"
|
|
62
62
|
},
|
|
63
|
-
"license": "
|
|
63
|
+
"license": "Apache-2.0",
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"tslib": "2.8.1"
|
|
66
66
|
},
|