@nodefony/realtime 10.0.0-beta.5 → 10.0.0-beta.6

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 (78) hide show
  1. package/README.md +50 -0
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/index.js +79 -12
  4. package/dist/nodefony/config/config.js +7 -0
  5. package/dist/nodefony/decorators/realtimeDecorators.js +125 -19
  6. package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
  7. package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
  8. package/dist/nodefony/src/backplane/RedisBackplane.js +143 -31
  9. package/dist/nodefony/src/backplane/controlOrder.js +57 -0
  10. package/dist/nodefony/src/backplane/envelope.js +113 -12
  11. package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
  12. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
  13. package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
  14. package/dist/nodefony/src/server/RealtimeConnection.js +110 -0
  15. package/dist/nodefony/src/server/RealtimeController.js +340 -81
  16. package/dist/nodefony/src/server/RealtimeHub.js +604 -61
  17. package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
  18. package/dist/nodefony/src/server/admissionBudget.js +176 -0
  19. package/dist/nodefony/src/server/channelPolicyRegistry.js +73 -0
  20. package/dist/nodefony/src/server/connectionAdmission.js +242 -0
  21. package/dist/nodefony/src/server/contractRoutes.js +85 -0
  22. package/dist/nodefony/src/server/deniedDetail.js +10 -3
  23. package/dist/nodefony/src/server/frameAdmission.js +168 -0
  24. package/dist/nodefony/src/server/subscribedAck.js +10 -1
  25. package/dist/nodefony/src/server/syslogUplink.js +3 -4
  26. package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
  27. package/dist/nodefony/src/service/RealtimeService.js +42 -1
  28. package/dist/nodefony/src/service/publicOrigin.js +40 -0
  29. package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
  30. package/dist/nodefony/testing/backplaneConformance.js +271 -0
  31. package/dist/nodefony/testing/index.js +21 -4
  32. package/dist/types/index.d.ts +34 -7
  33. package/dist/types/nodefony/config/config.d.ts +9 -0
  34. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +86 -6
  35. package/dist/types/nodefony/interfaces/IBackplane.d.ts +34 -50
  36. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
  37. package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
  38. package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
  39. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
  40. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
  41. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
  42. package/dist/types/nodefony/interfaces/index.d.ts +2 -1
  43. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
  44. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +79 -16
  45. package/dist/types/nodefony/src/backplane/controlOrder.d.ts +52 -0
  46. package/dist/types/nodefony/src/backplane/envelope.d.ts +34 -0
  47. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
  48. package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
  49. package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +17 -0
  50. package/dist/types/nodefony/src/server/RealtimeController.d.ts +91 -11
  51. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +321 -24
  52. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
  53. package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
  54. package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +34 -0
  55. package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
  56. package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
  57. package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
  58. package/dist/types/nodefony/src/server/frameAdmission.d.ts +70 -0
  59. package/dist/types/nodefony/src/server/subscribedAck.d.ts +7 -0
  60. package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
  61. package/dist/types/nodefony/src/service/RealtimeService.d.ts +10 -2
  62. package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
  63. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
  64. package/dist/types/nodefony/testing/backplaneConformance.d.ts +66 -0
  65. package/dist/types/nodefony/testing/index.d.ts +12 -1
  66. package/docs/actions.md +419 -18
  67. package/docs/architecture.md +83 -8
  68. package/docs/cadence-et-contre-pression.md +87 -11
  69. package/docs/canaux-a-membres.md +267 -0
  70. package/docs/cas-d-usage.md +1702 -0
  71. package/docs/configuration.md +137 -17
  72. package/docs/cookbook-chat.md +314 -85
  73. package/docs/index.md +11 -1
  74. package/docs/observabilite.md +71 -14
  75. package/docs/protocole.md +133 -24
  76. package/docs/securite.md +284 -32
  77. package/docs/vocabulaire.md +13 -12
  78. package/package.json +14 -11
@@ -25,22 +25,9 @@
25
25
  *
26
26
  * Cf vision « la socket Nodefony » + mémoire `project_cluster_backplane_vision`.
27
27
  */
28
- /**
29
- * Compteurs de la **file d'envoi** d'un backplane à transport réseau : ce qui est
30
- * publié mais pas encore acquitté par le bus. Sans borne, cette file (interne au
31
- * client réseau) grossit sans limite sous rafale — d'où le seuil et ses compteurs,
32
- * exposés jusqu'à la sonde et Studio. Cf `BackplanePublishQueue`.
33
- */
34
- export interface IBackplaneQueueInfo {
35
- /** Octets publiés en attente d'acquittement par le transport (instantané). */
36
- bytes: number;
37
- /** Seuil au-dessus duquel une publication est jetée ; `0` = illimité. */
38
- maxBytes: number;
39
- /** Publications JETÉES faute de place (monotone). `0` en régime sain. */
40
- droppedTotal: number;
41
- /** Publications REFUSÉES par le transport (bus coupé…) (monotone). */
42
- failedTotal: number;
43
- }
28
+ import type { IFrameCodec } from "nodefony";
29
+ import type { IBackplaneInfo, IBackplaneQueueInfo } from "nodefony";
30
+ export type { IBackplaneInfo, IBackplaneQueueInfo };
44
31
  /**
45
32
  * Message transporté entre pairs par le backplane. Sérialisé une fois à l'émission
46
33
  * (IPC `structuredClone` / Redis JSON) ; le `payload` doit donc être structurellement
@@ -60,44 +47,28 @@ export interface IBackplaneMessage {
60
47
  * condition (mais en fan-out **local only**).
61
48
  */
