@nodefony/realtime 10.0.0-beta.6 → 10.0.0-beta.7
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 +5 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +15 -3
- package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
- package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
- package/dist/nodefony/src/backplane/controlOrder.js +34 -5
- package/dist/nodefony/src/backplane/envelope.js +422 -45
- package/dist/nodefony/src/backplane/ingress.js +93 -0
- package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
- package/dist/nodefony/src/backplane/publishQueue.js +97 -3
- package/dist/nodefony/src/server/RealtimeConnection.js +31 -6
- package/dist/nodefony/src/server/RealtimeController.js +87 -44
- package/dist/nodefony/src/server/RealtimeHub.js +631 -127
- package/dist/nodefony/src/server/admissionBudget.js +8 -8
- package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
- package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
- package/dist/nodefony/src/server/channelPresence.js +488 -0
- package/dist/nodefony/src/server/contractRoutes.js +3 -13
- package/dist/nodefony/src/server/frameAdmission.js +27 -35
- package/dist/nodefony/src/server/presenceBus.js +486 -0
- package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
- package/dist/nodefony/src/server/remotePresence.js +541 -0
- package/dist/nodefony/src/server/subscribedAck.js +3 -0
- package/dist/nodefony/src/service/RealtimeService.js +9 -0
- package/dist/nodefony/testing/backplaneConformance.js +79 -3
- package/dist/nodefony/testing/index.js +22 -8
- package/dist/nodefony/testing/presenceClient.js +54 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
- package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
- package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
- package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
- package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +1 -1
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
- package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
- package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
- package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +3 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
- package/dist/types/nodefony/testing/index.d.ts +1 -1
- package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
- package/docs/actions.md +1 -0
- package/docs/architecture.md +21 -6
- package/docs/canaux-a-membres.md +13 -3
- package/docs/cas-d-usage.md +4 -3
- package/docs/configuration.md +38 -2
- package/docs/cookbook-chat.md +400 -152
- package/docs/observabilite.md +28 -9
- package/docs/protocole.md +10 -10
- package/docs/securite.md +64 -10
- package/package.json +12 -12
package/docs/architecture.md
CHANGED
|
@@ -21,7 +21,7 @@ tags:
|
|
|
21
21
|
]
|
|
22
22
|
version: "doc"
|
|
23
23
|
status: stable
|
|
24
|
-
updated: 2026-
|
|
24
|
+
updated: 2026-11-10
|
|
25
25
|
source: "src/packages/@nodefony/realtime/docs/architecture.md"
|
|
26
26
|
---
|
|
27
27
|
|
|
@@ -628,8 +628,11 @@ même process). Sans filtre, le fan-out local serait fait deux fois. D'où **deu
|
|
|
628
628
|
et non une :
|
|
629
629
|
|
|
630
630
|
1. **Côté hub** : l'arrivée passe par `publishLocal`, jamais par `publish`. Rien ne repart.
|
|
631
|
-
2. **Côté backplane** : à la réception,
|
|
632
|
-
|
|
631
|
+
2. **Côté backplane** : à la réception, l'écho se reconnaît à son `originId` et se jette
|
|
632
|
+
(`ingress.ts#BackplaneIngress`, `ClusterBackplane.ts#ClusterBackplane.#ingress`). Sur un bus
|
|
633
|
+
partagé, il se reconnaît à la **fin** du message, sans le décoder : sous un secret, un seul
|
|
634
|
+
calcul de sceau le vérifie (`envelope.ts#BackplaneEcho`). Chaque écho est compté
|
|
635
|
+
(`echoTotal` de la carte d'identité du backplane) : tant qu'il revient, le bus est vivant.
|
|
633
636
|
|
|
634
637
|
L'étiquette elle-même est calculée par `resolveBackplaneOriginId()` (`originId.ts#resolveBackplaneOriginId`), et
|
|
635
638
|
sa recette mérite qu'on s'y arrête :
|
|
@@ -745,9 +748,21 @@ drivers livrés, exporté pour lui : `sealBackplaneEnvelope` à l'émission,
|
|
|
745
748
|
La preuve est la même pour tous : `runBackplaneConformance`, exportée par
|
|
746
749
|
`@nodefony/realtime/testing` (`backplaneConformance.ts#runBackplaneConformance`). Elle
|
|
747
750
|
relie plusieurs pairs du driver sur un même bus et vérifie la diffusion à tous, l'ordre,
|
|
748
|
-
l'absence de rejeu, le sceau, le transport des ordres de contrôle
|
|
749
|
-
réservé à l'entrée du hub,
|
|
750
|
-
|
|
751
|
+
l'absence de rejeu, le sceau, le transport des ordres de contrôle et de la preuve de vie même
|
|
752
|
+
file saturée, le refus de l'espace réservé à l'entrée du hub, la présence entre deux pods, le
|
|
753
|
+
remplacement du handler et l'arrêt. Les drivers `cluster` et `redis` la passent dans le
|
|
754
|
+
dépôt ; `loopback`, qui n'a aucun pair, n'a rien à y prouver.
|
|
755
|
+
|
|
756
|
+
Le cas « présence entre deux pods » branche deux hubs réels sur deux pairs du driver.
|
|
757
|
+
Il vérifie trois choses :
|
|
758
|
+
|
|
759
|
+
- une identité ouverte sur les deux compte pour un seul présent ;
|
|
760
|
+
- l'arrêt propre d'un hub retire ses présents chez l'autre bien avant le délai d'oubli ;
|
|
761
|
+
- aucun message de présence n'arrive en double ni altéré.
|
|
762
|
+
|
|
763
|
+
Un bus « au moins une fois », qui relivre un message, y tombe : la présence compte les doublons
|
|
764
|
+
dans `presenceStaleDiscardedTotal`. Le driver n'a rien à écrire pour la présence, qui ne parle
|
|
765
|
+
qu'à `IBackplane`. Ce cas prouve que son transport la porte.
|
|
751
766
|
|
|
752
767
|
```typescript
|
|
753
768
|
import { describe, it } from "vitest";
|
package/docs/canaux-a-membres.md
CHANGED
|
@@ -173,6 +173,12 @@ du canal — ses onglets, quel que soit l'exemplaire qui les sert. Il rend le no
|
|
|
173
173
|
éjectées **ici** ; les autres exemplaires l'appliquent sans compte rendu. Un compte vide n'éjecte
|
|
174
174
|
rien. La même règle vaut : retirer l'appartenance là où `allow` la lit, **puis** éjecter.
|
|
175
175
|
|
|
176
|
+
**Un accès borné dans le temps** (l'invité d'un événement, un abonnement qui expire) ne se périme
|
|
177
|
+
pas seul : le verdict d'`allow` est retenu sur la connexion, et le hub ne relit rien. À l'échéance,
|
|
178
|
+
l'application rejoue sa règle et éjecte qui n'y a plus droit. Un seul minuteur suffit, visé sur
|
|
179
|
+
la prochaine échéance — ni un par accès, ni un balayage périodique qui lirait la base pour rien. La
|
|
180
|
+
démo le fait pour ses salons (`RoomService.closeEnded`, gabarit `create app --demo`).
|
|
181
|
+
|
|
176
182
|
**`closeChannel(canal)`** envoie `realtime:closed { reason: "ended" }` à chaque membre, sur tous les
|
|
177
183
|
exemplaires, puis libère le producteur. Les écouteurs du serveur (`listen`) restent inscrits, sur un canal redevenu passif. Le
|
|
178
184
|
hub ne garde **aucune marque « fermé »** : le `subscribe` suivant rejoue la fabrique, qui rend `null`
|
|
@@ -226,7 +232,10 @@ transporte les **publications** et deux **ordres** — terminer un canal, éject
|
|
|
226
232
|
chaque exemplaire applique à SES connexions ; ni membres, ni politiques, ni acquittements. Ces deux
|
|
227
233
|
ordres ne font qu'enlever : seul `allow` fait entrer. Livrés « au plus une fois », comme toute
|
|
228
234
|
publication : un exemplaire qui en manque un, ou qui démarre après, est rattrapé par `allow` (pour
|
|
229
|
-
une éjection) ou par la fabrique qui rend `null` à une instance terminée (pour une fin).
|
|
235
|
+
une éjection) ou par la fabrique qui rend `null` à une instance terminée (pour une fin). Un bus
|
|
236
|
+
chargé ne les sacrifie pas : quand la file d'envoi vers le bus est pleine, les publications en
|
|
237
|
+
trop sont abandonnées, les ordres passent quand même. Seul un bus injoignable en perd un, et la
|
|
238
|
+
perte se compte (`controlLostTotal`) et s'annonce.
|
|
230
239
|
|
|
231
240
|
Les gestes **par connexion** agissent sur les connexions de ce processus. Deux voies marchent en
|
|
232
241
|
cluster pour eux :
|
|
@@ -248,8 +257,9 @@ marcherait en développement et une fois sur N en production serait le pire des
|
|
|
248
257
|
gestionnaire entrant, refus de `allow` rendu au seul appelant, connexion fermée ou désabonnée
|
|
249
258
|
pendant l'attente ; fin du canal, producteur libéré, écouteur conservé, fabrique rejouée.
|
|
250
259
|
- **Entre exemplaires** (`memberOrdersAcrossPods.test.ts`) : fin et éjection d'un compte données sur
|
|
251
|
-
un hub atteignent les membres de l'autre ; canal de contrôle jamais diffusé ; ordres mal formés
|
|
252
|
-
|
|
260
|
+
un hub atteignent les membres de l'autre ; canal de contrôle jamais diffusé ; ordres mal formés
|
|
261
|
+
ou visant un canal de la plateforme rejetés et comptés (`ingressRejectedTotal`), ordre d'un type
|
|
262
|
+
inconnu de cette version jeté et compté à part (`ingressUnknownDiscardedTotal`). Sur un
|
|
253
263
|
**Redis réel** (`memberOrdersRedis.test.ts`, gate Redis) : ordre scellé appliqué, ordre non scellé
|
|
254
264
|
ignoré, et le rejeu d'une éjection captée — le risque accepté — décrit par un test. Sur le **bus du
|
|
255
265
|
cluster** (`clusterIpc.e2e.test.ts`, `npm run test:cluster`) : deux vrais processus.
|
package/docs/cas-d-usage.md
CHANGED
|
@@ -517,8 +517,8 @@ export class MatchRealtime extends RealtimeController {
|
|
|
517
517
|
return () => {};
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
//
|
|
521
|
-
//
|
|
520
|
+
// Le motif servirait toute la famille : la surcharge la FILTRE — seule une
|
|
521
|
+
// partie qui existe est servie, tout autre nom est refusé (`null`).
|
|
522
522
|
override createRealtimeChannel(channel: string): (() => void) | null {
|
|
523
523
|
return this.matches.exists(channel) ? () => {} : null;
|
|
524
524
|
}
|
|
@@ -758,7 +758,8 @@ export class RoomRealtime extends RealtimeController {
|
|
|
758
758
|
return () => {};
|
|
759
759
|
}
|
|
760
760
|
|
|
761
|
-
//
|
|
761
|
+
// La surcharge FILTRE la famille : un nom qui n'est pas un salon est refusé
|
|
762
|
+
// (`null`). Sans elle, le motif servirait tout `room:*`, sous `allow`.
|
|
762
763
|
override createRealtimeChannel(channel: string): (() => void) | null {
|
|
763
764
|
return RoomService.roomOf(channel) === null ? null : () => {};
|
|
764
765
|
}
|
package/docs/configuration.md
CHANGED
|
@@ -23,7 +23,7 @@ tags:
|
|
|
23
23
|
]
|
|
24
24
|
version: "doc"
|
|
25
25
|
status: stable
|
|
26
|
-
updated: 2026-
|
|
26
|
+
updated: 2026-11-10
|
|
27
27
|
source: "src/packages/@nodefony/realtime/docs/configuration.md"
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -750,7 +750,8 @@ class NatsBackplane implements IBackplane {
|
|
|
750
750
|
this.#handler = handler;
|
|
751
751
|
}
|
|
752
752
|
async stop(): Promise<void> {
|
|
753
|
-
/*
|
|
753
|
+
/* laisser partir ce qui est déjà émis (résoudre APRÈS l'acquittement),
|
|
754
|
+
puis fermer et libérer les écouteurs — idempotent */
|
|
754
755
|
}
|
|
755
756
|
describe(): IBackplaneInfo {
|
|
756
757
|
return {
|
|
@@ -782,6 +783,13 @@ Quatre points que la fabrique doit respecter :
|
|
|
782
783
|
4. `describe()` alimente **trois** sorties depuis une source unique : la ligne de journal, la sonde
|
|
783
784
|
de santé et l'écran Studio. Remplis-la honnêtement, en particulier `crossPod`.
|
|
784
785
|
|
|
786
|
+
Côté driver, un point compte à l'arrêt : **`stop()` laisse partir ce qui est déjà émis**. Le
|
|
787
|
+
processus sort au tick qui suit l'arrêt du noyau. Un `stop()` qui ferme la connexion sans attendre
|
|
788
|
+
l'acquittement perd la dernière publication, c'est-à-dire la fin de la présence de ce serveur. Les
|
|
789
|
+
autres serveurs gardent alors ses présents jusqu'au délai d'oubli. Le pilote Redis attend sa file
|
|
790
|
+
d'envoi (`publishQueue.ts#BackplanePublishQueue.settled`) ; un pilote maison bâti sur
|
|
791
|
+
`BackplanePublishQueue` en hérite.
|
|
792
|
+
|
|
785
793
|
**L'autre voie : fournir une instance déjà construite.** Déclare un service nommé
|
|
786
794
|
`realtimeBackplane` dans le conteneur d'injection de ton module. `RealtimeService.init()`
|
|
787
795
|
(`RealtimeService.ts#RealtimeService.init`) le lit et le branche **avant** que le registre soit consulté : une instance
|
|
@@ -795,6 +803,34 @@ backplane })`), et la documentation d'architecture présente cette voie. En prat
|
|
|
795
803
|
> inconnue, dont une instance de classe posée dans `backplane`, fait échouer le boot. Pour brancher un objet déjà construit, utilise
|
|
796
804
|
> le service `realtimeBackplane`.
|
|
797
805
|
|
|
806
|
+
### Arrêter proprement un bus que tu fournis
|
|
807
|
+
|
|
808
|
+
À l'arrêt du noyau, le module arrête **le bus qu'il a créé lui-même** depuis le registre. Il appelle
|
|
809
|
+
`shutdownRealtime` (`realtimeShutdown.ts#shutdownRealtime`), qui fait trois choses :
|
|
810
|
+
|
|
811
|
+
1. la présence des canaux annonce sa fin : dernier lot, puis preuve de vie finale ;
|
|
812
|
+
2. le bus s'arrête en laissant partir ce qui est déjà émis ;
|
|
813
|
+
3. le tout est borné à `BACKPLANE_STOP_TIMEOUT_MS` (2 s), pour qu'un bus injoignable ne retienne
|
|
814
|
+
pas l'arrêt.
|
|
815
|
+
|
|
816
|
+
Un bus fourni par `realtimeBackplane` reste **à toi** : le module envoie bien la fin de la
|
|
817
|
+
présence, mais n'arrête pas ce bus. Branche le même appel à l'arrêt du module qui l'a construit :
|
|
818
|
+
|
|
819
|
+
```ts ignore
|
|
820
|
+
import { getRealtimeHub, shutdownRealtime } from "@nodefony/realtime";
|
|
821
|
+
|
|
822
|
+
// Dans le module qui a construit `bus` et l'a déclaré comme `realtimeBackplane`.
|
|
823
|
+
this.kernel?.once("onTerminate", async () => {
|
|
824
|
+
const stopped = await shutdownRealtime(getRealtimeHub(), bus);
|
|
825
|
+
if (!stopped)
|
|
826
|
+
this.log("bus temps réel : arrêt non terminé dans le délai", "WARNING");
|
|
827
|
+
});
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
`shutdownRealtime` rend `false` quand le bus n'a pas fini à temps. Ce qui restait en vol peut alors
|
|
831
|
+
être perdu, et les autres serveurs oublieront celui-ci au délai d'oubli, sans fantôme durable.
|
|
832
|
+
L'appel est idempotent : la présence ne s'annonce terminée qu'une fois.
|
|
833
|
+
|
|
798
834
|
Le détail du contrat, de l'anti-écho et du cycle de vie d'un driver est dans
|
|
799
835
|
[Architecture](./architecture.md).
|
|
800
836
|
|