@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.
Files changed (62) hide show
  1. package/README.md +5 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +15 -3
  4. package/dist/nodefony/decorators/realtimeDecorators.js +231 -39
  5. package/dist/nodefony/src/backplane/RedisBackplane.js +33 -40
  6. package/dist/nodefony/src/backplane/controlOrder.js +34 -5
  7. package/dist/nodefony/src/backplane/envelope.js +422 -45
  8. package/dist/nodefony/src/backplane/ingress.js +93 -0
  9. package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
  10. package/dist/nodefony/src/backplane/publishQueue.js +97 -3
  11. package/dist/nodefony/src/server/RealtimeConnection.js +31 -6
  12. package/dist/nodefony/src/server/RealtimeController.js +87 -44
  13. package/dist/nodefony/src/server/RealtimeHub.js +631 -127
  14. package/dist/nodefony/src/server/admissionBudget.js +8 -8
  15. package/dist/nodefony/src/server/channelPolicyRegistry.js +35 -13
  16. package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
  17. package/dist/nodefony/src/server/channelPresence.js +488 -0
  18. package/dist/nodefony/src/server/contractRoutes.js +3 -13
  19. package/dist/nodefony/src/server/frameAdmission.js +27 -35
  20. package/dist/nodefony/src/server/presenceBus.js +486 -0
  21. package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
  22. package/dist/nodefony/src/server/remotePresence.js +541 -0
  23. package/dist/nodefony/src/server/subscribedAck.js +3 -0
  24. package/dist/nodefony/src/service/RealtimeService.js +9 -0
  25. package/dist/nodefony/testing/backplaneConformance.js +79 -3
  26. package/dist/nodefony/testing/index.js +22 -8
  27. package/dist/nodefony/testing/presenceClient.js +54 -0
  28. package/dist/types/index.d.ts +6 -1
  29. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +117 -16
  30. package/dist/types/nodefony/interfaces/IBackplane.d.ts +12 -1
  31. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +10 -2
  32. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +23 -0
  33. package/dist/types/nodefony/src/backplane/envelope.d.ts +50 -7
  34. package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
  35. package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
  36. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
  37. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +3 -0
  38. package/dist/types/nodefony/src/server/RealtimeController.d.ts +19 -1
  39. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +139 -30
  40. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +9 -2
  41. package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
  42. package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
  43. package/dist/types/nodefony/src/server/contractRoutes.d.ts +1 -1
  44. package/dist/types/nodefony/src/server/frameAdmission.d.ts +14 -18
  45. package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
  46. package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
  47. package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
  48. package/dist/types/nodefony/src/server/subscribedAck.d.ts +3 -0
  49. package/dist/types/nodefony/src/service/RealtimeService.d.ts +8 -1
  50. package/dist/types/nodefony/testing/backplaneConformance.d.ts +14 -2
  51. package/dist/types/nodefony/testing/index.d.ts +1 -1
  52. package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
  53. package/docs/actions.md +1 -0
  54. package/docs/architecture.md +21 -6
  55. package/docs/canaux-a-membres.md +13 -3
  56. package/docs/cas-d-usage.md +4 -3
  57. package/docs/configuration.md +38 -2
  58. package/docs/cookbook-chat.md +400 -152
  59. package/docs/observabilite.md +28 -9
  60. package/docs/protocole.md +10 -10
  61. package/docs/securite.md +64 -10
  62. package/package.json +12 -12