62
49
  export type BackplaneHandler = (msg: IBackplaneMessage) => void;
63
- /**
64
- * **Carte d'identité** d'un backplane — descripteur de DONNÉE (pas un log) décrivant
65
- * l'état du fond de panier. Source unique alimentant trois sorties cohérentes : la
66
- * ligne stdout au boot, le champ `backplane` de la sonde (`/api/health`) et la vue
67
- * Studio. Réutilisable par les tests / un agent IA sans dupliquer la connaissance
68
- * « c'est quoi ce backplane ».
69
- */
70
- export interface IBackplaneInfo {
71
- /** Nom du driver (clé du registre). Ex. `redis`, `cluster`, `loopback`. */
72
- driver: string;
73
- /** Nature du transport. Ex. `redis-pubsub`, `ipc`, `local`. */
74
- kind: string;
75
- /** Identité de CE pair (process/pod). Ex. `String(process.pid)`. */
76
- originId: string;
77
- /** `true` si le fan-out franchit les frontières de pod (multi-host). */
78
- crossPod: boolean;
79
- /** Canal/topic de transport, si applicable (canal Redis, topic Kafka). */
80
- channel?: string;
81
- /**
82
- * Les messages sont-ils **scellés** (authenticité de l'émetteur vérifiée) ?
83
- * Renseigné par les drivers à transport PARTAGÉ, où un tiers peut écrire :
84
- * `false` y signale un bus ouvert. Absent quand la question n'a pas de sens
85
- * (mono-process, IPC master↔workers — authentifié par construction).
86
- */
87
- sealed?: boolean;
88
- /**
89
- * État de la **file d'envoi** vers le bus (octets en vol, seuil, jetés). Renseigné
90
- * par les drivers à transport RÉSEAU, dont les publications sont acquittées de
91
- * façon asynchrone : c'est là qu'un bus lent fait grossir la mémoire du pod.
92
- * `droppedTotal > 0` = du fan-out cross-pod a été sacrifié pour tenir la mémoire.
93
- * Absent quand le transport n'a pas de file (mono-process, IPC synchrone).
94
- */
95
- queue?: IBackplaneQueueInfo;
96
- }
97
50
  /**
98
51
  * Contrat d'un backplane realtime. Implémentation **stateless du point de vue métier**
99
52
  * (l'état des canaux vit dans le hub) ; le backplane ne porte que le transport vers les
100
53
  * pairs + son identité.
54
+ *
55
+ * Exigences d'un driver (ADR-0022 §6, décision 4) — aucune ne se voit à la
56
+ * compilation, toutes se perdent en silence :
57
+ *
58
+ * - **chaque pair reçoit TOUT** : sur un journal partitionné, un groupe de lecteurs
59
+ * PAR pair — un groupe partagé répartirait les messages entre serveurs ;
60
+ * - **l'ordre par émetteur** est tenu : clé de partition = `originId`, jamais le
61
+ * canal (ADR-0021 S5) ;
62
+ * - **aucun rejeu au démarrage** : lire à partir du dernier message, jamais depuis le
63
+ * début — un ordre de fin rejoué couperait une partie rouverte ;
64
+ * - **aucune attente de regroupement** à l'émission : `publish` part tout de suite ;
65
+ * - **le sceau appliqué** dès que le bus quitte la machine (`crossPod`) :
66
+ * `sealBackplaneEnvelope` à l'émission, `openBackplaneEnvelope` à la réception ;
67
+ * - `start` après `stop` remet la réception en route sans doubler les messages ;
68
+ * - la dépendance npm du transport vit dans le driver, jamais au cœur.
69
+ *
70
+ * Un driver s'y prouve par la suite de conformité, la même que jouent les drivers
71
+ * livrés : `runBackplaneConformance` de `@nodefony/realtime/testing`.
101
72
  */
