@nodefony/realtime 10.0.0-beta.1 → 10.0.0-beta.3
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 +2 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +17 -10
- package/dist/nodefony/config/config.js +6 -3
- package/dist/nodefony/config/configSchema.js +19 -0
- package/dist/nodefony/config/defineModuleConfig.js +42 -15
- package/dist/nodefony/decorators/realtimeDecorators.js +68 -17
- package/dist/nodefony/src/backplane/RedisBackplane.js +21 -4
- package/dist/nodefony/src/backplane/bootNotices.js +43 -0
- package/dist/nodefony/src/backplane/publishQueue.js +1 -4
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +15 -4
- package/dist/nodefony/src/guards.js +41 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -4
- package/dist/nodefony/src/server/RealtimeController.js +120 -50
- package/dist/nodefony/src/server/RealtimeHub.js +29 -51
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +13 -5
- package/dist/nodefony/src/server/syslogUplink.js +4 -2
- package/dist/nodefony/src/service/RealtimeService.js +3 -2
- package/dist/nodefony/testing/index.js +41 -1
- package/dist/types/nodefony/config/configSchema.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -6
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +19 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +7 -0
- package/dist/types/nodefony/src/backplane/bootNotices.d.ts +25 -0
- package/dist/types/nodefony/src/guards.d.ts +32 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +24 -4
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
- package/dist/types/nodefony/testing/index.d.ts +2 -2
- package/docs/actions.md +124 -41
- package/docs/architecture.md +93 -91
- package/docs/cadence-et-contre-pression.md +634 -0
- package/docs/configuration.md +66 -56
- package/docs/cookbook-chat.md +39 -33
- package/docs/index.md +47 -31
- package/docs/observabilite.md +21 -18
- package/docs/protocole.md +79 -65
- package/docs/securite.md +69 -65
- package/docs/vocabulaire.md +80 -79
- package/package.json +16 -12
- package/docs/couverture-bancs.md +0 -139
|
@@ -140,8 +140,25 @@ export declare function RealtimeChannel(channel: string, policy?: IChannelPolicy
|
|
|
140
140
|
* }
|
|
141
141
|
*/
|
|
142
142
|
export declare function RealtimeInbound(method: string, policy?: IChannelPolicy): MethodDecorator;
|
|
143
|
-
/**
|
|
144
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Méthode d'action liée à son instance, appelable avec ses arguments réels —
|
|
145
|
+
* plusieurs quand ses paramètres sont décorés (`@Body`, `@CurrentUser`…).
|
|
146
|
+
*/
|
|
147
|
+
export type RealtimeActionMethod = (...args: unknown[]) => unknown;
|
|
148
|
+
/**
|
|
149
|
+
* Enveloppe posée sur une action décorée au handshake : reçoit la méthode liée
|
|
150
|
+
* et son nom de propriété, rend le handler JSON-RPC à enregistrer.
|
|
151
|
+
*/
|
|
152
|
+
export type RealtimeActionWrapper = (method: RealtimeActionMethod, propertyKey: string | symbol) => RpcActionHandler;
|
|
153
|
+
/**
|
|
154
|
+
* Lecture (lazy, au handshake) du registre des actions décorées.
|
|
155
|
+
*
|
|
156
|
+
* @param instance - le contrôleur de la connexion
|
|
157
|
+
* @param wrap - enveloppe optionnelle de chaque action (résolution des
|
|
158
|
+
* paramètres décorés) ; sans elle, l'action reçoit la charge JSON-RPC brute
|
|
159
|
+
* @returns les handlers par nom d'action, ou `null` si aucune n'est décorée
|
|
160
|
+
*/
|
|
161
|
+
export declare function getRealtimeActions(instance: object, wrap?: RealtimeActionWrapper): Record<string, RpcActionHandler> | null;
|
|
145
162
|
/** Lecture (lazy, au handshake) du registre des canaux décorés. */
|
|
146
163
|
export declare function getRealtimeChannels(instance: object): Record<string, RealtimeChannelFactory> | null;
|
|
147
164
|
/** Lecture (lazy, au handshake) du registre des canaux inbound décorés. */
|
|
@@ -22,7 +22,7 @@ export type RealtimeInboundHandler = (params: unknown, reply: (payload: unknown)
|
|
|
22
22
|
* (Les hooks `realtimeActions()` / `realtimeChannels()` sont `protected` sur la base
|
|
23
23
|
* — détail d'implémentation surchargeable, hors contrat public.)
|
|
24
24
|
*
|
|
25
|
-
* Côté client, le pendant n'est PAS un controller mais `
|
|
25
|
+
* Côté client, le pendant n'est PAS un controller mais `NodefonySocket` — tous deux
|
|
26
26
|
* composent le même peer ; cf vision « la socket Nodefony ».
|
|
27
27
|
*/
|
|
28
28
|
export interface IRealtimeController {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @see IRealtimeAuthenticator
|
|
13
13
|
*/
|
|
14
14
|
export interface IRealtimeHandshake {
|
|
15
|
-
/** En-têtes de la requête upgrade (clés en lowercase RFC
|
|
15
|
+
/** En-têtes de la requête upgrade (clés en lowercase RFC 9110 §5.1). */
|
|
16
16
|
readonly headers: Readonly<Record<string, string | string[] | undefined>>;
|
|
17
17
|
/** Cookies parsés depuis `Cookie:` (vide si en-tête absent ou mal formé). */
|
|
18
18
|
readonly cookies: ReadonlyMap<string, string>;
|
|
@@ -83,6 +83,13 @@ export interface IRedisSubscriber {
|
|
|
83
83
|
* Le listener Redis est conservé pour un `unsubscribe(channel, listener)` ciblé
|
|
84
84
|
* (n'arrache pas d'éventuels autres abonnés du même client).
|
|
85
85
|
*/
|
|
86
|
+
/**
|
|
87
|
+
* Vrai si la valeur expose la surface d'un client Redis publisher — vérifié au
|
|
88
|
+
* démarrage, sur le client que rend le service `redis`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isRedisPublisher(value: unknown): value is IRedisPublisher;
|
|
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;
|
|
86
93
|
export declare function createRedisServiceTransport(publisher: IRedisPublisher, subscriber: IRedisSubscriber): IRedisBackplaneTransport;
|
|
87
94
|
/**
|
|
88
95
|
* Réglages optionnels du driver — regroupés en objet pour ne pas allonger une
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IBootNotice } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Le temps réel ne dépasse pas ce processus : aucun backplane actif (driver
|
|
4
|
+
* `loopback`, `cluster` hors d'un worker, ou fabrique inactive ici).
|
|
5
|
+
*
|
|
6
|
+
* Simple information en développement mono-processus — c'est le cas normal —
|
|
7
|
+
* mais à savoir avant de déployer plusieurs exemplaires : un message publié
|
|
8
|
+
* sur l'un n'atteindrait pas les clients connectés aux autres.
|
|
9
|
+
*
|
|
10
|
+
* @param driver - le driver déclaré.
|
|
11
|
+
* @returns le point à déclarer.
|
|
12
|
+
*/
|
|
13
|
+
export declare function realtimeLocalOnlyNotice(driver: string): IBootNotice;
|
|
14
|
+
/**
|
|
15
|
+
* Le backplane déclaré n'a pas pu démarrer — le hub est retombé en local.
|
|
16
|
+
*
|
|
17
|
+
* Distinct de {@link realtimeLocalOnlyNotice} : ici, l'application a DEMANDÉ
|
|
18
|
+
* un fan-out entre exemplaires et ne l'a pas. Deux pods servent alors deux
|
|
19
|
+
* mondes temps réel disjoints, sans erreur visible.
|
|
20
|
+
*
|
|
21
|
+
* @param driver - le driver déclaré.
|
|
22
|
+
* @param reason - la cause (message de l'échec).
|
|
23
|
+
* @returns le point à déclarer.
|
|
24
|
+
*/
|
|
25
|
+
export declare function realtimeBackplaneDownNotice(driver: string, reason: string): IBootNotice;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gardes de type internes au paquet — elles remplacent une conversion (`as`)
|
|
3
|
+
* par une vérification faite à l'exécution, pour qu'une valeur d'une autre
|
|
4
|
+
* forme soit écartée au lieu de traverser le code sous un type qui ment.
|
|
5
|
+
*
|
|
6
|
+
* Non exportées par le paquet : ce sont des détails d'implémentation.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Vrai si la valeur est un objet non nul : ses propriétés se lisent alors,
|
|
10
|
+
* typées `unknown`, et chacune se vérifie avant usage.
|
|
11
|
+
*
|
|
12
|
+
* @param value - valeur venue du réseau, d'une métadonnée ou du conteneur
|
|
13
|
+
* @returns `true` pour tout objet (tableau compris), `false` pour `null` et les primitives
|
|
14
|
+
*/
|
|
15
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Vrai si la valeur est une fonction. Toute fonction JavaScript s'appelle avec
|
|
18
|
+
* n'importe quels arguments et rend une valeur inconnue : la signature annoncée
|
|
19
|
+
* ne promet rien de plus que ce que `typeof` a vérifié.
|
|
20
|
+
*
|
|
21
|
+
* @param value - valeur lue par son nom (métadonnée, propriété dynamique)
|
|
22
|
+
* @returns `true` si la valeur est appelable
|
|
23
|
+
*/
|
|
24
|
+
export declare function isCallable(value: unknown): value is (...args: unknown[]) => unknown;
|
|
25
|
+
/**
|
|
26
|
+
* Vrai si la valeur se comporte comme une promesse (transport asynchrone) —
|
|
27
|
+
* un client Redis rend une promesse, un faux transport de test rend `void`.
|
|
28
|
+
*
|
|
29
|
+
* @param value - retour d'un transport
|
|
30
|
+
* @returns `true` si la valeur porte une méthode `then`
|
|
31
|
+
*/
|
|
32
|
+
export declare function isThenable(value: unknown): value is Promise<unknown>;
|
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
import type { IRealtimeToken } from "../../interfaces/IRealtimeToken.js";
|
|
2
|
+
/**
|
|
3
|
+
* Token realtime ANONYME — fallback Zero Trust quand aucun matcher
|
|
4
|
+
* `IRealtimeAuthenticatorMatcher` ne capture le handshake.
|
|
5
|
+
*
|
|
6
|
+
* Singleton gelé (`Object.freeze`) — 0 alloc par connexion non authentifiée.
|
|
7
|
+
* Aligné sur le pattern `AnonymousToken` de `@nodefony/security` S1 (même
|
|
8
|
+
* surface logique, type `"anonymous"`, `roles = ["ROLE_ANONYMOUS"]`).
|
|
9
|
+
*
|
|
10
|
+
* Stocké par le hub sur la map `peer → token` au handshake si aucun
|
|
11
|
+
* authenticator ne capture la connexion. Les voters P6 (futur) verront un
|
|
12
|
+
* token avec `isAuthenticated() === false` et appliqueront leur politique
|
|
13
|
+
* (Zero Trust : zone protégée → frame refusée par `beforeDispatch`).
|
|
14
|
+
*/
|
|
2
15
|
export declare const ANONYMOUS_REALTIME_TOKEN: IRealtimeToken;
|
|
3
16
|
export default ANONYMOUS_REALTIME_TOKEN;
|
|
@@ -11,7 +11,7 @@ import type { IRealtimeController, RealtimePublish, RealtimeInboundHandler } fro
|
|
|
11
11
|
* — écrit UNE fois, partagé par tous les modules. Un contrôleur concret ne déclare que
|
|
12
12
|
* son métier : {@link createRealtimeChannel} (providers de canaux) + {@link realtimeActions}.
|
|
13
13
|
*
|
|
14
|
-
* Chaque connexion compose un `JsonRpcPeer` (le MÊME que `
|
|
14
|
+
* Chaque connexion compose un `JsonRpcPeer` (le MÊME que `NodefonySocket` côté navigateur
|
|
15
15
|
* — symétrie isomorphe) branché sur un {@link WsConnectionTransport}.
|
|
16
16
|
*
|
|
17
17
|
* Usage : le sous-classe garde sa route WS et délègue —
|
|
@@ -111,13 +111,13 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
111
111
|
* est structurel, pas un choix : c'est au client d'attendre `realtime:welcome`
|
|
112
112
|
* avant de pousser.
|
|
113
113
|
*
|
|
114
|
-
* ⚠️ Cette phrase a longtemps ajouté « ce que
|
|
114
|
+
* ⚠️ Cette phrase a longtemps ajouté « ce que le client fait nativement ».
|
|
115
115
|
* C'était FAUX : le client rejouait ses abonnements dès l'ouverture de la socket,
|
|
116
116
|
* si bien qu'un `subscribe` posé avant le démarrage et TOUS ceux d'après une
|
|
117
117
|
* reconnexion étaient perdus ici même, sans un mot des deux côtés. Un contrat écrit
|
|
118
118
|
* d'un seul côté du fil n'est pas tenu ; il l'est désormais par
|
|
119
|
-
* `
|
|
120
|
-
* écoute (welcome) » de `
|
|
119
|
+
* `NodefonySocket.replaySubscriptions` et les deux cas « la fenêtre où le serveur
|
|
120
|
+
* écoute (welcome) » de `NodefonySocketCoverage.test.ts`.
|
|
121
121
|
*/
|
|
122
122
|
protected handleRealtime(message: string | Buffer | null): void;
|
|
123
123
|
/**
|
|
@@ -166,6 +166,26 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
166
166
|
private onRealtimeNotification;
|
|
167
167
|
/** Abonne la connexion à un canal via le hub partagé (idempotent par connexion). */
|
|
168
168
|
private startChannel;
|
|
169
|
+
/**
|
|
170
|
+
* Enveloppe des actions `@RealtimeAction` dont un paramètre est décoré
|
|
171
|
+
* (`@Body`, `@CurrentUser`…) : l'action reçoit ses arguments comme une route,
|
|
172
|
+
* résolus par la fonction du framework et non par une copie.
|
|
173
|
+
*
|
|
174
|
+
* L'identité ne change PAS : un message est déjà traité dans la bulle
|
|
175
|
+
* `RequestContext` du handshake (`WebsocketContext` lie son écouteur par
|
|
176
|
+
* `AsyncResource.bind`), où le pare-feu a posé `user` et `token`. La bulle
|
|
177
|
+
* ouverte ici HÉRITE de ce store et n'y ajoute que la charge (`body`, que
|
|
178
|
+
* `@Body` lit dans l'ALS comme pour `api.request`) — une écriture
|
|
179
|
+
* `RequestContext.set()` dans l'action reste ainsi propre à l'appel.
|
|
180
|
+
*
|
|
181
|
+
* Une action sans paramètre décoré est rendue telle quelle : rien ne change
|
|
182
|
+
* pour elle, ni comportement ni coût. Les métadonnées se lisent ICI, une fois
|
|
183
|
+
* par connexion.
|
|
184
|
+
*
|
|
185
|
+
* @param ctx - le contexte de la connexion
|
|
186
|
+
* @returns l'enveloppe passée à {@link getRealtimeActions}
|
|
187
|
+
*/
|
|
188
|
+
private actionParamsWrapper;
|
|
169
189
|
/**
|
|
170
190
|
* Handler de la méthode RPC `api.request {path}` — le **pont API souverain**
|
|
171
191
|
* (POC Ph.3) : résout le path porté par le message via le Router
|
|
@@ -71,16 +71,13 @@ export declare const RESERVED_SYSTEM_PREFIXES: readonly ["nodefony:"];
|
|
|
71
71
|
*/
|
|
72
72
|
export declare const isReservedSystemChannel: typeof isPlatformChannel;
|
|
73
73
|
/**
|
|
74
|
-
* Période de re-validation des identités RÉVOCABLES — F4 (revue 0.6).
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* l'écart : toutes les `REVOCATION_REVALIDATE_MS`, les tokens révocables sont
|
|
80
|
-
* re-validés et la connexion fermée si l'identité n'est plus valide (≤ 1 fenêtre de
|
|
81
|
-
* délai). Ordre de grandeur aligné sur le heartbeat WS (`keepaliveInterval` 20 s).
|
|
74
|
+
* Période de re-validation des identités RÉVOCABLES — F4 (revue 0.6). Le verrou
|
|
75
|
+
* de frame est SYNC (identité figée au handshake) : ce tick ferme l'écart avec
|
|
76
|
+
* `api.request`, qui re-valide par requête. Définie par le cœur, qui porte le
|
|
77
|
+
* registre partagé avec les flux SSE ; ré-exportée ici pour les consommateurs
|
|
78
|
+
* du hub.
|
|
82
79
|
*/
|
|
83
|
-
export
|
|
80
|
+
export { REVOCATION_REVALIDATE_MS } from "nodefony";
|
|
84
81
|
/**
|
|
85
82
|
* Le MOTIF de famille d'un canal — son dernier segment remplacé par `*`.
|
|
86
83
|
*
|
|
@@ -156,7 +153,7 @@ export type ChannelFactory = (channel: string, publish: RealtimePublish) => (()
|
|
|
156
153
|
* multiplexe des canaux ; le hub aiguille entre les sockets. La façade *consommateur*
|
|
157
154
|
* côté serveur EXISTE — {@link ServerRealtimeSocket} (`subscribe/on/publish`), exportée
|
|
158
155
|
* par le barrel du module : c'est elle qu'un service back tient, jamais ce hub
|
|
159
|
-
* directement. Même rôle que `
|
|
156
|
+
* directement. Même rôle que `NodefonySocket` côté navigateur.
|
|
160
157
|
*/
|
|
161
158
|
export declare class RealtimeHub {
|
|
162
159
|
#private;
|
|
@@ -288,14 +285,23 @@ export declare class RealtimeHub {
|
|
|
288
285
|
*/
|
|
289
286
|
unregisterRevocable(entry: IRevocableConnection): void;
|
|
290
287
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
* `onFinish`). Exposé (public) pour un test déterministe sans fake timers.
|
|
288
|
+
* Re-valide toutes les connexions révocables et ferme (`4001`) celles dont
|
|
289
|
+
* l'identité est morte : session détruite ou passée à un autre compte, jeton
|
|
290
|
+
* expiré ou révoqué. Fail-closed : une re-validation qui lève ferme aussi. Le
|
|
291
|
+
* registre est celui des flux SSE ({@link RevocationWatch}) — une garde, deux
|
|
292
|
+
* transports. Exposé (public) pour un test déterministe sans faux minuteur.
|
|
297
293
|
*/
|
|
298
294
|
revalidateRevocable(): Promise<void>;
|
|
295
|
+
/**
|
|
296
|
+
* Ferme tout de suite les connexions qui ont figé cette identité — ses droits
|
|
297
|
+
* ont changé (`IDENTITY_CHANGED_EVENT`). Code {@link IDENTITY_REFRESH_CLOSE_CODE},
|
|
298
|
+
* reconnectable : le client revient avec ses rôles frais. Le registre est
|
|
299
|
+
* celui des flux SSE — une règle, deux transports.
|
|
300
|
+
*
|
|
301
|
+
* @param identifier - identifiant de connexion de l'identité changée.
|
|
302
|
+
* @returns le nombre de connexions fermées.
|
|
303
|
+
*/
|
|
304
|
+
refreshIdentity(identifier: string): number;
|
|
299
305
|
/**
|
|
300
306
|
* Compte une frame entrante full-duplex (canaux gated SIP/bridge). Appelé par le
|
|
301
307
|
* contrôleur quand un handler `realtimeInbound` traite un message client.
|
|
@@ -2,7 +2,7 @@ import { Service, Module, 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";
|
|
5
|
-
import type
|
|
5
|
+
import { type IRealtimeConfig } from "../../config/defineModuleConfig.js";
|
|
6
6
|
import type { IRealtimeAuthenticator } from "../../interfaces/IRealtimeAuthenticator.js";
|
|
7
7
|
import type { IRealtimeAuthenticatorMatcher } from "../../interfaces/IRealtimeAuthenticatorMatcher.js";
|
|
8
8
|
import type { IRealtimeToken } from "../../interfaces/IRealtimeToken.js";
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Le harnais pilote le **côté serveur** : il injecte des frames JSON-RPC 2.0
|
|
14
14
|
* telles qu'un client les enverrait, et lit celles qui sortent. Il n'embarque
|
|
15
|
-
* PAS `
|
|
15
|
+
* PAS `NodefonySocket` — le client est un artefact navigateur du cœur, et le
|
|
16
16
|
* tirer ici le ferait entrer dans le paquet serveur publié. Ce qu'on veut
|
|
17
17
|
* prouver d'un controller (ses canaux, ses actions, son plein-duplex, son
|
|
18
18
|
* nettoyage) s'observe entièrement aux frames.
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*
|
|
30
30
|
* @packageDocumentation
|
|
31
31
|
*/
|
|
32
|
-
import type
|
|
32
|
+
import { type IRealtimeDenied } from "nodefony";
|
|
33
33
|
import type { ContextType } from "@nodefony/http";
|
|
34
34
|
import type { RealtimeController } from "../src/server/RealtimeController.js";
|
|
35
35
|
import { type RealtimeHub, type FrameAuthorizer } from "../src/server/RealtimeHub.js";
|
package/docs/actions.md
CHANGED
|
@@ -117,7 +117,7 @@ attendre trop longtemps et repartir.
|
|
|
117
117
|
|
|
118
118
|
Techniquement, ce numéro est le champ `id` de la frame JSON-RPC 2.0. Le pair l'attribue, garde
|
|
119
119
|
l'appel en attente dans une table, arme une minuterie, et résout la `Promise` quand la réponse
|
|
120
|
-
portant cet `id` revient (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts:
|
|
120
|
+
portant cet `id` revient (`JsonRpcPeer.handleResponse()`, `JsonRpcPeer.ts:579`). Une frame sans
|
|
121
121
|
`id` ne crée aucune de ces trois choses — c'est pourquoi une publication ne coûte rien au repos.
|
|
122
122
|
|
|
123
123
|
## La vision Nodefony — un nom, un handler, une découverte
|
|
@@ -126,18 +126,18 @@ Trois partis pris distinguent les actions Nodefony d'une couche RPC classique.
|
|
|
126
126
|
|
|
127
127
|
**Il n'y a pas de couche RPC séparée.** Une action est une **méthode de contrôleur**, dans la même
|
|
128
128
|
classe que tes canaux et — si tu le veux — que tes routes HTTP. Pas de service dédié, pas de
|
|
129
|
-
schéma à compiler : `@RealtimeAction("orders:quote")` (`realtimeDecorators.ts:
|
|
129
|
+
schéma à compiler : `@RealtimeAction("orders:quote")` (`realtimeDecorators.ts:191`) suffit, et le
|
|
130
130
|
retour de la méthode devient le `result` de la réponse.
|
|
131
131
|
|
|
132
132
|
**Le contrat du handler est minuscule, volontairement.** Une action reçoit **un seul argument**,
|
|
133
133
|
les paramètres bruts du client, et rend une valeur — `RpcActionHandler`
|
|
134
|
-
(`JsonRpcPeer.ts:
|
|
134
|
+
(`JsonRpcPeer.ts:153`). Pas de contexte injecté dans la signature : le `this` est lié à
|
|
135
135
|
l'instance du contrôleur au handshake, ce qui donne accès au noyau, aux services et à la
|
|
136
136
|
connexion sans élargir le contrat.
|
|
137
137
|
|
|
138
138
|
**L'endpoint s'annonce lui-même.** La liste des actions exposées voyage dans la frame d'accueil —
|
|
139
139
|
`IRealtimeWelcome` (`RealtimeController.ts:12`) — et se lit côté client par
|
|
140
|
-
`
|
|
140
|
+
`NodefonySocket.serverMethods` (`NodefonySocket.ts:800`). Une interface n'écrit donc jamais un nom d'action en dur : elle
|
|
141
141
|
n'active un bouton que si le serveur a déclaré savoir le servir.
|
|
142
142
|
|
|
143
143
|
**Le compromis, dit franchement** : une action est **un aller-retour**, point. Elle ne diffuse pas,
|
|
@@ -199,7 +199,7 @@ export default OrdersRealtimeController;
|
|
|
199
199
|
|
|
200
200
|
```ts
|
|
201
201
|
// frontend/src/orders.ts
|
|
202
|
-
import {
|
|
202
|
+
import { NodefonySocket } from "nodefony/client";
|
|
203
203
|
|
|
204
204
|
interface IQuote {
|
|
205
205
|
orderId: string;
|
|
@@ -207,7 +207,7 @@ interface IQuote {
|
|
|
207
207
|
currency: string;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
const socket =
|
|
210
|
+
const socket = NodefonySocket.shared({
|
|
211
211
|
url: "wss://127.0.0.1:5152/orders/realtime",
|
|
212
212
|
});
|
|
213
213
|
|
|
@@ -276,18 +276,18 @@ Le trajet complet d'une requête, des étapes qu'elle traverse aux branches par
|
|
|
276
276
|
|
|
277
277
|
| Voie | Quand la choisir |
|
|
278
278
|
| ------------------------------------------------------ | ----------------------------------------------------------- |
|
|
279
|
-
| `@RealtimeAction("nom")` (`realtimeDecorators.ts:
|
|
280
|
-
| `realtimeActions()` (`RealtimeController.ts:
|
|
279
|
+
| `@RealtimeAction("nom")` (`realtimeDecorators.ts:191`) | le cas courant — un nom fixe, une méthode, déclaratif |
|
|
280
|
+
| `realtimeActions()` (`RealtimeController.ts:281`) | la table est **calculée** (noms dynamiques, boucle, config) |
|
|
281
281
|
|
|
282
282
|
Les deux sont fusionnées au handshake, et **l'override gagne** en cas de conflit de nom : une
|
|
283
283
|
classe peut ainsi remplacer une action héritée sans toucher au parent
|
|
284
|
-
(`RealtimeController.ts:
|
|
284
|
+
(`RealtimeController.ts:598`). Un endpoint sans aucune action ne paie rien — la table du pair est
|
|
285
285
|
allouée au premier enregistrement seulement.
|
|
286
286
|
|
|
287
287
|
### Ce que ton `return` et ton `throw` deviennent sur le fil
|
|
288
288
|
|
|
289
289
|
Le retour du handler est envoyé tel quel en `result`. Les erreurs, elles, suivent une règle Zero
|
|
290
|
-
Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:
|
|
290
|
+
Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:526`) :
|
|
291
291
|
|
|
292
292
|
| Côté serveur | Ce que reçoit le client | Pourquoi |
|
|
293
293
|
| ------------------------------- | ---------------------------------------- | ----------------------------------------------- |
|
|
@@ -299,7 +299,7 @@ Trust stricte, appliquée dans `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts:49
|
|
|
299
299
|
|
|
300
300
|
> [!WARNING]
|
|
301
301
|
> **Le temps réel n'a qu'UNE erreur, et elle vient du cœur.** Pour choisir ce que voit le client,
|
|
302
|
-
> c'est `RpcError` (`JsonRpcPeer.ts:
|
|
302
|
+
> c'est `RpcError` (`JsonRpcPeer.ts:101`), importée depuis `nodefony` — son `code` est un entier
|
|
303
303
|
> JSON-RPC et son `data` traverse le fil. Toute autre exception donne un `-32603` opaque. Ne pas
|
|
304
304
|
> chercher d'erreur maison dans `@nodefony/realtime` : il n'y en a pas, et
|
|
305
305
|
> `tests/unit/errorSurface.test.ts` refuse d'en exporter une que personne ne lève.
|
|
@@ -360,21 +360,55 @@ donc réservée, quoi qu'en dise son décorateur. À savoir avant de nommer une
|
|
|
360
360
|
|
|
361
361
|
Pour exiger un rôle sur tes propres actions, on déclare une règle de préfixe dans la
|
|
362
362
|
configuration de sécurité — la même liste que pour les canaux
|
|
363
|
-
(`realtimeChannels`, `security/nodefony/config/config.ts:
|
|
363
|
+
(`realtimeChannels`, `security/nodefony/config/config.ts:1447`) :
|
|
364
364
|
|
|
365
365
|
```ts ignore
|
|
366
366
|
use("@nodefony/security", {
|
|
367
367
|
realtimeChannels: [
|
|
368
368
|
// Couvre AUSSI bien le canal `orders:feed` que l'action `orders:quote` :
|
|
369
369
|
// le verrou résout une politique par NOM, sans savoir ce qu'il garde.
|
|
370
|
-
{
|
|
371
|
-
prefix: "orders:",
|
|
372
|
-
policy: { authenticated: true, roles: ["ROLE_USER"] },
|
|
373
|
-
},
|
|
370
|
+
{ pattern: "orders:", authenticated: true, roles: ["ROLE_USER"] },
|
|
374
371
|
],
|
|
375
372
|
});
|
|
376
373
|
```
|
|
377
374
|
|
|
375
|
+
### Savoir QUI appelle — `@CurrentUser` et les paramètres décorés
|
|
376
|
+
|
|
377
|
+
Une action authentifiée sait que l'appelant est identifié ; elle peut aussi savoir **qui** il est.
|
|
378
|
+
Les décorateurs de paramètres d'une route y prennent le même sens
|
|
379
|
+
(`actionParamsWrapper`, `RealtimeController.ts:1007`) :
|
|
380
|
+
|
|
381
|
+
```ts ignore
|
|
382
|
+
import { Body, CurrentUser } from "@nodefony/framework";
|
|
383
|
+
import type { IUser } from "@nodefony/user";
|
|
384
|
+
|
|
385
|
+
// Le client : socket.request("chat:send", { text: "salut" })
|
|
386
|
+
@RealtimeAction("chat:send")
|
|
387
|
+
send(@Body("text") text: string, @CurrentUser() user: IUser) {
|
|
388
|
+
// L'auteur vient du SERVEUR : le client ne peut pas mentir dessus.
|
|
389
|
+
return { author: user.identifier, text };
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
| Décorateur | Ce qu'il reçoit dans une action |
|
|
394
|
+
| ----------------------------------------- | ----------------------------------------------------------- |
|
|
395
|
+
| `@Body()` / `@Body("clé")` | la charge de l'appel (`params`), ou l'une de ses clés |
|
|
396
|
+
| `@CurrentUser()` | l'utilisateur de la connexion, `undefined` pour un anonyme |
|
|
397
|
+
| `@Query()` / `@Param()` | un objet vide — une action n'a ni URL ni variables de route |
|
|
398
|
+
| `@Headers()` / `@Cookie()` / `@Session()` | ceux de la requête d'ouverture de la socket |
|
|
399
|
+
|
|
400
|
+
Une action **sans** paramètre décoré reçoit la charge brute en premier argument, comme avant —
|
|
401
|
+
rien ne change pour elle, ni comportement ni coût.
|
|
402
|
+
|
|
403
|
+
> [!IMPORTANT]
|
|
404
|
+
> **L'identité est celle de l'ouverture de la socket.** Elle n'est pas relue à chaque appel : le
|
|
405
|
+
> hub la revalide toutes les 30 secondes (`REVOCATION_REVALIDATE_MS`, `RealtimeHub.ts:112`) et
|
|
406
|
+
> ferme la socket en `4001` si la session est morte (`RealtimeHub.ts:842`). Après une
|
|
407
|
+
> déconnexion, une action peut donc encore voir l'ancien utilisateur pendant cette fenêtre. Pour
|
|
408
|
+
> une écriture sensible — paiement, droits, suppression — passe par une route
|
|
409
|
+
> `POST + WEBSOCKET` appelée par `socket.mutate()` : ce pont revalide l'identité à **chaque**
|
|
410
|
+
> appel et porte la clé d'idempotence.
|
|
411
|
+
|
|
378
412
|
### La règle DEV-only
|
|
379
413
|
|
|
380
414
|
Une action réservée au développement se contrôle **dans le handler, côté serveur** — jamais par un
|
|
@@ -394,7 +428,7 @@ drapeau d'interface. Cacher un bouton n'empêche personne de forger la frame. Le
|
|
|
394
428
|
### Le délai d'expiration est la seule libération automatique
|
|
395
429
|
|
|
396
430
|
`request()` prend le délai en **troisième argument positionnel**, en millisecondes — il n'y a pas
|
|
397
|
-
d'objet d'options (`
|
|
431
|
+
d'objet d'options (`NodefonySocket.request()`, `NodefonySocket.ts:936`) :
|
|
398
432
|
|
|
399
433
|
```ts ignore
|
|
400
434
|
await socket.request("orders:quote", { orderId }); // 30 000 ms par défaut
|
|
@@ -403,12 +437,12 @@ await socket.request("orders:export", { scope }, 120_000); // action longue
|
|
|
403
437
|
|
|
404
438
|
| Défaut | Valeur | Ancrage |
|
|
405
439
|
| --------- | --------- | -------------------- |
|
|
406
|
-
| `request` | 30 000 ms | `JsonRpcPeer.ts:
|
|
440
|
+
| `request` | 30 000 ms | `JsonRpcPeer.ts:353` |
|
|
407
441
|
|
|
408
442
|
À l'expiration, l'entrée en attente est **retirée** et la `Promise` rejetée avec
|
|
409
|
-
`RPC timeout: <méthode>` (`JsonRpcPeer.ts:
|
|
443
|
+
`RPC timeout: <méthode>` (`JsonRpcPeer.ts:502`). Conséquence à connaître : une réponse qui
|
|
410
444
|
arriverait après coup ne trouve plus personne et est ignorée en silence
|
|
411
|
-
(`JsonRpcPeer.ts:
|
|
445
|
+
(`JsonRpcPeer.ts:584`) — pas de résolution tardive, pas de fuite.
|
|
412
446
|
|
|
413
447
|
### Abandonner : ce qui existe vraiment
|
|
414
448
|
|
|
@@ -422,8 +456,8 @@ Trois leviers existent, et ils ne font pas la même chose :
|
|
|
422
456
|
|
|
423
457
|
1. **Le délai d'expiration** — libère le client, laisse le serveur travailler.
|
|
424
458
|
2. **La fermeture de la connexion** — `dispose()` rejette **tous** les appels en attente d'un
|
|
425
|
-
coup (`JsonRpcPeer.ts:
|
|
426
|
-
(`RealtimeController.ts:
|
|
459
|
+
coup (`JsonRpcPeer.ts:480`), appelé au nettoyage de la socket
|
|
460
|
+
(`RealtimeController.ts:749`). Là encore : côté client seulement.
|
|
427
461
|
3. **Une action compagnon** — la seule vraie annulation. On expose une seconde action qui prend
|
|
428
462
|
l'identifiant du travail et l'interrompt côté serveur. Le modèle du dépôt est
|
|
429
463
|
`nodefony:scaffold:cancel` (`StudioRealtimeController.ts:159`), pendant de `nodefony:scaffold:run`.
|
|
@@ -444,7 +478,7 @@ fois ? »**.
|
|
|
444
478
|
| une mutation (`socket.mutate`) | **non** par nature | oui, **avec une clé d'idempotence** |
|
|
445
479
|
|
|
446
480
|
Pour les mutations passant par le pont API, la clé n'est pas une convention : elle est **exigée
|
|
447
|
-
par la signature** de `mutate()` (`
|
|
481
|
+
par la signature** de `mutate()` (`NodefonySocket.ts:1009`), et c'est la garde `@Idempotent`
|
|
448
482
|
(`routerDecorators.ts:1171`) qui, côté serveur, reconnaît le rejeu et rend la réponse déjà calculée
|
|
449
483
|
au lieu de refaire l'effet.
|
|
450
484
|
|
|
@@ -485,7 +519,7 @@ prudente est donc de **s'abonner avant** de lancer l'appel.
|
|
|
485
519
|
|
|
486
520
|
Nodefony a choisi la garantie côté serveur plutôt que la discipline côté client : le producteur du
|
|
487
521
|
canal **rejoue son historique** au nouvel abonné, de sorte qu'un arrivant tardif voit tout depuis
|
|
488
|
-
le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:
|
|
522
|
+
le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:456`). C'est ce qui autorise le front
|
|
489
523
|
à faire l'appel d'abord et à s'abonner ensuite, sans rien perdre.
|
|
490
524
|
|
|
491
525
|
> [!TIP]
|
|
@@ -495,7 +529,7 @@ le début (`ScaffoldService.subscribe()`, `ScaffoldService.ts:425`). C'est ce qu
|
|
|
495
529
|
|
|
496
530
|
### Et le streaming du protocole ?
|
|
497
531
|
|
|
498
|
-
**Il n'y en a pas.** Une action rend **une** valeur (`RpcActionHandler`, `JsonRpcPeer.ts:
|
|
532
|
+
**Il n'y en a pas.** Une action rend **une** valeur (`RpcActionHandler`, `JsonRpcPeer.ts:153`), que
|
|
499
533
|
le pair emballe en une réponse unique. Pour tout ce qui progresse — une réponse mot à mot d'un
|
|
500
534
|
modèle de langage, un export qui avance, un traitement long — la voie est le motif
|
|
501
535
|
**« travail + canal »** : l'action accuse réception, et la progression arrive sur un canal.
|
|
@@ -510,32 +544,32 @@ il sera conçu avec son premier consommateur réel.
|
|
|
510
544
|
Un cas particulier mérite d'être connu avant d'écrire une action : **elle existe peut-être déjà en
|
|
511
545
|
HTTP**. Le pont API expose la méthode `api.request`, qui rejoue une route de contrôleur sur la
|
|
512
546
|
socket, avec la même garde et le même résultat qu'en REST — `invokeApiRequest()`
|
|
513
|
-
(`RealtimeController.ts:
|
|
514
|
-
`realtimeApiRequest()` (`RealtimeController.ts:
|
|
547
|
+
(`RealtimeController.ts:1070`). Il est **désactivé par défaut** et s'active en surchargeant
|
|
548
|
+
`realtimeApiRequest()` (`RealtimeController.ts:321`).
|
|
515
549
|
|
|
516
550
|
```ts ignore
|
|
517
551
|
const modules = await socket.request("/nodefony/kernel/api/modules");
|
|
518
552
|
```
|
|
519
553
|
|
|
520
554
|
La forme se discrimine toute seule : un chemin commence par `/`, jamais un nom d'action —
|
|
521
|
-
`
|
|
555
|
+
`NodefonySocket.request()` (`NodefonySocket.ts:936`). Écris une action RPC pour ce qui n'a de sens **que** sur la socket ;
|
|
522
556
|
passe par le pont pour tout ce qui est déjà une route. Le détail du pont vit dans le
|
|
523
557
|
[vocabulaire](./vocabulaire.md) et l'[architecture](./architecture.md).
|
|
524
558
|
|
|
525
559
|
## ⚠️ Pièges
|
|
526
560
|
|
|
527
|
-
| Symptôme
|
|
528
|
-
|
|
|
529
|
-
| `RPC timeout: <méthode>` alors que le serveur a bien répondu
|
|
530
|
-
| Le client attend indéfiniment, aucune erreur
|
|
531
|
-
| `-32601 method not found`
|
|
532
|
-
| `-32603 internal error` sans détail
|
|
533
|
-
| `-32001 unauthorized` sur une action légitime
|
|
534
|
-
| Une action
|
|
535
|
-
| Un travail relancé crée deux jobs
|
|
536
|
-
| `abort()` introuvable sur la socket
|
|
537
|
-
| Les premières lignes d'un job manquent
|
|
538
|
-
| Toutes les `Promise` rejettent d'un coup
|
|
561
|
+
| Symptôme | Cause | Correction |
|
|
562
|
+
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
563
|
+
| `RPC timeout: <méthode>` alors que le serveur a bien répondu | le handler dure plus que 30 s, le défaut | passer le délai en **3ᵉ argument** : `request(m, p, 120_000)` |
|
|
564
|
+
| Le client attend indéfiniment, aucune erreur | le handler ne rend jamais (il publie au lieu de retourner) — pas de frame `result` | toujours `return` une valeur ; publier **en plus**, jamais **à la place** |
|
|
565
|
+
| `-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 |
|
|
566
|
+
| `-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 |
|
|
567
|
+
| `-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` |
|
|
568
|
+
| 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 |
|
|
569
|
+
| 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 |
|
|
570
|
+
| `abort()` introuvable sur la socket | il n'existe pas — et n'arrêterait pas le serveur de toute façon | exposer une action d'annulation qui prend l'identifiant du travail |
|
|
571
|
+
| Les premières lignes d'un job manquent | abonnement au canal après le début de la production, sans historique rejoué | s'abonner **avant** de lancer, ou faire rejouer l'historique par le producteur |
|
|
572
|
+
| Toutes les `Promise` rejettent d'un coup | la connexion s'est fermée : `dispose()` vide la table des appels en attente | attendu ; relancer après reconnexion, l'appel n'a pas survécu |
|
|
539
573
|
|
|
540
574
|
## 🧪 Tests & couverture
|
|
541
575
|
|
|
@@ -548,7 +582,7 @@ vivent dans la carte de l'aperçu, régénérée depuis les résultats réels, j
|
|
|
548
582
|
- **Unitaires, déclaration** (`realtimeDecorators.test.ts`) : `@RealtimeAction` enregistre bien le
|
|
549
583
|
nom et lie le `this` ; (`RealtimeController.test.ts`) : fusion décorateurs + override et
|
|
550
584
|
annonce dans l'accueil.
|
|
551
|
-
- **Unitaires, client** (`
|
|
585
|
+
- **Unitaires, client** (`NodefonySocketCoverage.test.ts`) : formes de `request`, contrat de
|
|
552
586
|
streaming, rejet des appels en attente au `dispose`.
|
|
553
587
|
- **Attaque, autorisation** (`realtimeFrameLock.test.ts`) : `nodefony:kernel:ping` et `nodefony:kernel:gc` refusés à
|
|
554
588
|
l'anonyme **et** à l'utilisateur authentifié, acceptés à l'administrateur — la preuve que le
|
|
@@ -559,6 +593,55 @@ vivent dans la carte de l'aperçu, régénérée depuis les résultats réels, j
|
|
|
559
593
|
fan-out, pas celui des appels corrélés). Pour en monter un, voir le skill
|
|
560
594
|
`nodefony-load-test` ; pour la mémoire, `nodefony-check-memory-health`.
|
|
561
595
|
|
|
596
|
+
### Éprouver SON action — le harnais, identité comprise
|
|
597
|
+
|
|
598
|
+
Une application teste ses actions sans serveur ni navigateur avec `createRealtimeHarness`
|
|
599
|
+
(`@nodefony/realtime/testing`, `nodefony/testing/index.ts:206`) : il monte le contrôleur sur une
|
|
600
|
+
fausse connexion, envoie les frames qu'un client enverrait et rend celles qui sortent. Sans option,
|
|
601
|
+
la connexion est **anonyme** — le repli Zero Trust. Une action qui lit `@CurrentUser()` s'éprouve
|
|
602
|
+
donc avec l'identité qu'aurait posée le pare-feu au handshake, passée en `identity`
|
|
603
|
+
(`nodefony/testing/index.ts:93`) :
|
|
604
|
+
|
|
605
|
+
```ts ignore
|
|
606
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
607
|
+
import { getRealtimeHub } from "@nodefony/realtime";
|
|
608
|
+
import type { IRealtimeToken } from "@nodefony/realtime";
|
|
609
|
+
import { createRealtimeHarness } from "@nodefony/realtime/testing";
|
|
610
|
+
import ChatController from "../nodefony/controllers/ChatController";
|
|
611
|
+
|
|
612
|
+
describe("ChatController — actions", () => {
|
|
613
|
+
afterEach(() => getRealtimeHub().clear());
|
|
614
|
+
|
|
615
|
+
it("l'action reçoit son appelant par @CurrentUser", async () => {
|
|
616
|
+
const alice = { identifier: "alice" };
|
|
617
|
+
const attributes = new Map<string, unknown>([["user", alice]]);
|
|
618
|
+
const identity: IRealtimeToken = {
|
|
619
|
+
type: "session",
|
|
620
|
+
getUserIdentifier: () => alice.identifier,
|
|
621
|
+
isAuthenticated: () => true,
|
|
622
|
+
getRoles: () => ["ROLE_USER"],
|
|
623
|
+
getScopes: () => [],
|
|
624
|
+
getAttribute: <T>(key: string) => attributes.get(key) as T | undefined,
|
|
625
|
+
};
|
|
626
|
+
const h = createRealtimeHarness((ctx) => new ChatController(ctx), {
|
|
627
|
+
identity,
|
|
628
|
+
});
|
|
629
|
+
await h.connect();
|
|
630
|
+
const me = await h.call<{ identifier: string }>("chat:whoami");
|
|
631
|
+
expect(me.identifier).toBe("alice");
|
|
632
|
+
h.dispose();
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
C'est l'attribut `user` du jeton que `@CurrentUser()` rend. Pour éprouver un **refus** (`@IsGranted`,
|
|
638
|
+
`policy` de canal), il faut en plus le verrou de frame de `@nodefony/security`, passé en
|
|
639
|
+
`frameAuthorizer` : `@nodefony/realtime` ne connaît pas la sécurité, et sans ce verrou une règle
|
|
640
|
+
déclarée n'est appliquée par personne — en test comme en production. La table des membres du
|
|
641
|
+
harnais et l'exemple de refus sont dans le `README.md` du paquet, section « Tester un controller ».
|
|
642
|
+
`nodefony create controller --kind realtime` génère ce test, identité comprise, quand l'application
|
|
643
|
+
porte la sécurité.
|
|
644
|
+
|
|
562
645
|
Couverture : `npm run coverage` dans `@nodefony/realtime`.
|
|
563
646
|
|
|
564
647
|
## 🔗 Pour aller plus loin
|