@nodefony/realtime 10.0.0-beta.4 → 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.
- package/README.md +79 -10
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.157.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +79 -12
- package/dist/nodefony/config/config.js +7 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +125 -19
- package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
- package/dist/nodefony/src/backplane/RedisBackplane.js +143 -31
- package/dist/nodefony/src/backplane/controlOrder.js +57 -0
- package/dist/nodefony/src/backplane/envelope.js +113 -12
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
- package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
- package/dist/nodefony/src/server/RealtimeConnection.js +110 -0
- package/dist/nodefony/src/server/RealtimeController.js +344 -85
- package/dist/nodefony/src/server/RealtimeHub.js +604 -61
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
- package/dist/nodefony/src/server/admissionBudget.js +176 -0
- package/dist/nodefony/src/server/channelPolicyRegistry.js +73 -0
- package/dist/nodefony/src/server/connectionAdmission.js +242 -0
- package/dist/nodefony/src/server/contractRoutes.js +85 -0
- package/dist/nodefony/src/server/deniedDetail.js +10 -3
- package/dist/nodefony/src/server/frameAdmission.js +168 -0
- package/dist/nodefony/src/server/subscribedAck.js +10 -1
- package/dist/nodefony/src/server/syslogUplink.js +3 -4
- package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
- package/dist/nodefony/src/service/RealtimeService.js +42 -1
- package/dist/nodefony/src/service/publicOrigin.js +40 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
- package/dist/nodefony/testing/backplaneConformance.js +271 -0
- package/dist/nodefony/testing/index.js +21 -4
- package/dist/types/index.d.ts +34 -7
- package/dist/types/nodefony/config/config.d.ts +9 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +86 -6
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +34 -50
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
- package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +79 -16
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +52 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +34 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
- package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +17 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +93 -13
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +321 -24
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
- package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +34 -0
- package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +70 -0
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +7 -0
- package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +10 -2
- package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +66 -0
- package/dist/types/nodefony/testing/index.d.ts +17 -4
- package/docs/actions.md +442 -19
- package/docs/architecture.md +83 -8
- package/docs/cadence-et-contre-pression.md +87 -11
- package/docs/canaux-a-membres.md +267 -0
- package/docs/cas-d-usage.md +1702 -0
- package/docs/configuration.md +137 -17
- package/docs/cookbook-chat.md +333 -85
- package/docs/index.md +11 -1
- package/docs/observabilite.md +71 -14
- package/docs/protocole.md +133 -24
- package/docs/securite.md +294 -33
- package/docs/vocabulaire.md +13 -12
- package/package.json +14 -11
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IBackplane, BackplaneHandler, IBackplaneInfo } from "../../interfaces/IBackplane.js";
|
|
2
|
+
import { type IFrameCodec } from "nodefony";
|
|
2
3
|
import { type BackplaneNotice } from "./publishQueue.js";
|
|
3
4
|
/**
|
|
4
5
|
* BASE du canal Redis **dédié** transportant les enveloppes realtime entre pods.
|
|
@@ -22,6 +23,17 @@ export declare const REDIS_RT_CHANNEL = "nodefony:realtime";
|
|
|
22
23
|
* @returns le nom du canal Redis à `SUBSCRIBE`/`PUBLISH`.
|
|
23
24
|
*/
|
|
24
25
|
export declare function resolveRedisChannel(namespace?: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Construit le **sujet binaire** du bus (ADR-0021 S6) : le canal texte suffixé du
|
|
28
|
+
* protocole du codec. Un second sujet, versionné par son nom, que les exemplaires
|
|
29
|
+
* plus anciens n'écoutent pas — le message texte n'est pas touché, et un codec
|
|
30
|
+
* binaire d'un autre format aurait son propre sujet.
|
|
31
|
+
*
|
|
32
|
+
* @param redisChannel - le canal texte effectif ({@link resolveRedisChannel}).
|
|
33
|
+
* @param codec - le codec binaire du serveur.
|
|
34
|
+
* @returns le nom du canal Redis qui porte les enveloppes binaires.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveRedisBytesChannel(redisChannel: string, codec: IFrameCodec): string;
|
|
25
37
|
/**
|
|
26
38
|
* Transport pub/sub du backplane Redis — abstrait `PUBLISH` / `SUBSCRIBE` derrière
|
|
27
39
|
* un seam **injectable** (même pattern que `IClusterBackplaneTransport`). Permet de
|
|
@@ -39,12 +51,18 @@ export interface IRedisBackplaneTransport {
|
|
|
39
51
|
* {@link BackplanePublishQueue}). Un transport synchrone (bus mémoire de test) rend
|
|
40
52
|
* `void` — il n'a pas de file, la borne reste alors inerte.
|
|
41
53
|
*/
|
|
42
|
-
publish(channel: string, message: string): void | Promise<unknown>;
|
|
54
|
+
publish(channel: string, message: string | Uint8Array): void | Promise<unknown>;
|
|
43
55
|
/**
|
|
44
56
|
* Abonne au canal Redis. Le listener reçoit le message brut (string) ; le tri
|
|
45
57
|
* (parse + anti-echo) est fait par le backplane. Async (Redis) ou sync (fake).
|
|
46
58
|
*/
|
|
47
59
|
subscribe(channel: string, onMessage: (message: string) => void): void | Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Abonne au canal Redis en mode **octets** : le listener reçoit le message tel
|
|
62
|
+
* que publié, sans décodage UTF-8 (sujet binaire, ADR-0021 S6). Optionnel : un
|
|
63
|
+
* transport qui ne l'offre pas laisse les octets sur le serveur qui les publie.
|
|
64
|
+
*/
|
|
65
|
+
subscribeBytes?(channel: string, onMessage: (message: Uint8Array) => void): void | Promise<void>;
|
|
48
66
|
/** Désabonne du canal Redis et libère le listener associé. Idempotent. */
|
|
49
67
|
unsubscribe(channel: string): void | Promise<void>;
|
|
50
68
|
}
|
|
@@ -54,18 +72,53 @@ export interface IRedisBackplaneTransport {
|
|
|
54
72
|
* en dépendance de `@nodefony/realtime`.
|
|
55
73
|
*/
|
|
56
74
|
export interface IRedisPublisher {
|
|
57
|
-
publish(channel: string, message: string): unknown;
|
|
75
|
+
publish(channel: string, message: string | Buffer): unknown;
|
|
58
76
|
}
|
|
77
|
+
/** Écouteur texte d'un client Redis abonné. */
|
|
78
|
+
type RedisTextListener = (message: string, channel: string) => unknown;
|
|
79
|
+
/** Écouteur octets d'un client Redis abonné (`bufferMode`). */
|
|
80
|
+
type RedisBytesListener = (message: Buffer, channel: Buffer) => unknown;
|
|
59
81
|
/**
|
|
60
82
|
* Client Redis **subscriber** dédié — surface structurelle minimale (compatible
|
|
61
83
|
* `RedisClientType` v5). Un client abonné ne peut plus émettre de commandes
|
|
62
84
|
* normales (protocole Redis) → publisher et subscriber sont 2 connexions distinctes
|
|
63
85
|
* (cf `@nodefony/redis` : connexions `publish` / `subscribe`).
|
|
86
|
+
*
|
|
64
87
|
*/
|
|
65
88
|
export interface IRedisSubscriber {
|
|
66
|
-
subscribe(channel: string, listener:
|
|
67
|
-
unsubscribe(channel: string, listener?:
|
|
89
|
+
subscribe(channel: string, listener: RedisTextListener): unknown;
|
|
90
|
+
unsubscribe(channel: string, listener?: RedisTextListener): unknown;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Client Redis abonné en mode **octets** — le troisième argument `true`
|
|
94
|
+
* (`bufferMode` de node-redis) rend les messages en `Buffer`, sans décodage
|
|
95
|
+
* UTF-8 : c'est lui qui porte le sujet binaire intact (ADR-0021 S6).
|
|
96
|
+
*
|
|
97
|
+
* Interface SÉPARÉE de {@link IRedisSubscriber}, et non une surcharge de
|
|
98
|
+
* celle-ci : les deux formes réunies dans une même interface ne sont plus
|
|
99
|
+
* satisfaites ni par le type de node-redis (générique sur le mode), ni par une
|
|
100
|
+
* doublure de test qui ne connaît que le texte. Séparées, chacune l'est.
|
|
101
|
+
*/
|
|
102
|
+
export interface IRedisBytesSubscriber {
|
|
103
|
+
subscribe(channel: string, listener: RedisBytesListener, bufferMode: true): unknown;
|
|
104
|
+
unsubscribe(channel: string, listener: RedisBytesListener, bufferMode: true): unknown;
|
|
68
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Vrai si la valeur expose la surface d'un client Redis publisher — vérifié au
|
|
108
|
+
* démarrage, sur le client que rend le service `redis`.
|
|
109
|
+
*/
|
|
110
|
+
export declare function isRedisPublisher(value: unknown): value is IRedisPublisher;
|
|
111
|
+
/** Vrai si la valeur expose la surface d'un client Redis subscriber dédié. */
|
|
112
|
+
export declare function isRedisSubscriber(value: unknown): value is IRedisSubscriber;
|
|
113
|
+
/**
|
|
114
|
+
* Vrai si la valeur expose la surface d'un client abonné capable du mode octets.
|
|
115
|
+
*
|
|
116
|
+
* La forme ne dit pas si le troisième argument est compris : ce garde vaut pour
|
|
117
|
+
* le client que rend le service `redis` (node-redis, qui l'accepte). Un client qui
|
|
118
|
+
* l'ignorerait livrerait du texte au sujet binaire — illisible pour le codec,
|
|
119
|
+
* donc jeté, compté sous un secret (`sealRejectedTotal`), jamais réinjecté.
|
|
120
|
+
*/
|
|
121
|
+
export declare function isRedisBytesSubscriber(value: unknown): value is IRedisBytesSubscriber;
|
|
69
122
|
/**
|
|
70
123
|
* Adapte deux clients Redis (publisher + subscriber dédié) au seam
|
|
71
124
|
* {@link IRedisBackplaneTransport}. SEUL point de contact avec la lib `redis` —
|
|
@@ -80,17 +133,17 @@ export interface IRedisSubscriber {
|
|
|
80
133
|
* defineRealtimeConfig({ backplane: { driver: "redis" } }, { backplane: bp });
|
|
81
134
|
* ```
|
|
82
135
|
*
|
|
83
|
-
*
|
|
84
|
-
* (n'arrache pas d'éventuels autres abonnés du même client).
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*
|
|
88
|
-
*
|
|
136
|
+
* Chaque listener Redis est conservé, par canal, pour un `unsubscribe(channel,
|
|
137
|
+
* listener)` ciblé (n'arrache pas d'éventuels autres abonnés du même client).
|
|
138
|
+
*
|
|
139
|
+
* @param publisher - connexion de publication.
|
|
140
|
+
* @param subscriber - connexion d'abonnement dédiée.
|
|
141
|
+
* @param bytesSubscriber - la MÊME connexion d'abonnement, vue en mode octets
|
|
142
|
+
* ({@link IRedisBytesSubscriber}) ; absente, le transport n'offre pas de sujet
|
|
143
|
+
* binaire et les octets restent sur le serveur qui les publie.
|
|
144
|
+
* @returns le transport du {@link RedisBackplane}.
|
|
89
145
|
*/
|
|
90
|
-
export declare function
|
|
91
|
-
/** Vrai si la valeur expose la surface d'un client Redis subscriber dédié. */
|
|
92
|
-
export declare function isRedisSubscriber(value: unknown): value is IRedisSubscriber;
|
|
93
|
-
export declare function createRedisServiceTransport(publisher: IRedisPublisher, subscriber: IRedisSubscriber): IRedisBackplaneTransport;
|
|
146
|
+
export declare function createRedisServiceTransport(publisher: IRedisPublisher, subscriber: IRedisSubscriber, bytesSubscriber?: IRedisBytesSubscriber): IRedisBackplaneTransport;
|
|
94
147
|
/**
|
|
95
148
|
* Réglages optionnels du driver — regroupés en objet pour ne pas allonger une
|
|
96
149
|
* liste de paramètres positionnels déjà à quatre entrées.
|
|
@@ -103,8 +156,8 @@ export interface IRedisBackplaneOptions {
|
|
|
103
156
|
*/
|
|
104
157
|
maxQueueBytes?: number;
|
|
105
158
|
/**
|
|
106
|
-
* Annonce des transitions de la file (saturation / retour à la normale)
|
|
107
|
-
* wiring y branche le syslog du module. Omis = silencieux : acceptable en test,
|
|
159
|
+
* Annonce des transitions de la file (saturation / retour à la normale) et des
|
|
160
|
+
* sceaux refusés à l'entrée — le wiring y branche le syslog du module. Omis = silencieux : acceptable en test,
|
|
108
161
|
* jamais en production (une dégradation doit être annoncée).
|
|
109
162
|
*/
|
|
110
163
|
onNotice?: BackplaneNotice;
|
|
@@ -152,6 +205,16 @@ export declare class RedisBackplane implements IBackplane {
|
|
|
152
205
|
* un 2ᵉ appel concurrent ne ré-abonne pas).
|
|
153
206
|
*/
|
|
154
207
|
start(): Promise<void>;
|
|
208
|
+
/**
|
|
209
|
+
* Arme le sujet binaire (ADR-0021 S6) : les charges qui contiennent des octets
|
|
210
|
+
* partent en enveloppe encodée par `codec` sur un second canal Redis, et ce
|
|
211
|
+
* canal est écouté. Les charges sans octets gardent le sujet texte, intact pour
|
|
212
|
+
* les exemplaires plus anciens. Idempotent.
|
|
213
|
+
*
|
|
214
|
+
* @param codec - le codec binaire du serveur.
|
|
215
|
+
* @returns `false` si le transport ne sait pas s'abonner en octets.
|
|
216
|
+
*/
|
|
217
|
+
enableBytes(codec: IFrameCodec): boolean;
|
|
155
218
|
/**
|
|
156
219
|
* Propage une publication locale aux autres pods via Redis. NE refait PAS le
|
|
157
220
|
* fan-out local (déjà fait par le hub). Sérialisé une fois ici.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordres de contrôle entre serveurs — la LISTE FERMÉE de ce qui circule sur le
|
|
3
|
+
* canal `PLATFORM_BUS_CHANNELS.control` (ADR-0022 §6 décision 4).
|
|
4
|
+
*
|
|
5
|
+
* Un ordre ne fait qu'ENLEVER : terminer un canal, éjecter un compte d'un
|
|
6
|
+
* canal. Aucun n'admet — seul `allow` fait entrer. Il voyage dans la charge
|
|
7
|
+
* d'un message ordinaire du bus, donc sous le sceau de l'enveloppe
|
|
8
|
+
* (`envelope.ts#sealBackplaneEnvelope`), sans champ neuf dans l'enveloppe ni
|
|
9
|
+
* méthode neuve dans `IBackplane` : un driver tiers le transporte sans le savoir.
|
|
10
|
+
*/
|
|
11
|
+
/** Longueur maximale d'un champ texte d'un ordre (nom de canal, identifiant de compte). */
|
|
12
|
+
export declare const MAX_CONTROL_FIELD_LENGTH = 1024;
|
|
13
|
+
/** Terminer un canal sur chaque serveur : ses membres reçoivent `realtime:closed { reason: "ended" }`. */
|
|
14
|
+
export interface ICloseChannelOrder {
|
|
15
|
+
/** Discriminant de l'ordre. */
|
|
16
|
+
readonly op: "close";
|
|
17
|
+
/** Nom exact du canal. */
|
|
18
|
+
readonly channel: string;
|
|
19
|
+
}
|
|
20
|
+
/** Éjecter d'un canal, sur chaque serveur, toutes les connexions d'un compte. */
|
|
21
|
+
export interface IEvictUserOrder {
|
|
22
|
+
/** Discriminant de l'ordre. */
|
|
23
|
+
readonly op: "evictUser";
|
|
24
|
+
/** Nom exact du canal. */
|
|
25
|
+
readonly channel: string;
|
|
26
|
+
/** Identifiant du compte (`IRealtimeConnection.userId`). */
|
|
27
|
+
readonly user: string;
|
|
28
|
+
/**
|
|
29
|
+
* Génération d'appartenance visée — RÉSERVÉE : aucun exemplaire 10.0 ne
|
|
30
|
+
* l'émet, et un exemplaire qui ne sait pas s'en servir l'ignore et éjecte
|
|
31
|
+
* toutes les générations (plus large, jamais moins). Elle portera en 10.x la
|
|
32
|
+
* parade au rejeu d'une éjection sur un canal durable (ADR-0022 §6 d4, S-rt-48).
|
|
33
|
+
*/
|
|
34
|
+
readonly generation?: number;
|
|
35
|
+
}
|
|
36
|
+
/** Un ordre de contrôle reconnu. */
|
|
37
|
+
export type ControlOrder = ICloseChannelOrder | IEvictUserOrder;
|
|
38
|
+
/**
|
|
39
|
+
* Lit un ordre de contrôle dans une charge venue du bus — `null` si la forme n'est pas celle d'un ordre connu.
|
|
40
|
+
*
|
|
41
|
+
* Une seule garde pour les deux bords : l'émetteur la passe avant de publier
|
|
42
|
+
* (il n'émet jamais ce que ses pairs refuseraient), le récepteur avant
|
|
43
|
+
* d'appliquer. Lecture par champs PROPRES (`ownField`) : une charge dont le
|
|
44
|
+
* prototype porterait `op` ne passe pas. Un champ inconnu est toléré — c'est
|
|
45
|
+
* ce qui laisse une version suivante ajouter un champ sans faire refuser ses
|
|
46
|
+
* ordres pendant un déploiement progressif ; un champ CONNU de forme fausse
|
|
47
|
+
* fait refuser l'ordre entier.
|
|
48
|
+
*
|
|
49
|
+
* @param payload - la charge du message reçu (forme inconnue).
|
|
50
|
+
* @returns l'ordre, ou `null`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readControlOrder(payload: unknown): ControlOrder | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type IFrameCodec } from "nodefony";
|
|
1
2
|
import type { IBackplaneMessage } from "../../interfaces/IBackplane.js";
|
|
2
3
|
/**
|
|
3
4
|
* Sérialise un message backplane, scellé si un secret est fourni.
|
|
@@ -25,3 +26,36 @@ export declare function sealBackplaneEnvelope(msg: IBackplaneMessage, secret?: s
|
|
|
25
26
|
* scellé alors qu'un secret est exigé, sceau invalide).
|
|
26
27
|
*/
|
|
27
28
|
export declare function openBackplaneEnvelope(raw: string, secret?: string | null): IBackplaneMessage | null;
|
|
29
|
+
/**
|
|
30
|
+
* Encode un message backplane pour le **sujet binaire** du bus (ADR-0021 S6) :
|
|
31
|
+
* la MÊME enveloppe `{ channel, payload, originId }`, encodée par le codec
|
|
32
|
+
* binaire du serveur, octets natifs dans `payload`, scellée si un secret est
|
|
33
|
+
* fourni.
|
|
34
|
+
*
|
|
35
|
+
* Scellée, la charge est encodée deux fois — une pour le sceau, une dans
|
|
36
|
+
* l'enveloppe : le codec générique ne permet pas d'assembler l'enveloppe à
|
|
37
|
+
* partir de fragments déjà encodés, comme le fait la voie texte.
|
|
38
|
+
*
|
|
39
|
+
* @param msg - message à transporter (canal, charge, origine).
|
|
40
|
+
* @param codec - le codec binaire du serveur (`CBOR_FRAME_CODEC` de `nodefony/cbor`).
|
|
41
|
+
* @param secret - secret partagé entre pods ; `null`/`undefined` → enveloppe nue.
|
|
42
|
+
* @returns les octets à publier sur le sujet binaire.
|
|
43
|
+
* @throws TypeError si le codec rend du texte.
|
|
44
|
+
*/
|
|
45
|
+
export declare function sealBackplaneEnvelopeBytes(msg: IBackplaneMessage, codec: IFrameCodec, secret?: string | null): Uint8Array;
|
|
46
|
+
/**
|
|
47
|
+
* Décode et **vérifie** une enveloppe reçue sur le sujet binaire du bus — même
|
|
48
|
+
* règle que {@link openBackplaneEnvelope} : fail-closed strict sous un secret,
|
|
49
|
+
* aucun downgrade en retirant `sig`.
|
|
50
|
+
*
|
|
51
|
+
* Le sceau se recalcule sur le RÉ-ENCODAGE de la charge décodée, comme la voie
|
|
52
|
+
* texte le fait sur sa re-sérialisation : le codec CBOR du cœur rend la forme la
|
|
53
|
+
* plus courte de chaque valeur, donc émetteur et récepteur signent les mêmes
|
|
54
|
+
* octets.
|
|
55
|
+
*
|
|
56
|
+
* @param raw - octets lus sur le sujet binaire.
|
|
57
|
+
* @param codec - le codec binaire du serveur.
|
|
58
|
+
* @param secret - secret partagé attendu, ou `null`/`undefined` si aucun.
|
|
59
|
+
* @returns le message vérifié, ou `null` s'il doit être ignoré.
|
|
60
|
+
*/
|
|
61
|
+
export declare function openBackplaneEnvelopeBytes(raw: Uint8Array, codec: IFrameCodec, secret?: string | null): IBackplaneMessage | null;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import type { ISubscriberIdentity } from "nodefony";
|
|
1
2
|
import type { IRealtimeToken } from "../../interfaces/IRealtimeToken.js";
|
|
3
|
+
/**
|
|
4
|
+
* Identité d'admission de l'ANONYME — gelée et partagée par toutes les connexions non authentifiées (0 allocation par connexion).
|
|
5
|
+
*
|
|
6
|
+
* Porte l'invariant de l'ADR-0021 §S10 : anonyme ⇔ `authenticated === false`,
|
|
7
|
+
* `id === null` et `issuer === null`. Un canal qui déclare `allow` refuse
|
|
8
|
+
* cette identité d'office, sauf option explicite `anonymous: true` — ce qui
|
|
9
|
+
* garantit que `user:${who.id}` ne vaut jamais `"user:null"`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ANONYMOUS_SUBSCRIBER_IDENTITY: ISubscriberIdentity;
|
|
2
12
|
/**
|
|
3
13
|
* Token realtime ANONYME — fallback Zero Trust quand aucun matcher
|
|
4
14
|
* `IRealtimeAuthenticatorMatcher` ne capture le handshake.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ContractActionNames, ContractClientEmit, ContractServerActions, ContractServerEmit, ContractServerListen, ContractServerMethods, EventNames, IRealtimeContract, TypedRpcActionHandler } from "nodefony";
|
|
2
|
+
import { RealtimeController } from "./RealtimeController.js";
|
|
3
|
+
/**
|
|
4
|
+
* Clé de l'étiquette de type qui relie une classe à son contrat. Déclaration
|
|
5
|
+
* ambiante : aucun symbole n'existe à l'exécution, seul le compilateur la voit.
|
|
6
|
+
*/
|
|
7
|
+
declare const contractType: unique symbol;
|
|
8
|
+
/**
|
|
9
|
+
* Table COMPLÈTE des méthodes que le serveur doit au contrat : une entrée par nom
|
|
10
|
+
* de `serverMethods`, chacune recevant ses paramètres lus par `Jsonify` et rendant
|
|
11
|
+
* le résultat déclaré. Une méthode oubliée, un nom hors contrat ou une forme
|
|
12
|
+
* fausse ne compilent pas.
|
|
13
|
+
*/
|
|
14
|
+
export type ContractRealtimeMethodHandlers<C extends IRealtimeContract> = {
|
|
15
|
+
[K in ContractActionNames<ContractServerMethods<C>>]: TypedRpcActionHandler<ContractServerMethods<C>, K>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Noms qu'un contrat déclare et qu'une politique peut garder : ses méthodes
|
|
19
|
+
* serveur et les canaux de ses deux directions. Un canal déclaré par gabarit
|
|
20
|
+
* (`user:${string}`) admet aussi son motif (`user:*`). Direction non déclarée :
|
|
21
|
+
* tout nom.
|
|
22
|
+
*/
|
|
23
|
+
export type ContractPolicyNames<C extends IRealtimeContract> = ContractActionNames<ContractServerMethods<C>> | EventNames<ContractServerEmit<C>> | EventNames<ContractClientEmit<C>>;
|
|
24
|
+
/**
|
|
25
|
+
* Le contrat qu'une classe de contrôleur implémente, lu sur son étiquette de
|
|
26
|
+
* type ; `never` pour un contrôleur sans contrat.
|
|
27
|
+
*/
|
|
28
|
+
export type RealtimeContractOf<T> = T extends abstract new (...args: never[]) => {
|
|
29
|
+
readonly [contractType]?: infer C;
|
|
30
|
+
} ? unknown extends C ? never : C extends IRealtimeContract ? C : never : never;
|
|
31
|
+
/**
|
|
32
|
+
* Socle commun des deux formes de {@link ContractRealtimeController} : l'étiquette
|
|
33
|
+
* de contrat et les canaux annoncés. Les actions y gardent le défaut de la base
|
|
34
|
+
* (`{}`) — un contrat SANS `serverMethods` n'a rien à écrire.
|
|
35
|
+
*
|
|
36
|
+
* Ne s'étend pas directement : on étend {@link ContractRealtimeController}, qui
|
|
37
|
+
* choisit cette forme ou la forme stricte selon le contrat.
|
|
38
|
+
*
|
|
39
|
+
* @typeParam C - le contrat de l'application.
|
|
40
|
+
*/
|
|
41
|
+
export declare abstract class ContractRealtimeControllerBase<C extends IRealtimeContract> extends RealtimeController<ContractServerEmit<C>, ContractServerActions<C>, ContractServerListen<C>> {
|
|
42
|
+
/**
|
|
43
|
+
* Étiquette de TYPE : relie la classe à son contrat pour que `@RealtimePolicy`
|
|
44
|
+
* le retrouve. `declare` — aucun champ n'existe à l'exécution.
|
|
45
|
+
*/
|
|
46
|
+
readonly [contractType]?: C;
|
|
47
|
+
/** Canaux annoncés au handshake, contraints aux noms de `serverToClient`. */
|
|
48
|
+
protected realtimeChannels(): EventNames<ContractServerEmit<C>>[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Forme stricte de {@link ContractRealtimeController}, retenue quand le contrat
|
|
52
|
+
* déclare des `serverMethods` : `realtimeActions()` y devient ABSTRAITE et typée
|
|
53
|
+
* par le contrat — la seule construction qui fait d'une méthode OUBLIÉE une
|
|
54
|
+
* erreur de compilation (un décorateur ne voit jamais la classe entière).
|
|
55
|
+
*
|
|
56
|
+
* Ne s'étend pas directement : on étend {@link ContractRealtimeController}.
|
|
57
|
+
*
|
|
58
|
+
* @typeParam C - le contrat de l'application.
|
|
59
|
+
*/
|
|
60
|
+
export declare abstract class ContractRealtimeControllerStrict<C extends IRealtimeContract> extends ContractRealtimeControllerBase<C> {
|
|
61
|
+
/**
|
|
62
|
+
* Les méthodes du contrat servies par ce contrôleur — toutes, et par cette
|
|
63
|
+
* table SEULE : une action servie ici ne se décore pas en plus de
|
|
64
|
+
* `@RealtimeAction` (handler fabriqué puis écrasé à chaque connexion). Sa
|
|
65
|
+
* politique se pose sur la classe, par `@RealtimePolicy`.
|
|
66
|
+
*
|
|
67
|
+
* Appelée à chaque connexion. Des handlers qui n'ont pas besoin de `this` se
|
|
68
|
+
* rangent dans une table CONSTANTE du module, rendue telle quelle : aucune
|
|
69
|
+
* allocation par connexion.
|
|
70
|
+
*/
|
|
71
|
+
protected abstract realtimeActions(): ContractRealtimeMethodHandlers<C>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Vrai quand le contrat `C` ne déclare aucune méthode serveur. Le tuple empêche
|
|
75
|
+
* la distribution du conditionnel sur `never`.
|
|
76
|
+
*/
|
|
77
|
+
type HasNoServerMethods<C extends IRealtimeContract> = [
|
|
78
|
+
ContractActionNames<ContractServerMethods<C>>
|
|
79
|
+
] extends [never] ? true : false;
|
|
80
|
+
/**
|
|
81
|
+
* L'instance d'un contrôleur de contrat : forme stricte si `C` déclare des
|
|
82
|
+
* `serverMethods`, socle sinon.
|
|
83
|
+
*
|
|
84
|
+
* @typeParam C - le contrat de l'application.
|
|
85
|
+
*/
|
|
86
|
+
export type ContractRealtimeController<C extends IRealtimeContract> = HasNoServerMethods<C> extends true ? ContractRealtimeControllerBase<C> : ContractRealtimeControllerStrict<C>;
|
|
87
|
+
/**
|
|
88
|
+
* Le constructeur de {@link ContractRealtimeController} : celui de la forme
|
|
89
|
+
* stricte, dont l'instance se lit par le contrat. Les statiques (`scope`,
|
|
90
|
+
* `assertScope`) restent ceux de la classe.
|
|
91
|
+
*/
|
|
92
|
+
type ContractRealtimeControllerConstructor = Omit<typeof ContractRealtimeControllerStrict, "prototype"> & (abstract new <C extends IRealtimeContract>(...args: ConstructorParameters<typeof RealtimeController>) => ContractRealtimeController<C>);
|
|
93
|
+
/**
|
|
94
|
+
* Base d'un contrôleur qui IMPLÉMENTE un contrat temps réel — la porte du
|
|
95
|
+
* serveur. Le contrat s'écrit UNE fois, sur `extends` ; la direction du serveur
|
|
96
|
+
* en est dérivée (il émet `serverToClient`, écoute `clientToServer`, appelle
|
|
97
|
+
* `clientMethods`) et il DOIT servir chaque méthode de `serverMethods`.
|
|
98
|
+
*
|
|
99
|
+
* Quand le contrat déclare des `serverMethods`, `realtimeActions()` est
|
|
100
|
+
* abstraite et typée par lui : une méthode OUBLIÉE ne compile pas. Un contrat
|
|
101
|
+
* SANS `serverMethods` (tableau de bord, chat qui ne fait que diffuser) n'a rien
|
|
102
|
+
* à écrire. Les entrées client (`realtimeInbound()`) restent facultatives canal
|
|
103
|
+
* par canal : accepter une entrée est une permission, pas une promesse. Les
|
|
104
|
+
* politiques d'accès se posent sur la classe par `@RealtimePolicy`, qui lit le
|
|
105
|
+
* contrat sur cette classe.
|
|
106
|
+
*
|
|
107
|
+
* Le fichier du contrat, fait de types seuls, est partagé avec le navigateur, qui
|
|
108
|
+
* entre par `NodefonySocket.fromContract<C>()`.
|
|
109
|
+
*
|
|
110
|
+
* Un contrat qui déclare des `serverRoutes` (routes du pont, typées côté client
|
|
111
|
+
* par le schéma que la route valide) se double d'une table statique de VALEURS,
|
|
112
|
+
* `static readonly serverRoutes = { … } satisfies ContractRouteSchemas<C>` : le
|
|
113
|
+
* compilateur y exige chaque route du contrat avec son schéma, et le démarrage
|
|
114
|
+
* vérifie que chacune est montée, servie par la socket et valide LE MÊME schéma
|
|
115
|
+
* (`BootConfigurationError` sinon). Sans cette table, aucun contrôle.
|
|
116
|
+
*
|
|
117
|
+
* @typeParam C - le contrat de l'application.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* @RealtimePolicy({ "chat:send": { roles: ["ROLE_USER"] } })
|
|
122
|
+
* class ChatRt extends ContractRealtimeController<ChatContract> {
|
|
123
|
+
* protected realtimeActions() {
|
|
124
|
+
* return { "chat:send": (p: { text: string }) => ({ id: this.store(p.text) }) };
|
|
125
|
+
* }
|
|
126
|
+
* }
|
|
127
|
+
*
|
|
128
|
+
* class DashRt extends ContractRealtimeController<DashContract> {} // sans serverMethods
|
|
129
|
+
*
|
|
130
|
+
* class ChatRoutesRt extends ContractRealtimeController<ChatContract> {
|
|
131
|
+
* static readonly serverRoutes = {
|
|
132
|
+
* "POST /chat/send": { body: SendBody }, // le schéma de la route, pas une copie
|
|
133
|
+
* } satisfies ContractRouteSchemas<ChatContract>;
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export declare const ContractRealtimeController: ContractRealtimeControllerConstructor;
|
|
138
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RealtimeClosedReason } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Ce que la poignée d'une connexion demande au contrôleur qui la sert — imposer
|
|
4
|
+
* un abonnement, y mettre fin. Implémenté par `RealtimeController`, une fois par
|
|
5
|
+
* connexion.
|
|
6
|
+
*/
|
|
7
|
+
export interface IRealtimeConnectionHost {
|
|
8
|
+
/** Lance l'abonnement imposé à `channel` ; le verdict passe par {@link RealtimeConnection.settleImposed}. */
|
|
9
|
+
impose(channel: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Met fin à l'abonnement de cette connexion à `channel` (hub ET état de la
|
|
12
|
+
* connexion) et le lui notifie (`realtime:closed`).
|
|
13
|
+
*
|
|
14
|
+
* @returns `false` si la connexion n'est pas abonnée à ce canal.
|
|
15
|
+
*/
|
|
16
|
+
end(channel: string, reason: RealtimeClosedReason): boolean;
|
|
17
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type EventsMap, type ActionsMap, type DefaultEventsMap, type DefaultActionsMap, type EventNames, type EventPayload, type ActionNames, type ActionParams, type ActionResult } from "nodefony";
|
|
2
2
|
import type { ContextType } from "@nodefony/http";
|
|
3
3
|
import { Controller } from "@nodefony/framework";
|
|
4
4
|
import type { ControllerScope } from "@nodefony/framework";
|
|
5
|
-
import type { IRealtimeController, RealtimePublish,
|
|
5
|
+
import type { IRealtimeController, RealtimePublish, RealtimeInboundHandlers, RealtimeActionHandler } from "../../interfaces/IRealtimeController.js";
|
|
6
6
|
/**
|
|
7
7
|
* RealtimeController — base d'un endpoint WebSocket temps réel SERVEUR (JSON-RPC 2.0).
|
|
8
8
|
*
|
|
@@ -25,7 +25,7 @@ import type { IRealtimeController, RealtimePublish, RealtimeInboundHandler } fro
|
|
|
25
25
|
* créé au 1ᵉʳ abonné, `dispose()` au dernier (`unsubscribe` ou `onFinish`). Le dispatch
|
|
26
26
|
* d'action n'est payé que sur une requête `id`. C'est aussi le seam du backplane Redis.
|
|
27
27
|
*/
|
|
28
|
-
export declare abstract class RealtimeController<Emit extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> extends Controller implements IRealtimeController {
|
|
28
|
+
export declare abstract class RealtimeController<Emit extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap, Listen extends EventsMap = DefaultEventsMap> extends Controller implements IRealtimeController {
|
|
29
29
|
#private;
|
|
30
30
|
/**
|
|
31
31
|
* Une instance PAR CONNEXION, par construction — le défaut `singleton` des
|
|
@@ -72,7 +72,7 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
72
72
|
*/
|
|
73
73
|
createRealtimeChannel(_channel: string, _publish: RealtimePublish): (() => void) | null;
|
|
74
74
|
/** Actions RPC exposées (requête→réponse). À surcharger ; défaut : aucune. */
|
|
75
|
-
protected realtimeActions(): Record<string,
|
|
75
|
+
protected realtimeActions(): Record<string, RealtimeActionHandler>;
|
|
76
76
|
/** Canaux annoncés au handshake. À surcharger ; défaut : aucun. */
|
|
77
77
|
protected realtimeChannels(): string[];
|
|
78
78
|
/**
|
|
@@ -88,13 +88,16 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
88
88
|
* surcharger ; défaut : **aucun** (sûr — un client ne peut rien pousser au serveur
|
|
89
89
|
* tant qu'un canal n'est pas explicitement déclaré ici). Seam des backings entrants
|
|
90
90
|
* (SIP, bridge). Cf {@link RealtimeInboundHandler} (params NON FIABLES).
|
|
91
|
+
*
|
|
92
|
+
* Typée par `Listen` (ce que le client émet, 3ᵉ générique) : un canal hors de
|
|
93
|
+
* la carte ne compile pas, et chaque handler reçoit la forme de SON canal.
|
|
91
94
|
*/
|
|
92
|
-
protected realtimeInbound():
|
|
95
|
+
protected realtimeInbound(): RealtimeInboundHandlers<Listen, Emit>;
|
|
93
96
|
/**
|
|
94
|
-
* Opt-in du **pont API** (
|
|
97
|
+
* Opt-in du **pont API** (une action, deux transports) : `true` → la
|
|
95
98
|
* connexion expose la méthode RPC `api.request {path}` qui re-route le path
|
|
96
99
|
* porté par le message vers l'action controller correspondante — la MÊME
|
|
97
|
-
* action que le GET REST (`socket.
|
|
100
|
+
* action que le GET REST (`socket.get("/nodefony/kernel/api/modules")`
|
|
98
101
|
* côté client). Le pont n'atteint QUE les routes déclarant le transport
|
|
99
102
|
* `WEBSOCKET` dans leurs `methods` (zéro bypass : une action dit à quels
|
|
100
103
|
* transports elle répond). À surcharger ; défaut : `false` — aucune surface
|
|
@@ -120,7 +123,7 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
120
123
|
* `NodefonySocket.replaySubscriptions` et les deux cas « la fenêtre où le serveur
|
|
121
124
|
* écoute (welcome) » de `NodefonySocketCoverage.test.ts`.
|
|
122
125
|
*/
|
|
123
|
-
protected handleRealtime(message: string |
|
|
126
|
+
protected handleRealtime(message: string | Uint8Array | null): void;
|
|
124
127
|
/**
|
|
125
128
|
* Lit les trames BINAIRES de cette connexion — le point d'extension d'un
|
|
126
129
|
* protocole binaire écrit par l'application, à côté du JSON-RPC ; pendant
|
|
@@ -128,7 +131,7 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
128
131
|
*
|
|
129
132
|
* Le protocole temps réel est du JSON-RPC texte : il ne sait pas à quel canal
|
|
130
133
|
* une trame binaire appartient. Sans cette méthode, une trame binaire est
|
|
131
|
-
* écartée, comptée (`
|
|
134
|
+
* écartée, comptée (`binaryDiscardedTotal` de la sonde du hub) et journalisée
|
|
132
135
|
* une fois par connexion. La surcharger la reçoit telle quelle, octets exacts,
|
|
133
136
|
* en `Uint8Array` — le type que livre aussi `NodefonySocket.onBinary` dans le
|
|
134
137
|
* navigateur : un décodeur s'écrit une fois pour les deux côtés. À l'exécution
|
|
@@ -140,6 +143,10 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
140
143
|
* et le pare-feu. L'identité se lit par `RequestContext.getAuthenticatedUser()`,
|
|
141
144
|
* et toute décision d'accès sur une trame binaire appartient à l'application.
|
|
142
145
|
*
|
|
146
|
+
* ⚠️ Connexions TEXTE seulement : une connexion qui a négocié un codec binaire
|
|
147
|
+
* (configuration `codec: "cbor"`) lit toute trame binaire comme une frame du
|
|
148
|
+
* codec, et cette méthode n'y est jamais appelée (ADR-0022 §2).
|
|
149
|
+
*
|
|
143
150
|
* ⚠️ Un `DataView` se construit sur `data.buffer` AVEC `data.byteOffset` et
|
|
144
151
|
* `data.byteLength` : un `Buffer` issu du pool de Node ne commence pas à 0.
|
|
145
152
|
*
|
|
@@ -183,10 +190,20 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
183
190
|
* client. Même contre-pression que les frames JSON-RPC.
|
|
184
191
|
*
|
|
185
192
|
* @param data - les octets à émettre (`Uint8Array`, donc un `Buffer` aussi).
|
|
186
|
-
*
|
|
187
|
-
*
|
|
193
|
+
* Sur une connexion qui a négocié un codec BINAIRE, refusé (ADR-0022 §2) : le
|
|
194
|
+
* client y lirait ces octets comme une frame du codec. Des octets passent
|
|
195
|
+
* alors par un canal, avec ses portes.
|
|
196
|
+
*
|
|
197
|
+
* @returns `false` si le handshake n'est pas terminé, la connexion fermée, la
|
|
198
|
+
* trame jetée par la contre-pression, ou la connexion en codec binaire.
|
|
188
199
|
*/
|
|
189
200
|
protected sendBinaryToClient(data: Uint8Array): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Journalise, une fois par connexion, le flux brut refusé sous codec binaire.
|
|
203
|
+
*
|
|
204
|
+
* @param state - l'état de la connexion.
|
|
205
|
+
*/
|
|
206
|
+
private noticeRawBinaryUnderCodec;
|
|
190
207
|
/**
|
|
191
208
|
* État realtime de CETTE connexion, `undefined` avant le welcome — pour les
|
|
192
209
|
* chemins FROIDS (requête sortante, binaire). `notifyClient` et la réception
|
|
@@ -207,6 +224,8 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
207
224
|
* Échecs (close WebSocket, codes plage applicative RFC 6455 §7.4.2) :
|
|
208
225
|
* - Origin refusée → code 4003 (`forbidden`).
|
|
209
226
|
* - `authenticate` throw → code 4001 (`unauthorized`).
|
|
227
|
+
* - `authenticate` sans réponse dans `authenticateTimeoutMs` → code 1013
|
|
228
|
+
* (`try again later`, ADR-0021 §S11) : le client recule puis revient.
|
|
210
229
|
*/
|
|
211
230
|
private onHandshake;
|
|
212
231
|
/**
|
|
@@ -222,8 +241,69 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
222
241
|
* par connexion).
|
|
223
242
|
*/
|
|
224
243
|
private acknowledgeSubscription;
|
|
225
|
-
/**
|
|
244
|
+
/**
|
|
245
|
+
* Poignée publique de la connexion, créée au premier besoin (abonnement, trame entrante) puis gardée sur son état.
|
|
246
|
+
*
|
|
247
|
+
* Cold path : une fois par connexion. Les deux fermetures la relient à CE
|
|
248
|
+
* contrôleur, dont les fabriques servent ses canaux.
|
|
249
|
+
*/
|
|
250
|
+
private connectionOf;
|
|
251
|
+
/**
|
|
252
|
+
* Abonnement imposé par le serveur (`RealtimeHub.subscribeConnection`) — la politique fixe par le verrou des trames, puis le chemin d'un `subscribe` du client, `allow` compris (ADR-0022 §6 décision 2).
|
|
253
|
+
*
|
|
254
|
+
* Le verdict est rendu à l'appelant par `RealtimeConnection.settleImposed` :
|
|
255
|
+
* à l'acquittement ({@link acknowledgeSubscription}) ou au refus
|
|
256
|
+
* ({@link refuseChannel}) — qui n'envoie alors rien au client, qui n'a rien
|
|
257
|
+
* demandé.
|
|
258
|
+
*/
|
|
259
|
+
private imposeChannel;
|
|
260
|
+
/**
|
|
261
|
+
* Met fin à l'abonnement de la connexion à un canal, sur décision du serveur, et le lui notifie (`realtime:closed`).
|
|
262
|
+
*
|
|
263
|
+
* Même chemin que l'`unsubscribe` du client (hub ET état de la connexion),
|
|
264
|
+
* jamais un second : retirer le seul sink du hub laisserait l'entrée de la
|
|
265
|
+
* connexion, et le `subscribe` suivant serait acquitté sans rien rouvrir.
|
|
266
|
+
* Retire aussi la marque « admise » et le verdict retenu de ce canal.
|
|
267
|
+
*
|
|
268
|
+
* @returns `false` si la connexion n'est pas abonnée à ce canal.
|
|
269
|
+
*/
|
|
270
|
+
private endChannel;
|
|
271
|
+
/**
|
|
272
|
+
* Retire la connexion d'un canal : hub, état de la connexion, admission de sa règle — le chemin UNIQUE d'une sortie, demandée par le client ou décidée par le serveur.
|
|
273
|
+
*
|
|
274
|
+
* @returns `false` si la connexion n'était pas abonnée.
|
|
275
|
+
*/
|
|
276
|
+
private leaveChannel;
|
|
277
|
+
/**
|
|
278
|
+
* Refus d'un abonnement : rendu à l'appelant serveur s'il s'agit d'une imposition, au client sinon — et aux deux quand le client avait aussi demandé ce canal.
|
|
279
|
+
*/
|
|
280
|
+
private refuseChannel;
|
|
281
|
+
/**
|
|
282
|
+
* Abonne la connexion à un canal via le hub partagé (idempotent par connexion).
|
|
283
|
+
*
|
|
284
|
+
* Un canal dont la politique déclare `allow` passe d'abord par la file
|
|
285
|
+
* d'admission de la connexion ; tout autre canal s'ouvre tout de suite, sans
|
|
286
|
+
* rien lire de plus qu'aujourd'hui (une recherche de politique au `subscribe`,
|
|
287
|
+
* trame de contrôle rare).
|
|
288
|
+
*/
|
|
226
289
|
private startChannel;
|
|
290
|
+
/**
|
|
291
|
+
* Soumet l'abonnement à la règle `allow` du canal — APRÈS le verrou des trames, qui a déjà jugé `authenticated`/`roles`/`scopes` (ADR-0022 §6, décision 1 ter, moment 2).
|
|
292
|
+
*
|
|
293
|
+
* `allow` s'applique même sans module de sécurité : il ne fait que retirer
|
|
294
|
+
* des abonnés, jamais en ajouter — l'ignorer ouvrirait `user:<id>` à tous.
|
|
295
|
+
* La file de la connexion n'est créée qu'à son premier canal à `allow`.
|
|
296
|
+
*/
|
|
297
|
+
private admitChannel;
|
|
298
|
+
/**
|
|
299
|
+
* Rend un refus d'abonnement observable (`realtime:denied`) — le détail ne part qu'hors production.
|
|
300
|
+
*
|
|
301
|
+
* @param detail - phrase pour le développeur ; `undefined` quand l'appelant
|
|
302
|
+
* ne l'a pas rédigée (production).
|
|
303
|
+
*/
|
|
304
|
+
private denyChannel;
|
|
305
|
+
/** Ouvre l'abonnement au hub et l'acquitte — canal sans `allow`, ou admis par sa règle. */
|
|
306
|
+
private openChannel;
|
|
227
307
|
/**
|
|
228
308
|
* Enveloppe des actions `@RealtimeAction` dont un paramètre est décoré
|
|
229
309
|
* (`@Body`, `@CurrentUser`…) : l'action reçoit ses arguments comme une route,
|
|
@@ -245,7 +325,7 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
245
325
|
*/
|
|
246
326
|
private actionParamsWrapper;
|
|
247
327
|
/**
|
|
248
|
-
* Handler de la méthode RPC `api.request {path}` — le **pont
|
|
328
|
+
* Handler de la méthode RPC `api.request {path}` — le **pont socket**
|
|
249
329
|
* (POC Ph.3) : résout le path porté par le message via le Router
|
|
250
330
|
* (`cleanPathOverride` — l'URL de la connexion n'est jamais mutée) puis
|
|
251
331
|
* exécute l'action SANS la rendre (`executeAction`) : la valeur nue est
|