102
73
  export interface IBackplane {
103
74
  /** Identité stable de CE pair (process/pod). Discrimine l'echo. Ex. `String(process.pid)`. */
@@ -117,6 +88,19 @@ export interface IBackplane {
117
88
  * Un seul handler à la fois — un appel ultérieur remplace le précédent.
118
89
  */
119
90
  onMessage(handler: BackplaneHandler): void;
91
+ /**
92
+ * Arme le transport des **octets** (ADR-0021 S6) : après un `true`, une charge qui
93
+ * est ou contient un `Uint8Array` traverse {@link publish} intacte jusqu'aux autres
94
+ * pairs. Appelé par le hub, une fois, quand le serveur a un codec binaire.
95
+ *
96
+ * Optionnel : un driver qui ne l'implémente pas — ou qui rend `false` — garde ses
97
+ * messages texte inchangés, et le hub retient les octets sur ce serveur en
98
+ * prévenant l'exploitant une fois, plutôt que de les laisser se déformer.
99
+ *
100
+ * @param codec - le codec binaire du serveur (`CBOR_FRAME_CODEC` de `nodefony/cbor`).
101
+ * @returns `true` si les octets traverseront le bus intacts.
102
+ */
103
+ enableBytes?(codec: IFrameCodec): boolean;
120
104
  /** Arrête le transport et libère les ressources (connexions, listeners). Idempotent. */
121
105
  stop(): void | Promise<void>;
122
106
  /**
@@ -1,28 +1,8 @@
1
1
  /**
2
- * Politique d'autorisation d'un **canal** WebSocket — exigences à satisfaire
3
- * pour s'y abonner (`subscribe`) ou y pousser (canal inbound full-duplex).
2
+ * Politique d'autorisation d'un canal — déclaration UNIQUE, au cœur (`nodefony`).
4
3
  *
5
- * Déclarée côté métier via {@link RealtimeChannel}/{@link RealtimeInbound}
6
- * (`@RealtimeChannel("admin:x", { roles: ["ROLE_ADMIN"] })`), agrégée par le
7
- * {@link RealtimeHub} et **lue par `@nodefony/security`** (P6) au moment de la
8
- * frame via le seam `resolveChannelPolicy`. Le module realtime NE décide PAS de
9
- * l'autorisation (il ne connaît ni la hiérarchie de rôles ni l'identité réelle) :
10
- * il ne fait que **transporter la déclaration** jusqu'au décideur (le firewall).
11
- *
12
- * ⚠️ Cette interface est un **miroir structurel** de `IChannelPolicy` côté
13
- * `@nodefony/security` (`realtimeContracts.ts`) — les deux modules ne s'importent
14
- * pas (`package.json` disjoints). Le typage structurel de TS fait le pont à
15
- * l'exécution. Toute dérive de forme casserait le seam silencieusement.
16
- *
17
- * Champs cumulatifs (ET) ; un champ absent = pas de contrainte sur cet axe ;
18
- * objet entièrement vide = canal libre (équivaut à « pas de politique »).
4
+ * Réexportée ici pour que `import type { IChannelPolicy } from "@nodefony/realtime"`
5
+ * continue de compiler. Elle n'est plus recopiée : `@nodefony/security`, qui la
6
+ * juge, lit la même déclaration — deux copies dérivaient en silence.
19
7
  */
20
- export interface IChannelPolicy {
21
- /** Exige une connexion authentifiée (token non anonyme). */
22
- readonly authenticated?: boolean | undefined;
23
- /** Un de ces rôles suffit (évalué AVEC la hiérarchie de rôles, côté security). */
24
- readonly roles?: readonly string[] | undefined;
25
- /** Un de ces scopes suffit (axe API : JWT/clé API ; session BFF n'en porte pas). */
26
- readonly scopes?: readonly string[] | undefined;
27
- }
28
- export default IChannelPolicy;
8
+ export type { IChannelPolicy, IChannelPolicy as default } from "nodefony";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Une connexion temps réel servie par CE processus — la poignée qu'un
3
+ * gestionnaire `@RealtimeInbound` reçoit, et que les gestes du hub sur les
4
+ * membres d'un canal désignent (`RealtimeHub.subscribeConnection`,
5
+ * `RealtimeHub.evictConnection`, `RealtimeHub.channelMembers`).
6
+ *
7
+ * Elle n'existe que dans la mémoire du processus qui a accepté la socket : elle
8
+ * ne traverse pas le backplane, et un autre exemplaire (pod, `nodefony cluster
9
+ * -w N`) ne la connaît pas. Une poignée fermée, ou venue d'ailleurs, fait rendre
10
+ * `false` aux gestes du hub — jamais un geste muet.
11
+ */
12
+ export interface IRealtimeConnection {
13
+ /**
14
+ * Identifiant du compte connecté (`RealtimeIdentity.id`) — la clé des canaux
15
+ * personnels et de `RealtimeHub.evictUser`, qui l'atteint sur tous les
16
+ * exemplaires ; `null` pour l'anonyme (qu'aucun ordre par compte n'atteint),
17
+ * et pour un authentificateur qui ne produit pas d'identité d'admission.
18
+ */
19
+ readonly userId: string | null;
20
+ /** `false` une fois la socket fermée : la poignée ne désigne plus rien. */
21
+ readonly open: boolean;
22
+ }
@@ -1,6 +1,8 @@
1
- import type { JsonRpcPeer } from "nodefony";
1
+ import type { IFrameCodec, JsonRpcPeer } from "nodefony";
2
2
  import type { WsConnectionTransport } from "../src/transport/WsConnectionTransport.js";
3
3
  import type { ChannelSink } from "../src/server/RealtimeHub.js";
4
+ import type { RealtimeConnection } from "../src/server/RealtimeConnection.js";
5
+ import type { ConnectionAdmission } from "../src/server/connectionAdmission.js";
4
6
  import type { RealtimeInboundHandler } from "./IRealtimeController.js";
5
7
  /**
6
8
  * État du protocole temps réel d'UNE connexion WebSocket — posé au welcome, et
@@ -22,10 +24,47 @@ export interface IRealtimeConnectionState {
22
24
  peer: JsonRpcPeer;
23
25
  /** Le transport qui relie le pair à la socket (contre-pression, sonde). */
24
26
  transport: WsConnectionTransport;
27
+ /**
28
+ * Le codec BINAIRE accordé à cette connexion au handshake (ADR-0022 §2), `null`
29
+ * pour une connexion restée en texte. Non nul, toute trame binaire reçue est une
30
+ * frame du codec, et le flux brut (`onRealtimeBinary`, `sendBinaryToClient`) est
31
+ * refusé : les octets passent par les canaux, et leurs portes.
32
+ */
33
+ binaryCodec: IFrameCodec | null;
25
34
  /** Canal → sink de CETTE connexion auprès du hub partagé, pour s'en désabonner. */
26
35
  channels: Map<string, ChannelSink>;
27
- /** Canaux full-duplex qui acceptent une entrée client ; `null` s'il n'y en a aucun. */
36
+ /**
37
+ * Canaux full-duplex qui acceptent une entrée client ; `null` s'il n'y en a aucun.
38
+ * Objet ordinaire, de même forme pour toutes les connexions d'un contrôleur :
39
+ * un nom hérité de son prototype (`__proto__`, `toString`) est refusé par le
40
+ * verrou des trames avant d'atteindre la distribution.
41
+ */
28
42
  inbound: Record<string, RealtimeInboundHandler> | null;
43
+ /**
44
+ * File d'admission aux canaux à `allow` — `null` tant que la connexion n'a
45
+ * demandé aucun de ces canaux (la plupart ne le font jamais).
46
+ */
47
+ admission: ConnectionAdmission | null;
48
+ /**
49
+ * Canaux entrants dont les trames passent sans le verrou : ouverts par `allow`,
50
+ * ou dont le verrou a déjà dit oui (ADR-0022 §6, décision 1 ter). Le coût d'une
51
+ * trame chaude tient dans cette lecture (`admitted[method] === true`). Mêmes
52
+ * clés que {@link inbound}, à `false` tant que non admises : une seule forme
53
+ * d'objet pour toutes les connexions d'un contrôleur. `null` jusqu'à la
54
+ * première admission.
55
+ */
56
+ admitted: Record<string, boolean> | null;
57
+ /**
58
+ * Ceux de {@link admitted} que le VERROU a admis — au plus 64, vidés quand une
59
+ * politique arrive après coup. `null` s'il n'y en a aucun.
60
+ */
61
+ retained: string[] | null;
62
+ /**
63
+ * Poignée publique de la connexion (membre d'un canal, argument des
64
+ * gestionnaires `@RealtimeInbound`) — `null` jusqu'au premier abonnement ou à
65
+ * la première trame entrante qui la demande.
66
+ */
67
+ connection: RealtimeConnection | null;
29
68
  }