@@ -0,0 +1,155 @@
1
+ import type { IChannelPolicy, IPresenceView, IRealtimePresenceCount, IRealtimePresenceList, RealtimePresenceFrame } from "nodefony";
2
+ import type { IPresenceChannelDelta, IPresenceChannelState } from "../backplane/presenceMessage.js";
3
+ import type { ChannelFrame } from "./RealtimeHub.js";
4
+ import type { IPresenceAggregate } from "./remotePresence.js";
5
+ /**
6
+ * Fenêtre de regroupement des changements de présence vers le navigateur (ms) — front montant : le premier changement d'un canal calme part tout de suite, les suivants attendent la fin de la fenêtre (ADR-0025 P2 bis).
7
+ */
8
+ export declare const PRESENCE_WINDOW_MS = 250;
9
+ /** Seuil par défaut d'identités nommées au-delà duquel la trame ne porte que les nombres (ADR-0025 P2 bis). */
10
+ export declare const DEFAULT_PRESENCE_MAX_NAMED = 1000;
11
+ /** Réglages RÉSOLUS de la présence d'un canal — défauts appliqués. */
12
+ export interface IPresenceSettings {
13
+ /** Délai de grâce d'un départ (ms), `0` = départs exacts. */
14
+ readonly grace: number;
15
+ /** Identités nommées au-delà desquelles la trame ne porte que les nombres. */
16
+ readonly maxNamed: number;
17
+ }
18
+ /**
19
+ * Ce qu'un observateur serveur lit de la présence d'un canal (`hub.presence()`, ADR-0025 P3) — les membres TOUJOURS, quel que soit le seuil de la trame.
20
+ *
21
+ * La forme est celle du cœur (`IPresenceView`), que le navigateur lit aussi : serveur et client voient le même objet.
22
+ */
23
+ export type IChannelPresenceView = IPresenceView;
24
+ /**
25
+ * Défaut de la valeur de la clé `presence` d'une politique, ou `null` si elle est recevable — en clair, pour que le refus au démarrage dise QUOI corriger.
26
+ *
27
+ * @param value - valeur de `policy.presence` (`unknown` : une politique se lit
28
+ * depuis une métadonnée publique).
29
+ */
30
+ export declare function presenceDefect(value: unknown): string | null;
31
+ /**
32
+ * Réglages résolus de la présence qu'une politique déclare, ou `null` si elle n'en déclare pas.
33
+ *
34
+ * Précondition : la politique a passé {@link presenceDefect}.
35
+ */
36
+ export declare function presenceSettingsOf(policy: IChannelPolicy | null | undefined): IPresenceSettings | null;
37
+ /**
38
+ * Deux politiques déclarent-elles la même présence ? Défauts appliqués : `true` et `{ grace: 0 }` sont la même.
39
+ *
40
+ * Sert à refuser deux déclarations d'un même nom qui ne diffèrent que par la
41
+ * présence — une seule s'appliquerait. Démarrage seulement.
42
+ */
43
+ export declare function samePresence(a: IChannelPolicy, b: IChannelPolicy): boolean;
44
+ /**
45
+ * État de présence LOCAL d'un canal à présence (ADR-0025 P2 bis, P5) — identités nommées et onglets, connexions non nommées, version, changements en attente de la fenêtre, départs en grâce.
46
+ *
47
+ * Alloué au premier client d'un canal qui déclare `presence`, jamais pour un
48
+ * autre canal. Ne connaît ni l'horloge ni le protocole : le hub lui passe
49
+ * l'instant et fabrique les trames. Les changements portent des valeurs
50
+ * ABSOLUES (nombre d'onglets, départ), donc rejouer un changement déjà inclus
51
+ * dans une liste ne change rien — c'est ce qui permet de servir à un nouvel
52
+ * abonné la liste prise au dernier envoi, encodée une fois.
53
+ *
54
+ * Sur un canal DIFFUSÉ entre pods, c'est aussi l'agrégat de la vue des pairs
55
+ * ({@link IPresenceAggregate}) : les compteurs portent le TOTAL (local + pairs),
56
+ * et une table à part garde la seule part locale — ce que ce pod annonce à ses
57
+ * pairs, en deltas par lot. Ailleurs, cette table n'existe pas.
58
+ */
59
+ export declare class ChannelPresence implements IPresenceAggregate {
60
+ #private;
61
+ readonly channel: string;
62
+ readonly settings: IPresenceSettings;
63
+ /** Canal diffusé entre pods : sa part locale part sur le bus, la vue des pairs s'y ajoute. */
64
+ readonly distributed: boolean;
65
+ /** Fin de la fenêtre de regroupement en cours (horloge monotone, ms). */
66
+ windowEnd: number;
67
+ /** Liste complète encodée, servie telle quelle jusqu'au prochain envoi. */
68
+ listFrame: ChannelFrame | null;
69
+ /** Le canal est-il encore tenu par le hub ? `false` après son retrait. */
70
+ live: boolean;
71
+ /**
72
+ * Fin de la synchronisation initiale avec les pairs (ms monotone) — la vue
73
+ * n'est que locale tant que la demande d'état du premier abonné n'a pas eu le
74
+ * temps d'être servie ; `0` = synchronisée (ou canal non diffusé).
75
+ */
76
+ syncUntil: number;
77
+ /**
78
+ * @param distributed - le canal est diffusé entre pods (bus branché, préfixe
79
+ * diffusé) : la part locale est tenue à part pour le bus.
80
+ */
81
+ constructor(channel: string, settings: IPresenceSettings, distributed?: boolean);
82
+ /** La part locale a-t-elle changé depuis le dernier lot ? */
83
+ get busDirty(): boolean;
84
+ /** La vue est-elle connue incomplète (synchronisation initiale, trou chez un pair) ? */
85
+ get partial(): boolean;
86
+ /** Version courante de la vue (+1 par trame de changements). */
87
+ get version(): number;
88
+ /** Des changements attendent-ils un envoi ? */
89
+ get dirty(): boolean;
90
+ /**
91
+ * Une connexion entre.
92
+ *
93
+ * @param key - identité nommée, ou `null` pour une connexion non nommée.
94
+ */
95
+ join(key: string | null): void;
96
+ /**
97
+ * Une connexion sort.
98
+ *
99
+ * Un départ nommé attend la grâce du canal ; un départ non nommé est décompté
100
+ * tout de suite (rien ne reconnaîtrait son retour).
101
+ *
102
+ * @param key - même valeur qu'à {@link ChannelPresence.join}.
103
+ * @param now - instant monotone (ms), pour la grâce.
104
+ */
105
+ leave(key: string | null, now: number): void;
106
+ /** Publie les départs dont la grâce est écoulée. */
107
+ expire(now: number): void;
108
+ /** Prochaine échéance de grâce (ms monotone), `Infinity` sans départ retenu — O(1). */
109
+ nextDeadline(): number;
110
+ /**
111
+ * Trame complète à la version COURANTE — la liste, ou les nombres seuls au-delà du seuil.
112
+ *
113
+ * Une liste ne prend jamais de numéro : servie à un nouvel abonné, elle ferait
114
+ * voir un trou à tous les autres (ADR-0025 P2 bis).
115
+ */
116
+ listParams(): IRealtimePresenceList | IRealtimePresenceCount;
117
+ /**
118
+ * Consomme les changements en attente et rend la trame à envoyer à tous les membres, ou `null` s'il n'y en a pas.
119
+ *
120
+ * Sous le seuil : les changements, version +1. Au-delà : les nombres seuls,
121
+ * version inchangée (trame complète). Au retour sous le seuil : la liste.
122
+ * Toute trame envoyée périme la liste encodée.
123
+ */
124
+ takeChanges(): RealtimePresenceFrame | null;
125
+ /** Vue de l'observateur serveur : les membres toujours, une copie. */
126
+ view(): IChannelPresenceView;
127
+ /**
128
+ * Pose l'état « incomplet » de la vue ; un changement fait partir une trame (la version suit) et périme la liste encodée.
129
+ */
130
+ setPartial(partial: boolean): void;
131
+ /** {@inheritDoc IPresenceAggregate.remoteTabs} */
132
+ remoteTabs(key: string, delta: number): void;
133
+ /** {@inheritDoc IPresenceAggregate.remoteUnnamed} */
134
+ remoteUnnamed(delta: number): void;
135
+ /** {@inheritDoc IPresenceAggregate.remoteLeave} */
136
+ remoteLeave(key: string, tabs: number, now: number): void;
137
+ /**
138
+ * Consomme les changements de la part locale depuis le dernier lot et rend l'entrée du lot, en valeurs absolues — `null` s'ils s'annulent tous.
139
+ */
140
+ takeBusDelta(): IPresenceChannelDelta | null;
141
+ /** État COMPLET de la part locale, pour une réponse d'état (paires à plat). */
142
+ localState(): IPresenceChannelState;
143
+ /**
144
+ * Des départs LOCAUX attendent-ils la fin de leur grâce ? Le dernier client
145
+ * parti d'un pod n'y échappe pas : la présence reste servie au bus jusqu'à
146
+ * l'échéance (ADR-0025 P5), puis se libère.
147
+ */
148
+ get hasLocalGrace(): boolean;
149
+ /** La part locale compte-t-elle quelqu'un ? */
150
+ get hasLocal(): boolean;
151
+ /**
152
+ * Le canal n'est plus servi ici : toute la part locale part à zéro au prochain lot, pour que les pairs ne gardent aucun fantôme.
153
+ */
154
+ release(): void;
155
+ }
@@ -5,7 +5,7 @@
5
5
  export interface IMountedBridgeRoute {
6
6
  /** Chemin complet, préfixe du contrôleur compris (`/chat/send`). */
7
7
  readonly path: string;
8
- /** Méthodes acceptées, en capitales ; `undefined` = toutes. */
8
+ /** Méthodes acceptées, en capitales ; `undefined` = aucune déclarée (tous les verbes HTTP, jamais la socket). */
9
9
  readonly methods: ReadonlySet<string> | undefined;
10
10
  /** Schémas d'entrée de l'action (`schema` de la route), ou `null`. */
11
11
  readonly schema: {
@@ -10,16 +10,6 @@ import type { RealtimeHub } from "./RealtimeHub.js";
10
10
  * de la connexion.
11
11
  */
12
12
  export declare const MAX_RETAINED_FRAME_VERDICTS = 64;
13
- /**
14
- * Admet les trames entrantes d'un canal que sa règle `allow` vient d'ouvrir à la connexion.
15
- *
16
- * Pas de plafond propre : le nombre de canaux ouverts l'est déjà. Sans effet si le
17
- * canal n'accepte pas d'entrée client.
18
- *
19
- * @param state - état de la connexion.
20
- * @param channel - canal ouvert par `allow`.
21
- */
22
- export declare function admitByRule(state: IRealtimeConnectionState, channel: string): void;
23
13
  /**
24
14
  * Retire TOUTE admission d'un canal à la connexion — marque de sa règle ET verdict retenu du verrou — quand le serveur l'en éjecte ou le termine (ADR-0022 §6 décision 3).
25
15
  *
@@ -31,18 +21,22 @@ export declare function admitByRule(state: IRealtimeConnectionState, channel: st
31
21
  */
32
22
  export declare function revokeAdmission(state: IRealtimeConnectionState, channel: string): void;
33
23
  /**
34
- * Retire l'admission d'un canal à la fin de son abonnement — celle de sa règle
35
- * `allow`, que rien ne justifie plus. Un verdict du verrou, lui, ne tient qu'à
36
- * l'identité et survit au désabonnement.
24
+ * Retire l'admission d'un canal à la fin de son abonnement quand elle en dépendait — celle d'un canal à `allow`, dont l'abonnement est la condition.
25
+ *
26
+ * Un verdict du verrou sur un canal SANS `allow` ne tient qu'à l'identité et
27
+ * survit au désabonnement ; sur un canal à `allow`, il ne valait que le temps
28
+ * de l'abonnement (ADR-0022 §6, décisions 1 et 1 quater).
37
29
  *
38
30
  * @param state - état de la connexion.
39
31
  * @param channel - canal quitté.
32
+ * @param hub - hub qui porte les règles d'admission.
40
33
  */
41
- export declare function releaseRuleAdmission(state: IRealtimeConnectionState, channel: string): void;
34
+ export declare function releaseRuleAdmission(state: IRealtimeConnectionState, channel: string, hub: RealtimeHub): void;
42
35
  /**
43
36
  * Oublie les verdicts du verrou retenus par la connexion — leur politique vient
44
- * de changer. Les admissions par `allow` restent : elles valent autant que
45
- * l'abonnement qui les porte.
37
+ * de changer. Ceux des canaux à `allow` compris : une écriture inscrite après
38
+ * coup doit être jugée à la trame suivante, abonnement ou pas. `allow`, lui,
39
+ * n'est pas redemandé — l'abonnement reste.
46
40
  *
47
41
  * @param state - état de la connexion.
48
42
  */
@@ -55,8 +49,10 @@ export declare function dropRetainedVerdicts(state: IRealtimeConnectionState): v
55
49
  * - `ping` et `unsubscribe` passent sans règle ;
56
50
  * - `subscribe` passe par le verrou (`allow` est jugé après, à l'ouverture) ;
57
51
  * - un nom hérité du prototype de la table entrante est refusé ;
58
- * - un canal entrant à `allow` que l'abonnement n'a pas admis est refusé ;
59
- * - un canal entrant à politique fixe passe par le verrou, dont le « oui » est retenu ;
52
+ * - un canal entrant à `allow` exige l'abonnement de CETTE connexion (`allow`
53
+ * n'est jamais rappelé), puis passe par le verrou comme les autres ;
54
+ * - un canal entrant passe par le verrou — lecture ET écriture —, dont le « oui »
55
+ * est retenu ;
60
56
  * - toute autre notification passe par le verrou.
61
57
  *
62
58
  * Le premier test du contrôleur (`admitted[method] === true`) a déjà écarté les trames
@@ -0,0 +1,112 @@
1
+ import type { ChannelPresence } from "./channelPresence.js";
2
+ import { RemotePresence } from "./remotePresence.js";
3
+ /** Fenêtre des lots, demandes et réponses entre pods (ms) — ADR-0025 P2. */
4
+ export declare const PRESENCE_BATCH_MS = 50;
5
+ /** Intervalle local de la preuve de vie (ms), annoncé aux pairs — ADR-0025 P2, « Oubli ». */
6
+ export declare const PRESENCE_INTERVAL_MS = 5000;
7
+ /** Délai d'oubli d'un pair muet, en intervalles ANNONCÉS (≥ 2) — ADR-0025 P2. */
8
+ export declare const PRESENCE_FORGET_FACTOR = 3;
9
+ /**
10
+ * Ce que la présence répartie demande au hub qui la porte — le bus, et les canaux à présence DIFFUSÉS qu'il sert.
11
+ */
12
+ export interface IPresenceBusHost {
13
+ /** `originId` de ce pod (celui de son backplane). */
14
+ readonly originId: string;
15
+ /** Publie sur le bus (la file d'envoi tranche : la preuve de vie passe hors quota). */
16
+ publish(channel: string, payload: object): void;
17
+ /**
18
+ * Résout un canal reçu : sa présence s'il est servi et diffusé ici, `null` s'il déclare la présence sans être servi, `false` s'il ne la déclare pas (refus compté).
19
+ */
20
+ lookup(channel: string): ChannelPresence | null | false;
21
+ /** Présence d'un canal servi et diffusé ici, `null` sinon. */
22
+ served(channel: string): ChannelPresence | null;
23
+ /** Les présences servies et diffusées ici — itérées aux seules transitions. */
24
+ servedPresences(): Iterable<ChannelPresence>;
25
+ /** Nombre de présences servies et diffusées ici. */
26
+ servedCount(): number;
27
+ /**
28
+ * Échos de ses propres messages comptés par le pilote (`IBackplaneInfo.echoTotal`), `undefined` s'il n'en expose pas — l'oubli reste alors normal.
29
+ */
30
+ echoTotal(): number | undefined;
31
+ }
32
+ /** Réglages de la présence répartie (défauts de l'ADR). */
33
+ export interface IPresenceBusOptions {
34
+ /** Intervalle de la preuve de vie (ms). */
35
+ readonly interval?: number;
36
+ /** Délai d'oubli, en intervalles annoncés (≥ 2). */
37
+ readonly factor?: number;
38
+ /** Tirage dans [0, 1) — incarnation et gigue ; injecté par les tests. */
39
+ readonly random?: () => number;
40
+ /** Horloge murale (ms) — incarnation seulement ; injectée par les tests. */
41
+ readonly wallClock?: () => number;
42
+ }
43
+ /**
44
+ * Présence répartie d'UN pod (ADR-0025 P2) — ce qu'il émet sur le bus (lots de 50 ms, demandes d'état, réponses, preuve de vie) et ce qu'il en reçoit (vue des pairs, {@link RemotePresence}).
45
+ *
46
+ * Sans minuteur ni horloge : le hub la fait avancer à son tic UNIQUE
47
+ * ({@link PresenceBus.tick}) et arme ce tic sur {@link PresenceBus.due}, O(1).
48
+ * Elle ne parle qu'à `IBackplane` par son hôte : un pilote neuf (Kafka,
49
+ * NATS…) n'écrit rien pour la présence.
50
+ *
51
+ * Coût : rien n'existe tant qu'aucun canal à présence n'est diffusé ; un
52
+ * changement local = un ajout dans un `Set` ; un lot = un parcours des seuls
53
+ * canaux changés.
54
+ */
55
+ export declare class PresenceBus {
56
+ #private;
57
+ /** Vue des pairs. */
58
+ readonly remote: RemotePresence;
59
+ /** Vie de ce processus : `Date.now() × 1024 + aléa sur 10 bits` (entier sûr jusqu'en 2248). */
60
+ readonly incarnation: number;
61
+ /**
62
+ * @throws RangeError si le délai d'oubli est sous 2 intervalles (cf `RemotePresence`).
63
+ */
64
+ constructor(host: IPresenceBusHost, options?: IPresenceBusOptions);
65
+ /** Prochain instant (ms monotone) où le bus a quelque chose à faire — O(1). */
66
+ due(): number;
67
+ /**
68
+ * Présences à rafraîchir depuis le dernier appel (agrégat changé, état incomplet changé) — `null` si aucune ; le hub les touche.
69
+ */
70
+ takeTouched(): Set<ChannelPresence> | null;
71
+ /**
72
+ * Un canal à présence diffusé vient d'être servi (premier abonné local) : la vue reste incomplète le temps que la demande d'état groupée soit servie.
73
+ */
74
+ served(presence: ChannelPresence, now: number): void;
75
+ /** La part locale d'une présence a changé : elle part au prochain lot. */
76
+ changed(presence: ChannelPresence, now: number): void;
77
+ /**
78
+ * Applique un message du canal des lots, demandes et réponses.
79
+ *
80
+ * @returns `true` si c'est un type de message inconnu de cette version (le
81
+ * hub le compte dans `ingressUnknownDiscardedTotal`) ; `false` sinon — y
82
+ * compris refusé, compté ici (`presenceRejectedTotal`).
83
+ */
84
+ receive(origin: string, payload: unknown, now: number): boolean;
85
+ /** Applique une preuve de vie ; une forme fausse est refusée et comptée. */
86
+ receiveAlive(origin: string, payload: unknown, now: number): void;
87
+ /** Fait avancer la présence répartie à l'instant `now` (tic unique du hub). */
88
+ tick(now: number): void;
89
+ /**
90
+ * Arrêt propre (ADR-0025 P5) : le dernier lot part, puis une preuve de vie FINALE — les pairs retirent aussitôt les présents de ce pod, sans attendre le délai d'oubli. Plus rien ne part ensuite ; idempotent.
91
+ *
92
+ * Rien n'est émis si ce pod ne s'est jamais annoncé (aucune preuve de vie
93
+ * émise ni vue des pairs) : personne n'a rien à retirer.
94
+ */
95
+ end(): void;
96
+ /** Arrêt propre émis ? */
97
+ get ended(): boolean;
98
+ /** Bus perdu, constaté par l'écho : la vue est figée et `partial` (instantané). */
99
+ get busLost(): boolean;
100
+ /** Pertes du bus constatées par l'écho. */
101
+ get busLostTotal(): number;
102
+ /** Réarmements du délai d'oubli (tic en retard, retour de l'écho). */
103
+ get rearmTotal(): number;
104
+ /** Compteurs de la sonde. */
105
+ get batchTotal(): number;
106
+ /** Demandes d'état émises (ciblées et non ciblées). */
107
+ get requestTotal(): number;
108
+ /** Parts de réponse d'état émises. */
109
+ get answerTotal(): number;
110
+ /** Preuves de vie émises. */
111
+ get aliveTotal(): number;
112
+ }
@@ -0,0 +1,21 @@
1
+ import type { IBackplane } from "../../interfaces/IBackplane.js";
2
+ import type { RealtimeHub } from "./RealtimeHub.js";
3
+ /**
4
+ * Délai laissé au bus pour faire partir ce qui est déjà émis à l'arrêt (ms) — sous la limite d'arrêt du noyau (15 s) et sous le délai de grâce d'un orchestrateur.
5
+ */
6
+ export declare const BACKPLANE_STOP_TIMEOUT_MS = 2000;
7
+ /**
8
+ * Arrêt propre du temps réel d'un processus — la présence répartie annonce sa fin (dernier lot, puis preuve de vie FINALE, ADR-0025 P5), puis le bus POSSÉDÉ s'arrête en laissant partir ce qui est émis, dans une limite de temps.
9
+ *
10
+ * Sans l'attente du bus, la sortie du processus au tick suivant perdait la
11
+ * preuve de vie finale sur un bus réseau (Redis) : les pairs gardaient ce
12
+ * serveur jusqu'au délai d'oubli. Un bus fourni par l'application (`owned`
13
+ * `null`) n'est pas arrêté ici : c'est à elle de le faire.
14
+ *
15
+ * @param hub - le hub du processus.
16
+ * @param owned - le bus que l'appelant a créé et doit arrêter, `null` sinon.
17
+ * @param timeoutMs - limite de l'arrêt du bus.
18
+ * @returns `false` si le bus n'a pas fini dans la limite (ou a échoué) — ce
19
+ * qui restait peut être perdu, les pairs oublieront ce serveur au délai.
20
+ */
21
+ export declare function shutdownRealtime(hub: RealtimeHub, owned: IBackplane | null, timeoutMs?: number): Promise<boolean>;
@@ -0,0 +1,151 @@
1
+ import { type IPresenceAlive, type IPresenceRequest, type PresenceMessage } from "../backplane/presenceMessage.js";
2
+ /**
3
+ * Agrégat d'un canal servi par ce pod — ce que la vue distante ajuste quand la présence d'un pair change (ADR-0025 P2).
4
+ *
5
+ * Les deux méthodes reçoivent un DELTA : la vue distante range ce que chaque
6
+ * pod annonce (valeurs absolues) et ne transmet que la différence, si bien que
7
+ * l'agrégat n'a jamais à reparcourir les pods.
8
+ */
9
+ export interface IPresenceAggregate {
10
+ /**
11
+ * L'identité a `delta` onglets de plus (négatif : de moins) chez les pairs.
12
+ *
13
+ * @param key - identité nommée.
14
+ * @param delta - entier non nul.
15
+ */
16
+ remoteTabs(key: string, delta: number): void;
17
+ /**
18
+ * Les pairs comptent `delta` connexions non nommées de plus (négatif : de moins).
19
+ *
20
+ * @param delta - entier non nul.
21
+ */
22
+ remoteUnnamed(delta: number): void;
23
+ /**
24
+ * Un pair PART (oublié, arrêt propre, vie remplacée) avec les `tabs` onglets de l'identité qu'il annonçait — retenu par la grâce du canal s'il en déclare une (ADR-0025 P5), annulé si l'identité revient pendant ce délai.
25
+ *
26
+ * @param key - identité nommée.
27
+ * @param tabs - onglets retirés, entier > 0.
28
+ * @param now - instant monotone local (ms).
29
+ */
30
+ remoteLeave(key: string, tabs: number, now: number): void;
31
+ }
32
+ /**
33
+ * Résout un canal reçu du bus : son agrégat s'il est SERVI ici, `null` s'il déclare la présence sans être servi (lu puis jeté, sans stockage), `false` s'il ne déclare pas la présence sur ce pod (refusé, compté).
34
+ */
35
+ export type PresenceAggregateLookup = (channel: string) => IPresenceAggregate | null | false;
36
+ /** Réglages de la vue distante. */
37
+ export interface IRemotePresenceOptions {
38
+ /** Résolution d'un canal reçu (cf {@link PresenceAggregateLookup}). */
39
+ readonly lookup: PresenceAggregateLookup;
40
+ /** Délai d'oubli en intervalles ANNONCÉS par le pair (≥ 2, défaut de l'ADR : 3). */
41
+ readonly factor: number;
42
+ /**
43
+ * Intervalle LOCAL (ms) : cadence des demandes répétées pendant un trou, et
44
+ * intervalle supposé d'un pair dont aucune preuve de vie n'est encore arrivée.
45
+ */
46
+ readonly interval: number;
47
+ }
48
+ /**
49
+ * Vue de la présence des PAIRS, rangée par pod puis par canal (ADR-0025 P2) — la règle répartie, sans horloge, sans minuteur, sans bus.
50
+ *
51
+ * Elle applique ce que le bus apporte — lots chaînés par `seq`/`prev`, réponses
52
+ * d'état en parts appliquées d'un bloc, preuves de vie —, détecte les trous
53
+ * (règle pure `classifyNumberedChange`, RFC 5263 §4.5), départage les vies d'un
54
+ * même `originId` par l'incarnation, oublie un pair muet au bout de `factor`
55
+ * intervalles ANNONCÉS, et ne transmet aux agrégats que des DELTAS. L'instant
56
+ * (`now`, horloge monotone) est passé à chaque appel : la règle se prouve à
57
+ * l'horloge simulée, au compte exact.
58
+ *
59
+ * Coût : un canal non servi ne stocke rien ; oublier un pod libère ses tables
60
+ * d'un coup ; un message ne parcourt que ses propres entrées. Aucun parcours de
61
+ * tous les pods hors de {@link RemotePresence.expire},
62
+ * {@link RemotePresence.nextDeadline}, {@link RemotePresence.rearm} et
63
+ * {@link RemotePresence.takeRequests}, appelés au tic.
64
+ */
65
+ export declare class RemotePresence {
66
+ #private;
67
+ /**
68
+ * @throws RangeError si `factor` < 2 (Phoenix Tracker exige le même plancher :
69
+ * sous deux intervalles, une seule preuve de vie retardée fait oublier un pair
70
+ * vivant) ou si `interval` n'est pas un entier > 0.
71
+ */
72
+ constructor(options: IRemotePresenceOptions);
73
+ /** Pairs connus (pierres tombales comprises). */
74
+ get size(): number;
75
+ /** Pairs dont la vue est en trou — la présence servie est alors `partial`. */
76
+ get gaps(): number;
77
+ /** Messages refusés : canal non déclaré à présence ici, plafond dépassé (`presenceRejectedTotal`). */
78
+ get rejectedTotal(): number;
79
+ /** Messages déjà vus ou d'une vie plus ancienne (`presenceStaleDiscardedTotal`). */
80
+ get staleTotal(): number;
81
+ /** Pairs oubliés faute de signe de vie. */
82
+ get forgottenTotal(): number;
83
+ /** Arrêts propres reçus (preuve de vie `final`) — présents retirés sans attendre l'oubli. */
84
+ get endedTotal(): number;
85
+ /** Trous ouverts depuis le début (lot perdu, pair inconnu, vie neuve). */
86
+ get gapTotal(): number;
87
+ /**
88
+ * Compte un message de présence REFUSÉ par le lecteur à liste fermée (forme invalide) — même compteur que les refus de la vue.
89
+ */
90
+ countRejected(): void;
91
+ /**
92
+ * Applique un message du canal des lots, demandes et réponses.
93
+ *
94
+ * @param origin - `originId` de l'émetteur, lu dans l'enveloppe.
95
+ * @param message - message validé par `readPresenceMessage`.
96
+ * @param now - instant monotone local (ms).
97
+ * @returns la demande d'état si le message en est une, FRAÎCHE, à servir par
98
+ * le répondeur ; `null` sinon.
99
+ */
100
+ receive(origin: string, message: PresenceMessage, now: number): IPresenceRequest | null;
101
+ /**
102
+ * Applique une preuve de vie.
103
+ *
104
+ * @param origin - `originId` de l'émetteur.
105
+ * @param alive - preuve de vie validée par `readPresenceAlive`.
106
+ * @param now - instant monotone local (ms).
107
+ */
108
+ receiveAlive(origin: string, alive: IPresenceAlive, now: number): void;
109
+ /**
110
+ * Pairs à qui demander leur état MAINTENANT — en trou, et pas encore redemandés depuis un intervalle local ; ils sont marqués demandés.
111
+ *
112
+ * @param now - instant monotone local (ms).
113
+ * @param served - canaux à présence servis ici, nommés par la demande ; une
114
+ * réponse ne ferme le trou que si elle les couvre tous.
115
+ * @returns les `originId` à viser (`to`), `null` s'il n'y en a pas.
116
+ */
117
+ takeRequests(now: number, served: readonly string[]): string[] | null;
118
+ /**
119
+ * Oublie les pairs muets depuis `factor` intervalles ANNONCÉS, et retire les pierres tombales échues.
120
+ *
121
+ * @param now - instant monotone local (ms).
122
+ * @returns le nombre de pairs VIVANTS oubliés (hors pierres tombales).
123
+ */
124
+ expire(now: number): number;
125
+ /**
126
+ * Plus proche échéance d'oubli APPARUE depuis le dernier appel — celle d'une preuve de vie, dont l'intervalle annoncé peut être plus court que le local ; `Infinity` sinon. O(1), relevée à chaque réception pour que le porteur avance son tic sans attendre le suivant.
127
+ *
128
+ * Tout autre message recule l'échéance, et celle d'un pair neuf tombe au
129
+ * moins deux intervalles LOCAUX plus loin : le tic suivant la relève à temps.
130
+ * {@link RemotePresence.nextDeadline} reste la valeur exacte, recalculée au tic.
131
+ */
132
+ takeCloserDeadline(): number;
133
+ /** Prochaine échéance d'oubli (ms monotone), `Infinity` sans pair. */
134
+ nextDeadline(): number;
135
+ /**
136
+ * Réarme le délai d'oubli de TOUS les pairs — après un tic en retard (récepteur gelé) ou au retour de l'écho (bus revenu).
137
+ *
138
+ * Sans lui, un pod bloqué 20 s oublierait d'un coup tous les pairs vivants
139
+ * dont les preuves de vie attendent encore dans son tampon : les minuteurs
140
+ * passent avant la lecture des sockets (Lifeguard).
141
+ *
142
+ * @param now - instant monotone local (ms).
143
+ */
144
+ rearm(now: number): void;
145
+ /**
146
+ * Retire un canal de toutes les vues — il n'est plus servi ici (dernier abonné local parti). L'agrégat n'est pas notifié : il disparaît avec le canal.
147
+ */
148
+ dropChannel(channel: string): void;
149
+ /** Oublie tout, sans notifier les agrégats (arrêt, réinitialisation). */
150
+ clear(): void;
151
+ }
@@ -17,6 +17,9 @@
17
17
  /**
18
18
  * Sérialise la notification `realtime:subscribed { channel }`.
19
19
  *
20
+ * Connexion TEXTE seulement : sous codec binaire, l'accusé passe par
21
+ * `peer.notify` (`RealtimeController.ts#RealtimeController.acknowledgeSubscription`).
22
+ *
20
23
  * @param channel - le canal acquitté (entrée réseau : échappé par `JSON.stringify`).
21
24
  * @returns la trame JSON-RPC 2.0 prête à écrire sur le transport.
22
25
  */
@@ -1,4 +1,4 @@
1
- import { Service, Module, type IFrameCodec, type JsonRpcPeer } from "nodefony";
1
+ import { Service, Module, type IChannelPolicies, type IFrameCodec, type JsonRpcPeer } from "nodefony";
2
2
  import { type RealtimeHub, type ChannelFactory, type ChannelSink, type FrameAuthorizer } from "../server/RealtimeHub.js";
3
3
  import type { IBackplane } from "../../interfaces/IBackplane.js";
4
4
  import type { IRealtimeProbe } from "../../interfaces/IRealtimeProbe.js";
@@ -163,6 +163,13 @@ declare class RealtimeService extends Service {
163
163
  * @returns la politique déclarée, ou `null`.
164
164
  */
165
165
  resolveChannelPolicy(channel: string): IChannelPolicy | null;
166
+ /**
167
+ * **Seam #1b** — lecture ET écriture distincte d'un nom, en une résolution (ADR-0022 §6, décision 1 quater). Lu par `@nodefony/security` sur chaque trame jugée.
168
+ *
169
+ * @param channel - nom du canal, de la méthode entrante ou de l'action.
170
+ * @returns les deux politiques, ou `null` si aucune ne couvre ce nom.
171
+ */
172
+ resolveChannelPolicies(channel: string): IChannelPolicies | null;
166
173
  /**
167
174
  * Renvoie le token associé à un peer (posé au handshake par le hub). Jamais
168
175
  * `null` (Zero Trust : `ANONYMOUS_REALTIME_TOKEN` si aucun authenticator n'a
@@ -28,6 +28,14 @@ export interface IBackplaneConformanceOptions extends IConformanceRunner {
28
28
  quietMs?: number;
29
29
  /** Délai maximal d'arrivée d'un message attendu, en millisecondes. Défaut 2 000. */
30
30
  timeoutMs?: number;
31
+ /**
32
+ * Octets publiés d'une traite avant l'ordre du cas « ordre sous saturation ».
33
+ * Défaut : deux fois le seuil par défaut de la file d'envoi (16 Mio). Un driver
34
+ * dont la file est bornée plus haut relève cette valeur : si la rafale ne
35
+ * sature pas la file que `describe().queue` déclare, le cas échoue plutôt que
36
+ * de passer sans rien prouver.
37
+ */
38
+ floodBytes?: number;
31
39
  }
32
40
  /**
33
41
  * Fabrique d'un pair du driver, relié au MÊME bus que les autres pairs de la suite.
@@ -45,7 +53,10 @@ export declare const BACKPLANE_CONFORMANCE_CASES: {
45
53
  readonly noReplay: "pas de rejeu : un pair démarré après une publication ne la reçoit pas";
46
54
  readonly seal: "sceau : un message non scellé ou mal scellé est refusé";
47
55
  readonly control: "ordres de contrôle : le canal de contrôle traverse intact";
56
+ readonly controlUnderLoad: "ordre sous saturation : un ordre de contrôle n'est jamais jeté pour faire place aux publications";
57
+ readonly aliveUnderLoad: "preuve de vie sous saturation : la preuve de vie de la présence n'est jamais jetée pour faire place aux publications";
48
58
  readonly reserved: "espace réservé : refusé et compté à l'entrée du hub, la diffusion passe sans revenir";
59
+ readonly presence: "présence entre deux pods : chaque hub voit les présents de l'autre, l'arrêt propre les retire sans attendre l'oubli, sans doublon ni rejeu";
49
60
  readonly replaceHandler: "onMessage remplace le handler précédent";
50
61
  readonly stop: "stop coupe la réception, et se rappelle sans erreur";
51
62
  };
@@ -56,8 +67,9 @@ export declare const BACKPLANE_CONFORMANCE_CASES: {
56
67
  * Vérifie, sur plusieurs pairs reliés au même bus : la diffusion à TOUS les pairs
57
68
  * sans écho, l'ordre par émetteur, l'absence de rejeu au démarrage, le sceau
58
69
  * (message non scellé ou mal scellé refusé), le transport des ordres de contrôle,
59
- * le refus de l'espace réservé à l'entrée du hub, le remplacement du handler et
60
- * l'arrêt.
70
+ * même quand une rafale de publications sature la file d'envoi, le refus de
71
+ * l'espace réservé à l'entrée du hub, la présence répartie entre deux hubs
72
+ * (ADR-0025), le remplacement du handler et l'arrêt.
61
73
  *
62
74
  * @param name - nom du driver, affiché dans le rapport.
63
75
  * @param makePeer - fabrique d'un pair relié au bus commun.
@@ -31,7 +31,7 @@
31
31
  */
32
32
  import { type IChannelAdmission, type IRealtimeDenied } from "nodefony";
33
33
  import type { ContextType } from "@nodefony/http";
34
- import type { RealtimeController } from "../src/server/RealtimeController.js";
34
+ import { type RealtimeController } from "../src/server/RealtimeController.js";
35
35
  import { type RealtimeHub, type FrameAuthorizer } from "../src/server/RealtimeHub.js";
36
36
  import type { IRealtimeToken } from "../interfaces/IRealtimeToken.js";
37
37
  /**
@@ -0,0 +1,20 @@
1
+ import { PresenceView, type NumberedChangeOutcome, type RealtimePresenceFrame } from "nodefony";
2
+ import type { RealtimeHub } from "../src/server/RealtimeHub.js";
3
+ /** Un client de présence branché sur un hub : ses trames `realtime:presence`, sa vue, et son départ. */
4
+ export interface IPresenceClient {
5
+ /** Trames reçues, dans l'ordre. */
6
+ frames: RealtimePresenceFrame[];
7
+ /** Sort de chaque trame appliquée à {@link view} (même index que {@link frames}). */
8
+ outcomes: NumberedChangeOutcome[];
9
+ /**
10
+ * Vue du canal tenue par la vue PURE du cœur — celle du navigateur (`NodefonySocket`) : ce que le serveur écrit, le navigateur le lit ainsi.
11
+ */
12
+ view: PresenceView;
13
+ leave: () => void;
14
+ }
15
+ /**
16
+ * Abonne à `channel` une connexion de `hub` — nommée `user`, ou anonyme (`null`) — et relève les trames de présence qu'elle reçoit, lues et appliquées par le lecteur et la vue du cœur, comme le ferait un navigateur. Une trame que le lecteur refuse est un défaut du serveur : le banc lève en la citant.
17
+ */
18
+ export declare function presenceClient(hub: RealtimeHub, channel: string, user: string | null): IPresenceClient;
19
+ /** Présents nommés d'un canal vus par `hub` (identité → onglets), triés. */
20
+ export declare function presenceView(hub: RealtimeHub, channel: string): Record<string, number>;
package/docs/actions.md CHANGED
@@ -966,6 +966,7 @@ contrainte passe par `requirements`. Le chat complet, messages privés compris :
966
966
  | `-32601 method not found` | nom mal orthographié, ou action déclarée sur un **autre** endpoint | vérifier `socket.serverMethods` — c'est la liste réelle de CETTE connexion |
967
967
  | `-32603 internal error` sans détail | un `throw` ordinaire est rendu opaque au client (Zero Trust) | lever une `RpcError` avec un code et un message publiables |
968
968
  | `api.request` rejeté avec `data.status: 422` | la route invoquée valide son entrée (`schema`, ou une `ZodError` de l'action) | lire `data.fields` : chaque champ fautif, sa partie (`in`) et son message |
969
+ | `api.request` rejeté avec `data.status: 429` | la route borne le débit de l'appelant | attendre `data.retryAfter` secondes (l'en-tête `Retry-After` de la même route en HTTP) |
969
970
  | `-32001 unauthorized` sur une action légitime | le nom commence par `nodefony:`, le namespace réservé à la plateforme | renommer hors de `nodefony:`, ou obtenir `ROLE_NODEFONY_ADMIN` |
970
971
  | Une action est refusée à un anonyme alors qu'on la croyait publique | une action est fermée par défaut (`{ authenticated: true }`) | déclarer `{ authenticated: false }` pour l'ouvrir ; un rôle se pose par une règle de préfixe |
971
972
  | Un travail relancé crée deux jobs | action non idempotente rejouée après une reconnexion | action compagnon d'annulation, ou identifiant fourni par l'appelant + mémorisation |