30
69
  declare module "@nodefony/http" {
31
70
  interface IWebsocketConnectionState {
@@ -1,5 +1,33 @@
1
- /** Publie une charge sur un canal (notification serveur→client). */
2
- export type RealtimePublish = (channel: string, payload: unknown) => void;
1
+ import type { ContractServerEmit, EventNames, EventPayload, EventsMap, IRealtimeContract } from "nodefony";
2
+ import type { IRealtimeConnection } from "./IRealtimeConnection.js";
3
+ /**
4
+ * Publie une charge sur un canal (notification serveur→client) — forme LIBRE :
5
+ * tout nom, toute charge objet. Sous un contrat, préférer {@link ContractRealtimePublish}.
6
+ */
7
+ export type RealtimePublish = (channel: string, payload?: object) => void;
8
+ /**
9
+ * Publie sur un canal DÉCLARÉ par le contrat `C` : le nom est l'un des canaux de
10
+ * `serverToClient`, la charge est celle qu'il déclare. Une faute de frappe dans
11
+ * le nom ou dans la charge ne compile pas.
12
+ *
13
+ * C'est le `publish` qu'une fabrique de canal déclare sous un contrat — méthode
14
+ * `@RealtimeChannel` ou surcharge `createRealtimeChannel`. La fonction reçue à
15
+ * l'exécution est la même qu'en forme libre ({@link RealtimePublish}) : seul le
16
+ * compilateur voit la différence. La charge est un objet : le hub refuse un
17
+ * scalaire à l'exécution (`isRealtimePayload`), le type le dit avant.
18
+ *
19
+ * @typeParam C - le contrat de l'application.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * @RealtimeChannel("chat:presence", { authenticated: true })
24
+ * presence(_channel: string, publish: ContractRealtimePublish<ChatContract>) {
25
+ * publish("chat:presence", { online: 3 });
26
+ * return () => undefined;
27
+ * }
28
+ * ```
29
+ */
30
+ export type ContractRealtimePublish<C extends IRealtimeContract> = <K extends EventNames<ContractServerEmit<C>>>(channel: K, payload: EventPayload<ContractServerEmit<C>, K> & object) => void;
3
31
  /**
4
32
  * Handler d'un message ENTRANT sur un canal full-duplex (client → serveur). C'est le
5
33
  * seam des backings entrants (SIP, bridge) : le client `publish(channel, payload)` ; si
@@ -9,10 +37,43 @@ export type RealtimePublish = (channel: string, payload: unknown) => void;
9
37
  * valider (origine non authentifiée au niveau transport).
10
38
  * - `reply` = pousse une charge serveur→client sur le MÊME canal, vers CETTE connexion
11
39
  * uniquement (ex. réponse SIP). Per-connexion (≠ fan-out).
40
+ * - `connection` = la connexion émettrice, locale à ce processus : de quoi l'abonner
41
+ * d'office à un canal (`RealtimeHub.subscribeConnection`) ou l'en éjecter. Un
42
+ * gestionnaire qui n'en a pas l'usage l'omet.
12
43
  *
13
44
  * Par défaut aucun canal n'accepte d'entrée (sûr) : un contrôleur opte explicitement.
14
45
  */
15
- export type RealtimeInboundHandler = (params: unknown, reply: (payload: unknown) => void) => void;
46
+ export type RealtimeInboundHandler = (params: unknown, reply: (payload: unknown) => void, connection: IRealtimeConnection) => void;
47
+ /**
48
+ * Handler d'entrée TYPÉ par le contrat : `params` est ce que le client a émis sur
49
+ * ce canal, lu par `Jsonify` ; `reply` pousse la charge que le serveur émet sur le
50
+ * même canal. Les paramètres restent NON FIABLES : le type n'est pas une validation.
51
+ *
52
+ * Écrit en signature de MÉTHODE pour que ses paramètres se comparent de façon
53
+ * bivariante : c'est ce qui permet à une table typée de remplacer la table
54
+ * permissive de la base, que le contrôleur lit en `unknown` à l'exécution.
55
+ */
56
+ export type TypedRealtimeInboundHandler<P, R> = {
57
+ handle(params: P, reply: (payload: R) => void, connection: IRealtimeConnection): void;
58
+ }["handle"];
59
+ /**
60
+ * Table des entrées client d'un contrôleur : un handler par canal de `Listen`,
61
+ * chacun facultatif — accepter une entrée reste un choix explicite, canal par
62
+ * canal (par défaut, un client ne peut rien pousser). `Listen` non déclaré :
63
+ * n'importe quel nom, `params: unknown`.
64
+ */
65
+ export type RealtimeInboundHandlers<Listen extends EventsMap, Emit extends EventsMap> = {
66
+ [K in EventNames<Listen>]?: TypedRealtimeInboundHandler<EventPayload<Listen, K>, K extends keyof Emit ? Emit[K] : unknown>;
67
+ };
68
+ /**
69
+ * Handler d'action RPC tel que la table de `realtimeActions()` le range. Signature
70
+ * de MÉTHODE (paramètre comparé de façon bivariante) : une table typée par le
71
+ * contrat, dont chaque handler attend sa forme, remplace ainsi la table
72
+ * permissive de la base.
73
+ */
74
+ export type RealtimeActionHandler = {
75
+ handle(params: unknown): unknown;
76
+ }["handle"];
16
77
  /**
17
78
  * IRealtimeController — contrat d'un contrôleur temps réel SERVEUR (endpoint WS
18
79
  * JSON-RPC 2.0). Le protocole (discrimination, dispatch, actions, pending, pub/sub,
@@ -13,8 +13,7 @@
13
13
  * seuil de drop). Tous les cumuls sont **monotones** → le débit/s se dérive côté
14
14
  * lecteur (delta `total`/`ts`, comme le CPU% ou le flux ORM) : 0 état de lecture.
15
15
  */
16
- import type { IProcessHealth, IProcessRich, IOrmLeanHealth, IInstanceErrorHealth } from "nodefony";
17
- import type { IBackplaneInfo } from "./IBackplane.js";
16
+ export type { IRealtimeChannelStat, IRealtimeProbe, IRealtimeHealth, IRealtimeClusterHealth, RealtimeHealthReport, } from "nodefony";
18
17
  /** Vue d'UNE connexion realtime pour la sonde (backpressure = risque #1). */
19
18
  export interface IRealtimeConnProbe {
20
19
  /** État du transport (0..3, aligné `WebSocket.readyState`). */
@@ -35,172 +34,14 @@ export interface IRealtimeConnProbe {
35
34
  * coupe un slow-consumer (`close 1013`). `> 0` = ce client n'absorbe pas le flux.
36
35
  */
37
36
  readonly dropped: number;
38
- }
39
- /** Stat per-canal du hub (fan-out). */
40
- export interface IRealtimeChannelStat {
41
- /** Nom du canal (suffixe `:<ms>` de granularité inclus). */
42
- channel: string;
43
- /** Abonnés locaux vivants (sinks) — instantané. */
44
- subscribers: number;
45
- /** Publications cumulées sur ce canal (monotone) → débit dérivé côté lecteur. */
46
- messages: number;
47
- }
48
- /**
49
- * Snapshot PUR du hub (sans identité process) — ce que renvoie
50
- * {@link RealtimeHub.probe}. Per-instance/pod (cloud-native) ; l'agrégat multi-pod
51
- * = backplane Redis / Prometheus (P13).
52
- */
53
- export interface IRealtimeProbe {
54
- /** Horodatage du snapshot (ms epoch) — base du calcul de débit côté lecteur. */
55
- ts: number;
56
- /** Canaux actifs (≥ 1 abonné) avec abonnés + publications cumulées. */
57
- channels: IRealtimeChannelStat[];
58
- /** Nombre de canaux actifs (= 1 provider/canal/pod). */
59
- channelCount: number;
60
- /** Appels `publish` cumulés (monotone). */
61
- publishTotal: number;
62
- /** Livraisons cumulées (`publish` × abonnés) — vrai coût de fan-out (monotone). */
63
- fanoutTotal: number;
64
- /** Frames entrantes full-duplex cumulées (canaux gated SIP/bridge) (monotone). */
65
- inboundTotal: number;
66
- /**
67
- * Messages d'ingress backplane REFUSÉS (canal non déclaré broadcast) — monotone.
68
- * Reste à 0 en fonctionnement normal : un pair légitime n'émet que des canaux
69
- * broadcast. Un compteur qui décolle = pair mal configuré, ou écriture tierce
70
- * dans un bus partagé (Redis mutualisé). Cf `RealtimeHub` (contrôle d'admission).
71
- */
72
- ingressRejectedTotal: number;
73
37
  /**
74
- * Abonnements clients REFUSÉS par le plancher des canaux de plateforme —
75
- * monotone. Un canal réservé (`syslog:`, `orm:`…) a été demandé alors qu'aucun
76
- * module de sécurité n'est chargé : sans identité vérifiable, le hub ferme.
77
- * `> 0` signale une configuration incomplète (sécurité absente) autant qu'une
78
- * tentative d'accès — dans les deux cas, quelque chose est à regarder.
38
+ * Cumul de trames BINAIRES reçues et écartées faute de lecteur (monotone) :
39
+ * connexion texte sans `onRealtimeBinary`. Absent = la connexion ne compte pas.
79
40
  */
80
- systemFloorDeniedTotal: number;
81
- /** Connexions realtime vivantes. */
82
- connectionCount: number;
83
- /** Somme cumulée des octets envoyés, toutes connexions (monotone). */
84
- bytesSentTotal: number;
85
- /** Somme cumulée des frames envoyées, toutes connexions (monotone). */
86
- messagesSentTotal: number;
87
- /** Backpressure agrégée (instantanée) — risque #1. */
88
- backpressure: {
89
- /** Pire `bufferedAmount` parmi les connexions (octets). */
90
- maxBufferedAmount: number;
91
- /** Somme des `bufferedAmount` (octets en attente process-wide). */
92
- totalBufferedAmount: number;
93
- /** Connexions au-dessus du seuil d'alerte slow-consumer. */
94
- slowConsumers: number;
95
- /**
96
- * Cumul de frames jetées par back-pressure (drop latest-wins + close
97
- * slow-consumer), toutes connexions (monotone) → taux de perte dérivé côté
98
- * lecteur. `0` en régime sain ; croît = des clients lents sont contenus pour
99
- * protéger la mémoire process (sinon file `ws` non bornée → OOM).
100
- */
101
- drops: number;
102
- };
41
+ readonly binaryDiscarded?: number;
103
42
  /**
104
- * Carte d'identité du backplane effectif (driver, transport, cross-pod, canal).
105
- * Reflète l'état RÉEL : `local` si le hub n'a aucun backplane (mono-process ou
106
- * fallback fail-soft), sinon le descripteur du driver branché. Optionnel pour
107
- * rétrocompat des consommateurs qui construisent un probe partiel.
43
+ * Cumul de trames reçues et écartées parce qu'illisibles par le codec de la
44
+ * connexion (monotone). Absent = la connexion ne compte pas.
108
45
  */
109
- backplane?: IBackplaneInfo;
110
- }
111
- /** Snapshot enrichi de l'identité d'instance — santé per-instance d'un worker/pod. */
112
- export interface IRealtimeHealth extends IRealtimeProbe {
113
- /** Identifiant de CE process/pod (per-instance). */
114
- instanceId: string;
115
- /**
116
- * Santé PROCESS du worker (CPU/mém/event-loop) — additif : la grille « salle des
117
- * machines » de la vue pod lit `instances[].process` par worker. Optionnel (absent si
118
- * sonde process coupée) → les consommateurs realtime existants l'ignorent (non-breaking).
119
- */
120
- process?: IProcessHealth;
121
- /**
122
- * Sonde process **riche** (GC/heap-spaces/handles/ELU active-idle/ctx-switches) —
123
- * présente UNIQUEMENT pendant un drill-down de CE worker (Phase 2 : le master a envoyé
124
- * `nf:probe:enrich` à ce pid). Absente sinon → « on paie ce qu'on regarde » (0 surcoût
125
- * hors drill). Fusionnée côté front avec `process` pour la vue Supervision complète.
126
- */
127
- rich?: IProcessRich;
128
- /**
129
- * Santé ORM **lean** du worker (connecteurs/requêtes/erreurs/latence EWMA) — additif,
130
- * comme `process`. Présent si un driver ORM a branché sa sonde (`setOrmHealthProvider`),
131
- * absent sinon → les consommateurs realtime l'ignorent (non-breaking). Agrégée pod dans
132
- * {@link IRealtimeClusterHealth.totals}.`orm`.
133
- */
134
- orm?: IOrmLeanHealth;
135
- /**
136
- * Compteurs d'erreurs Syslog du worker (ERROR/CRITIC cumulés) — additif. Permet une
137
- * carte « erreurs par worker » + un taux d'erreur pod (delta côté lecteur). Absent si la
138
- * sonde n'a pu lire le syslog du kernel.
139
- */
140
- errors?: IInstanceErrorHealth;
141
- /**
142
- * Drivers de backplane **enregistrés dans ce process** (clés du registre ouvert :
143
- * natifs + ceux qu'une application a ajoutés par `registerBackplaneDriver`). Sert la
144
- * même question que la liste des backends côté stores : « qu'est-ce que je POURRAIS
145
- * brancher ? », à laquelle `backplane.driver` (l'actif) ne répond pas. Absent si le
146
- * registre n'est pas joignable.
147
- */
148
- backplaneDrivers?: string[];
149
- /**
150
- * Diagnostic ORM **riche** du worker (par connecteur : ping/latence/stockage/pool +
151
- * flux requêtes) — blob OPAQUE `{ health, flow }` produit par le driver, présent
152
- * UNIQUEMENT pendant un drill ORM de CE worker (facette `"orm"` : le master a envoyé
153
- * `nf:probe:enrich {facet:"orm"}` à ce pid). Absent sinon → « on paie ce qu'on regarde »
154
- * (0 ping ORM hors drill). Consommé par la page drill `/nodefony/orm/<pid>` (canal
155
- * `nodefony:orm:rich@<pid>`) → diagnostic riche du pid EXACT (≠ round-robin).
156
- */
157
- ormRich?: unknown;
158
- }
159
- /**
160
- * Vue **POD agrégée** de la socket (Phase 4c, mode cluster) — la santé de TOUS les workers
161
- * du pod consolidée. Servie par n'importe quel worker via l'endpoint santé quand le master
162
- * pousse le snapshot agrégé (sinon on retombe sur {@link IRealtimeHealth} per-instance).
163
- *
164
- * `cluster: true` est le discriminant : un consommateur (Studio) distingue la vue pod de la
165
- * vue per-instance. `instances` garde le détail par worker (drill-down) ; `totals` somme les
166
- * scalaires (la backpressure `maxBufferedAmount` est un MAX, pas une somme — c'est le pire pod).
167
- */
168
- export interface IRealtimeClusterHealth {
169
- /** Discriminant : vue agrégée multi-worker (≠ {@link IRealtimeHealth} per-instance). */
170
- cluster: true;
171
- /** Horodatage du snapshot agrégé (ms epoch). */
172
- ts: number;
173
- /** Nombre de workers présents dans l'agrégat. */
174
- instanceCount: number;
175
- /** Santé par worker (détail per-instance pour le drill-down). */
176
- instances: IRealtimeHealth[];
177
- /** Totaux pod (sommes des scalaires ; backpressure max = pire worker). */
178
- totals: {
179
- channelCount: number;
180
- publishTotal: number;
181
- fanoutTotal: number;
182
- inboundTotal: number;
183
- /** Ingress backplane refusés, tous workers (signal de sécurité pod-wide). */
184
- ingressRejectedTotal: number;
185
- connectionCount: number;
186
- bytesSentTotal: number;
187
- messagesSentTotal: number;
188
- backpressure: {
189
- /** Pire `maxBufferedAmount` parmi les workers (octets) — pas une somme. */
190
- maxBufferedAmount: number;
191
- /** Somme des octets en attente, tous workers. */
192
- totalBufferedAmount: number;
193
- /** Total des slow-consumers du pod. */
194
- slowConsumers: number;
195
- /** Total des frames jetées par back-pressure (drop + close), tous workers. */
196
- drops: number;
197
- };
198
- /**
199
- * Santé ORM agrégée du pod (sommes ; `maxEwmaMs` = pire worker). Présent si ≥ 1 worker
200
- * a remonté sa sonde ORM, absent sinon.
201
- */
202
- orm?: IOrmLeanHealth;
203
- /** Erreurs Syslog agrégées du pod (sommes). Présent si ≥ 1 worker a remonté ses compteurs. */
204
- errors?: IInstanceErrorHealth;
205
- };
46
+ readonly malformedDiscarded?: number;
206
47
  }
@@ -1,3 +1,4 @@
1
+ import type { ISubscriberIdentity, TokenStatus } from "nodefony";
1
2
  /**
2
3
  * Jeton realtime — surface MINIMALE de l'identité d'une connexion WebSocket.
3
4
  *
@@ -45,6 +46,15 @@ export interface IRealtimeToken {
45
46
  * (publish/subscribe), payé seulement sur `api.request`.
46
47
  */
47
48
  isValid?(nowMs?: number): boolean | Promise<boolean>;
49
+ /**
50
+ * La CAUSE de l'état de l'identité, quand elle est connaissable : `active`,
51
+ * `expired` (jeton porteur arrivé à `exp`) ou `revoked` (révoqué l'emporte sur
52
+ * expiré). Le hub en tire le code de fermeture d'une socket ouverte : `expired` →
53
+ * `TOKEN_EXPIRED_CLOSE_CODE` (4006), sinon `IDENTITY_DEAD_CLOSE_CODE` (4001) —
54
+ * ADR-0021 §S11. Optionnel : absent, `isValid` décide et son `false` vaut 4001
55
+ * (une session ne sait pas dire si elle a expiré ou été détruite).
56
+ */
57
+ status?(nowMs?: number): TokenStatus | Promise<TokenStatus>;
48
58
  /**
49
59
  * Le COMPTE qui a ouvert la socket existe-t-il toujours, actif, et est-ce
50
60
  * bien le même ? `false` si le compte a disparu, a été désactivé ou
@@ -58,4 +68,15 @@ export interface IRealtimeToken {
58
68
  * qui le paierait pour chaque socket ouverte, à chaque tour.
59
69
  */
60
70
  confirmAccount?(): Promise<boolean>;
71
+ /**
72
+ * Identité d'ADMISSION de la connexion — la donnée plate que reçoit la règle `allow` d'un canal, construite UNE fois et gelée.
73
+ *
74
+ * Rend toujours la MÊME référence : elle est lue au `welcome` puis à chaque
75
+ * admission, jamais recalculée. Optionnelle pour un authentificateur tiers ;
76
+ * absente, la connexion n'entre sur AUCUN canal qui déclare `allow` (refus
77
+ * `forbidden` et une ligne de journal) — jamais un `id` déduit de
78
+ * {@link getUserIdentifier}, qui est un login et se réattribue
79
+ * (ADR-0021 §S10).
80
+ */
81
+ getIdentity?(): ISubscriberIdentity;
61
82
  }
@@ -9,9 +9,10 @@
9
9
  * `@nodefony/security` (P6) les implémente sans coupler ce module au security.
10
10
  */
11
11
  export type { IBackplane, IBackplaneMessage, BackplaneHandler, } from "./IBackplane.js";
12
- export type { IRealtimeController, RealtimePublish, RealtimeInboundHandler, } from "./IRealtimeController.js";
12
+ export type { IRealtimeController, RealtimePublish, ContractRealtimePublish, RealtimeInboundHandler, } from "./IRealtimeController.js";
13
13
  export type { IChannelPolicy } from "./IChannelPolicy.js";
14
14
  export type { IRealtimeConnectionState } from "./IRealtimeConnectionState.js";
15
+ export type { IRealtimeConnection } from "./IRealtimeConnection.js";
15
16
  export type { IRealtimeProbe, IRealtimeHealth, IRealtimeClusterHealth, IRealtimeChannelStat, IRealtimeConnProbe, } from "./IRealtimeProbe.js";
16
17
  export type { IRealtimeToken } from "./IRealtimeToken.js";
17
18
  export type { IRealtimeHandshake } from "./IRealtimeHandshake.js";
@@ -2,8 +2,11 @@ import type { IBackplane, BackplaneHandler, IBackplaneMessage, IBackplaneInfo }
2
2
  import { CLUSTER_RT_KIND } from "nodefony";
3
3
  /**
4
4
  * Enveloppe IPC d'une publication realtime = {@link IBackplaneMessage} + discriminant.
5
- * Sérialisée une fois par `process.send`/`worker.send` (`structuredClone` IPC) → le
6
- * `payload` doit rester structurellement clonable (cf contrat IBackplane).
5
+ * Sérialisée une fois par `process.send`/`worker.send`. Le master de Nodefony forke
6
+ * ses workers en sérialisation `advanced` (clonage structuré,
7
+ * `src/nodefony/src/service/cluster/clusterMaster.ts#startClusterMaster`) : un
8
+ * `Uint8Array` y traverse intact, sans codec — l'IPC ne quitte pas la machine, il
9
+ * n'a ni sceau ni second sujet à porter.
7
10
  */
8
11
  export interface ClusterBackplaneEnvelope extends IBackplaneMessage {
9
12
  readonly kind: typeof CLUSTER_RT_KIND;
@@ -26,6 +29,11 @@ export interface IClusterBackplaneTransport {
26
29
  * Appelé une seule fois, au `start()`.
27
30
  */
28
31
  onReceive(cb: (msg: unknown) => void): void;
32
+ /**
33
+ * Le canal porte-t-il un `Uint8Array` intact (sérialisation `advanced`) ?
34
+ * Optionnel : absent, les octets restent sur ce process.
35
+ */
36
+ carriesBytes?(): boolean;
29
37
  }
30
38
  /**
31
39
  * Transport de production : `process.send` (émission) + `process.on("message")`
@@ -60,7 +68,7 @@ export declare class ClusterBackplane implements IBackplane {
60
68
  static readonly driver = "cluster";
61
69
  readonly originId: string;
62
70
  constructor(transport?: IClusterBackplaneTransport, originId?: string);
63
- /** Branche la réception IPC. Idempotent. */
71
+ /** Branche la réception IPC. Idempotent, et redémarrable après {@link stop}. */
64
72
  start(): void;
65
73
  /**
66
74
  * Propage une publication locale aux autres pairs via le master. NE refait PAS le
@@ -69,7 +77,17 @@ export declare class ClusterBackplane implements IBackplane {
69
77
  publish(channel: string, payload: unknown): void;
70
78
  /** Un seul handler d'ingress ; un appel ultérieur remplace le précédent. */
71
79
  onMessage(handler: BackplaneHandler): void;
72
- /** Détache le handler. Le listener du transport prod (`process`) survit au process. */
80
+ /**
81
+ * Les octets traversent l'IPC tels quels quand le canal est en clonage
82
+ * structuré : aucun codec n'est appliqué, l'argument est ignoré.
83
+ *
84
+ * @returns `true` si le transport constate un canal `advanced`.
85
+ */
86
+ enableBytes(): boolean;
87
+ /**
88
+ * Coupe la réception et détache le handler. Le récepteur du transport survit
89
+ * (`process` ne le retire pas) mais ne livre plus rien jusqu'au prochain {@link start}.
90
+ */
73
91
  stop(): void;
74
92
  describe(): IBackplaneInfo;
75
93
  }