@nodefony/realtime 10.0.0-beta.2 → 10.0.0-beta.4
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.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +18 -13
- package/dist/nodefony/config/config.js +7 -5
- package/dist/nodefony/config/configSchema.js +18 -0
- package/dist/nodefony/config/defineModuleConfig.js +43 -17
- package/dist/nodefony/decorators/realtimeDecorators.js +69 -19
- package/dist/nodefony/interfaces/IRealtimeConnectionState.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +1 -2
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +1 -2
- package/dist/nodefony/src/backplane/RedisBackplane.js +22 -6
- package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
- package/dist/nodefony/src/backplane/bootNotices.js +42 -0
- package/dist/nodefony/src/backplane/envelope.js +1 -2
- package/dist/nodefony/src/backplane/originId.js +1 -2
- package/dist/nodefony/src/backplane/publishQueue.js +1 -5
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +16 -6
- package/dist/nodefony/src/guards.js +40 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +2 -6
- package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
- package/dist/nodefony/src/server/RealtimeController.js +226 -82
- package/dist/nodefony/src/server/RealtimeHub.js +32 -54
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +30 -13
- package/dist/nodefony/src/server/deniedDetail.js +1 -2
- package/dist/nodefony/src/server/subscribedAck.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +5 -4
- package/dist/nodefony/src/server/welcomeEnv.js +1 -2
- package/dist/nodefony/src/service/RealtimeService.js +4 -4
- package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
- package/dist/nodefony/testing/index.js +118 -5
- package/dist/types/index.d.ts +1 -0
- 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/IRealtimeConnectionState.d.ts +40 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +13 -0
- package/dist/types/nodefony/interfaces/index.d.ts +1 -0
- 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 +82 -4
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +22 -16
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +18 -8
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +23 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +1 -1
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +26 -1
- package/dist/types/nodefony/testing/index.d.ts +8 -3
- package/docs/actions.md +129 -45
- package/docs/architecture.md +143 -135
- package/docs/cadence-et-contre-pression.md +643 -0
- package/docs/configuration.md +126 -97
- package/docs/cookbook-chat.md +50 -43
- package/docs/index.md +63 -38
- package/docs/observabilite.md +58 -56
- package/docs/protocole.md +110 -76
- package/docs/securite.md +128 -117
- package/docs/vocabulaire.md +133 -129
- package/package.json +17 -13
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
- package/docs/couverture-bancs.md +0 -139
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
1
|
+
import type { z } from "zod";
|
|
2
2
|
import { realtimeConfigSchema, type IRealtimeSchemaConfig } from "./config.js";
|
|
3
3
|
import type { IBackplane } from "../interfaces/IBackplane.js";
|
|
4
4
|
/**
|
|
@@ -19,11 +19,13 @@ import type { IBackplane } from "../interfaces/IBackplane.js";
|
|
|
19
19
|
* faire dans un JSON Schema Studio). L'instance, si fournie, prend précédence
|
|
20
20
|
* sur `driver` côté service ; ce dernier reste source d'introspection/UI.
|
|
21
21
|
*
|
|
22
|
-
* **
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* (
|
|
26
|
-
*
|
|
22
|
+
* **Variables d'environnement** : `NF_REALTIME_DRIVER`,
|
|
23
|
+
* `NF_REALTIME_BACKPLANE_SECRET` et `NF_REALTIME_BACKPLANE_NAMESPACE` ne se
|
|
24
|
+
* lisent PAS ici. Chacune est déclarée sur son champ dans `config.ts`
|
|
25
|
+
* (`.meta({ env })`) et posée par le Kernel AVANT la validation : le schéma
|
|
26
|
+
* les valide comme une valeur de l'application (un secret de moins de 32
|
|
27
|
+
* caractères arrête le démarrage). Un appel direct de ce builder, hors Kernel,
|
|
28
|
+
* ne les voit donc pas.
|
|
27
29
|
*
|
|
28
30
|
* @param config - configuration brute (sections omises = défauts sûrs).
|
|
29
31
|
* @param options.backplane - instance `IBackplane` custom (override le driver à
|
|
@@ -34,6 +36,27 @@ import type { IBackplane } from "../interfaces/IBackplane.js";
|
|
|
34
36
|
export declare function defineRealtimeConfig(config?: IRealtimeConfigInput, options?: {
|
|
35
37
|
backplane?: IBackplane;
|
|
36
38
|
}): IRealtimeConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Le builder pour une entrée NON typée — la config reçue par le module au
|
|
41
|
+
* démarrage, dont le schéma décide seul de la forme. Interne au paquet : une
|
|
42
|
+
* application passe par {@link defineRealtimeConfig}, qui type ce qu'elle écrit.
|
|
43
|
+
*
|
|
44
|
+
* @param config - configuration brute, validée par le schéma
|
|
45
|
+
* @param options.backplane - instance `IBackplane` custom
|
|
46
|
+
* @returns config gelée prête pour `RealtimeService`.
|
|
47
|
+
* @throws BootConfigurationError si invalide.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildRealtimeConfig(config?: unknown, options?: {
|
|
50
|
+
backplane?: IBackplane;
|
|
51
|
+
}): IRealtimeConfig;
|
|
52
|
+
/**
|
|
53
|
+
* Vrai si la valeur est une config produite par {@link buildRealtimeConfig}
|
|
54
|
+
* (donc validée par le schéma). Lu une fois, à l'init du service.
|
|
55
|
+
*
|
|
56
|
+
* @param value - la config lue sur le module
|
|
57
|
+
* @returns `true` si elle sort du builder
|
|
58
|
+
*/
|
|
59
|
+
export declare function isRealtimeConfig(value: unknown): value is IRealtimeConfig;
|
|
37
60
|
/**
|
|
38
61
|
* JSON Schema introspectable de la config realtime — destiné au formulaire
|
|
39
62
|
* d'édition Studio (futur). N'inclut PAS `backplane.instance` (non sérialisable).
|
|
@@ -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. */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { JsonRpcPeer } from "nodefony";
|
|
2
|
+
import type { WsConnectionTransport } from "../src/transport/WsConnectionTransport.js";
|
|
3
|
+
import type { ChannelSink } from "../src/server/RealtimeHub.js";
|
|
4
|
+
import type { RealtimeInboundHandler } from "./IRealtimeController.js";
|
|
5
|
+
/**
|
|
6
|
+
* État du protocole temps réel d'UNE connexion WebSocket — posé au welcome, et
|
|
7
|
+
* relu à chaque frame jusqu'à la fermeture.
|
|
8
|
+
*
|
|
9
|
+
* Il vit sur la CONNEXION (`WebsocketContext`), pas sur le contrôleur : un
|
|
10
|
+
* `forward` fait construire une instance neuve qui sert la même socket, et
|
|
11
|
+
* l'état doit lui survivre.
|
|
12
|
+
*
|
|
13
|
+
* Le pair y est rangé sous son contrat EFFACÉ, ce qu'il est d'ailleurs à sa
|
|
14
|
+
* construction. Plusieurs contrôleurs peuvent servir la même connexion, et aucun
|
|
15
|
+
* type ne relie cet état à l'un d'eux : chacun le relit sous son propre contrat
|
|
16
|
+
* (`RealtimeController.requestClient` → `JsonRpcPeer.requestAs`).
|
|
17
|
+
*/
|
|
18
|
+
export interface IRealtimeConnectionState {
|
|
19
|
+
/** `realtime:welcome` émis : la poignée de main ne se rejoue pas. */
|
|
20
|
+
welcomed: boolean;
|
|
21
|
+
/** Le pair JSON-RPC de la connexion, sous son contrat effacé. */
|
|
22
|
+
peer: JsonRpcPeer;
|
|
23
|
+
/** Le transport qui relie le pair à la socket (contre-pression, sonde). */
|
|
24
|
+
transport: WsConnectionTransport;
|
|
25
|
+
/** Canal → sink de CETTE connexion auprès du hub partagé, pour s'en désabonner. */
|
|
26
|
+
channels: Map<string, ChannelSink>;
|
|
27
|
+
/** Canaux full-duplex qui acceptent une entrée client ; `null` s'il n'y en a aucun. */
|
|
28
|
+
inbound: Record<string, RealtimeInboundHandler> | null;
|
|
29
|
+
}
|
|
30
|
+
declare module "@nodefony/http" {
|
|
31
|
+
interface IWebsocketConnectionState {
|
|
32
|
+
/**
|
|
33
|
+
* État du protocole temps réel de la connexion, posé par
|
|
34
|
+
* `@nodefony/realtime` au welcome ; `undefined` avant, et sur toute socket
|
|
35
|
+
* qu'aucun `RealtimeController` ne sert. Réservé au module : le lire,
|
|
36
|
+
* jamais l'écrire.
|
|
37
|
+
*/
|
|
38
|
+
__nfRealtime?: IRealtimeConnectionState;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -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>;
|
|
@@ -45,4 +45,17 @@ export interface IRealtimeToken {
|
|
|
45
45
|
* (publish/subscribe), payé seulement sur `api.request`.
|
|
46
46
|
*/
|
|
47
47
|
isValid?(nowMs?: number): boolean | Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* Le COMPTE qui a ouvert la socket existe-t-il toujours, actif, et est-ce
|
|
50
|
+
* bien le même ? `false` si le compte a disparu, a été désactivé ou
|
|
51
|
+
* verrouillé, ou si un AUTRE compte a repris son identifiant (suppression
|
|
52
|
+
* hors framework puis recréation). Optionnel — absent = considéré confirmé.
|
|
53
|
+
*
|
|
54
|
+
* Distinct d'{@link isValid} parce que son coût l'est : il relit l'annuaire
|
|
55
|
+
* des comptes (une lecture, comme une requête HTTP en zone), là où
|
|
56
|
+
* `isValid` ne lit qu'un store de jetons ou de sessions. Appelé par le pont
|
|
57
|
+
* `api.request` SEULEMENT, jamais par la re-validation périodique du hub —
|
|
58
|
+
* qui le paierait pour chaque socket ouverte, à chaque tour.
|
|
59
|
+
*/
|
|
60
|
+
confirmAccount?(): Promise<boolean>;
|
|
48
61
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
export type { IBackplane, IBackplaneMessage, BackplaneHandler, } from "./IBackplane.js";
|
|
12
12
|
export type { IRealtimeController, RealtimePublish, RealtimeInboundHandler, } from "./IRealtimeController.js";
|
|
13
13
|
export type { IChannelPolicy } from "./IChannelPolicy.js";
|
|
14
|
+
export type { IRealtimeConnectionState } from "./IRealtimeConnectionState.js";
|
|
14
15
|
export type { IRealtimeProbe, IRealtimeHealth, IRealtimeClusterHealth, IRealtimeChannelStat, IRealtimeConnProbe, } from "./IRealtimeProbe.js";
|
|
15
16
|
export type { IRealtimeToken } from "./IRealtimeToken.js";
|
|
16
17
|
export type { IRealtimeHandshake } from "./IRealtimeHandshake.js";
|
|
@@ -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 —
|
|
@@ -26,6 +26,7 @@ import type { IRealtimeController, RealtimePublish, RealtimeInboundHandler } fro
|
|
|
26
26
|
* d'action n'est payé que sur une requête `id`. C'est aussi le seam du backplane Redis.
|
|
27
27
|
*/
|
|
28
28
|
export declare abstract class RealtimeController<Emit extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> extends Controller implements IRealtimeController {
|
|
29
|
+
#private;
|
|
29
30
|
/**
|
|
30
31
|
* Une instance PAR CONNEXION, par construction — le défaut `singleton` des
|
|
31
32
|
* contrôleurs ne s'applique pas ici. Le pair JSON-RPC de la connexion capture
|
|
@@ -111,15 +112,48 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
111
112
|
* est structurel, pas un choix : c'est au client d'attendre `realtime:welcome`
|
|
112
113
|
* avant de pousser.
|
|
113
114
|
*
|
|
114
|
-
* ⚠️ Cette phrase a longtemps ajouté « ce que
|
|
115
|
+
* ⚠️ Cette phrase a longtemps ajouté « ce que le client fait nativement ».
|
|
115
116
|
* C'était FAUX : le client rejouait ses abonnements dès l'ouverture de la socket,
|
|
116
117
|
* si bien qu'un `subscribe` posé avant le démarrage et TOUS ceux d'après une
|
|
117
118
|
* reconnexion étaient perdus ici même, sans un mot des deux côtés. Un contrat écrit
|
|
118
119
|
* d'un seul côté du fil n'est pas tenu ; il l'est désormais par
|
|
119
|
-
* `
|
|
120
|
-
* écoute (welcome) » de `
|
|
120
|
+
* `NodefonySocket.replaySubscriptions` et les deux cas « la fenêtre où le serveur
|
|
121
|
+
* écoute (welcome) » de `NodefonySocketCoverage.test.ts`.
|
|
121
122
|
*/
|
|
122
123
|
protected handleRealtime(message: string | Buffer | null): void;
|
|
124
|
+
/**
|
|
125
|
+
* Lit les trames BINAIRES de cette connexion — le point d'extension d'un
|
|
126
|
+
* protocole binaire écrit par l'application, à côté du JSON-RPC ; pendant
|
|
127
|
+
* serveur de `NodefonySocket.onBinary`.
|
|
128
|
+
*
|
|
129
|
+
* Le protocole temps réel est du JSON-RPC texte : il ne sait pas à quel canal
|
|
130
|
+
* une trame binaire appartient. Sans cette méthode, une trame binaire est
|
|
131
|
+
* écartée, comptée (`binaryDiscarded` de la sonde de connexion) et journalisée
|
|
132
|
+
* une fois par connexion. La surcharger la reçoit telle quelle, octets exacts,
|
|
133
|
+
* en `Uint8Array` — le type que livre aussi `NodefonySocket.onBinary` dans le
|
|
134
|
+
* navigateur : un décodeur s'écrit une fois pour les deux côtés. À l'exécution
|
|
135
|
+
* c'est un `Buffer` de `ws`, qui en est un.
|
|
136
|
+
*
|
|
137
|
+
* ⚠️ Hors du verrou de trame, et pour toute la série 10 : la trame brute ne
|
|
138
|
+
* passe ni par l'autorisation `beforeDispatch` ni par les gardes d'action et de
|
|
139
|
+
* canal du JSON-RPC. La connexion, elle, a déjà franchi Origin, l'authentificateur
|
|
140
|
+
* et le pare-feu. L'identité se lit par `RequestContext.getAuthenticatedUser()`,
|
|
141
|
+
* et toute décision d'accès sur une trame binaire appartient à l'application.
|
|
142
|
+
*
|
|
143
|
+
* ⚠️ Un `DataView` se construit sur `data.buffer` AVEC `data.byteOffset` et
|
|
144
|
+
* `data.byteLength` : un `Buffer` issu du pool de Node ne commence pas à 0.
|
|
145
|
+
*
|
|
146
|
+
* @param data - les octets reçus.
|
|
147
|
+
*/
|
|
148
|
+
protected onRealtimeBinary?(data: Uint8Array): void;
|
|
149
|
+
/**
|
|
150
|
+
* Faute de câblage : `handleRealtime` reçu par une instance qui ne sert pas
|
|
151
|
+
* une connexion WebSocket — route sans `methods: ["WEBSOCKET"]`, ou faux
|
|
152
|
+
* contexte de test sans `type`. Levée plutôt que passée sous silence : le
|
|
153
|
+
* client attendrait un welcome qui ne vient jamais. Chemin froid.
|
|
154
|
+
*/
|
|
155
|
+
private outsideWebsocketError;
|
|
156
|
+
private receiveBinary;
|
|
123
157
|
/**
|
|
124
158
|
* **L1 — duplex serveur→client.** Émet une REQUÊTE vers le client de CETTE
|
|
125
159
|
* connexion (sur une action qu'il a exposée via `client.register(...)`) et attend
|
|
@@ -143,6 +177,22 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
143
177
|
* handshake n'est pas terminé.
|
|
144
178
|
*/
|
|
145
179
|
protected notifyClient<K extends EventNames<Emit>>(method: K, params?: EventPayload<Emit, K>): void;
|
|
180
|
+
/**
|
|
181
|
+
* Émet une trame BINAIRE vers CETTE connexion, sans la sérialiser — pendant
|
|
182
|
+
* sortant de {@link onRealtimeBinary}, et de `NodefonySocket.sendBinary` côté
|
|
183
|
+
* client. Même contre-pression que les frames JSON-RPC.
|
|
184
|
+
*
|
|
185
|
+
* @param data - les octets à émettre (`Uint8Array`, donc un `Buffer` aussi).
|
|
186
|
+
* @returns `false` si le handshake n'est pas terminé, la connexion fermée ou
|
|
187
|
+
* la trame jetée par la contre-pression.
|
|
188
|
+
*/
|
|
189
|
+
protected sendBinaryToClient(data: Uint8Array): boolean;
|
|
190
|
+
/**
|
|
191
|
+
* État realtime de CETTE connexion, `undefined` avant le welcome — pour les
|
|
192
|
+
* chemins FROIDS (requête sortante, binaire). `notifyClient` et la réception
|
|
193
|
+
* gardent leur lecture en ligne : ils sont sur le chemin de chaque frame.
|
|
194
|
+
*/
|
|
195
|
+
private coldConnState;
|
|
146
196
|
/**
|
|
147
197
|
* Handshake : seams sécurité #4 (Origin RFC 6455 §10.2) + #2 (authenticator
|
|
148
198
|
* réseau) PUIS crée peer+transport, enregistre les actions, welcome + cleanup.
|
|
@@ -164,8 +214,36 @@ export declare abstract class RealtimeController<Emit extends EventsMap = Defaul
|
|
|
164
214
|
* canaux FULL-DUPLEX déclarés (entrée client → handler `realtimeInbound`).
|
|
165
215
|
*/
|
|
166
216
|
private onRealtimeNotification;
|
|
217
|
+
/**
|
|
218
|
+
* Acquitte un abonnement ACCEPTÉ (`realtime:subscribed`) — le seul signal
|
|
219
|
+
* qui dise au client à partir de quand le flux du canal lui est livré.
|
|
220
|
+
*
|
|
221
|
+
* Un refus n'y passe jamais : il a `realtime:denied`. Cold path (1×/canal et
|
|
222
|
+
* par connexion).
|
|
223
|
+
*/
|
|
224
|
+
private acknowledgeSubscription;
|
|
167
225
|
/** Abonne la connexion à un canal via le hub partagé (idempotent par connexion). */
|
|
168
226
|
private startChannel;
|
|
227
|
+
/**
|
|
228
|
+
* Enveloppe des actions `@RealtimeAction` dont un paramètre est décoré
|
|
229
|
+
* (`@Body`, `@CurrentUser`…) : l'action reçoit ses arguments comme une route,
|
|
230
|
+
* résolus par la fonction du framework et non par une copie.
|
|
231
|
+
*
|
|
232
|
+
* L'identité ne change PAS : un message est déjà traité dans la bulle
|
|
233
|
+
* `RequestContext` du handshake (`WebsocketContext` lie son écouteur par
|
|
234
|
+
* `AsyncResource.bind`), où le pare-feu a posé `user` et `token`. La bulle
|
|
235
|
+
* ouverte ici HÉRITE de ce store et n'y ajoute que la charge (`body`, que
|
|
236
|
+
* `@Body` lit dans l'ALS comme pour `api.request`) — une écriture
|
|
237
|
+
* `RequestContext.set()` dans l'action reste ainsi propre à l'appel.
|
|
238
|
+
*
|
|
239
|
+
* Une action sans paramètre décoré est rendue telle quelle : rien ne change
|
|
240
|
+
* pour elle, ni comportement ni coût. Les métadonnées se lisent ICI, une fois
|
|
241
|
+
* par connexion.
|
|
242
|
+
*
|
|
243
|
+
* @param ctx - le contexte de la connexion
|
|
244
|
+
* @returns l'enveloppe passée à {@link getRealtimeActions}
|
|
245
|
+
*/
|
|
246
|
+
private actionParamsWrapper;
|
|
169
247
|
/**
|
|
170
248
|
* Handler de la méthode RPC `api.request {path}` — le **pont API souverain**
|
|
171
249
|
* (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.
|
|
@@ -16,8 +16,9 @@ import { type IRealtimeSocket, type IRealtimeChannel, type IChannelStats, type R
|
|
|
16
16
|
* l'état créé est passif, et c'est la fabrique du controller qui tranchera quand
|
|
17
17
|
* une connexion cliente le demandera. Un service n'invente donc jamais un canal
|
|
18
18
|
* pour les autres.
|
|
19
|
-
* - `request` : **non supporté** — un handle au-dessus du hub n'a PAS de
|
|
20
|
-
* (le hub est multi-clients)
|
|
19
|
+
* - `request` : **non supporté, et déprécié** — un handle au-dessus du hub n'a PAS de
|
|
20
|
+
* pair unique (le hub est multi-clients), et le contrat commun `IRealtimeSocket` ne
|
|
21
|
+
* le promet plus. Pour un RPC serveur→client 1-1, utiliser
|
|
21
22
|
* `RealtimeController.requestClient` (par connexion, L1).
|
|
22
23
|
*
|
|
23
24
|
* Lazy : aucune structure allouée tant que le service n'`on`/`subscribe`/`publish` pas.
|
|
@@ -25,7 +26,7 @@ import { type IRealtimeSocket, type IRealtimeChannel, type IChannelStats, type R
|
|
|
25
26
|
* @typeParam Emit — canaux pub/sub SORTANTS (`publish`) = ServerToClient.
|
|
26
27
|
* @typeParam Listen — canaux pub/sub RÉCEPTIONNÉS (`subscribe`/`on`) = ClientToServer.
|
|
27
28
|
*/
|
|
28
|
-
export declare class ServerRealtimeSocket<Emit extends EventsMap = DefaultEventsMap, Listen extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> implements IRealtimeSocket<Emit, Listen
|
|
29
|
+
export declare class ServerRealtimeSocket<Emit extends EventsMap = DefaultEventsMap, Listen extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> implements IRealtimeSocket<Emit, Listen> {
|
|
29
30
|
#private;
|
|
30
31
|
private readonly hub;
|
|
31
32
|
constructor(hub?: import("./RealtimeHub.js").RealtimeHub);
|
|
@@ -39,14 +40,23 @@ export declare class ServerRealtimeSocket<Emit extends EventsMap = DefaultEvents
|
|
|
39
40
|
subscribe(channel: EventNames<Listen> | (string & {})): void;
|
|
40
41
|
/** Désabonne (ref-compté) : coupe le sink du hub au DERNIER consommateur. */
|
|
41
42
|
unsubscribe(channel: EventNames<Listen> | (string & {})): void;
|
|
42
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Branche un handler de réception sur un canal. Renvoie un `dispose`.
|
|
45
|
+
*
|
|
46
|
+
* Le handler typé par `Listen` se range sans conversion sous
|
|
47
|
+
* `RealtimeHandler`, qui porte la frontière du contrat typé : il reçoit ce
|
|
48
|
+
* que le hub publie, dont `Listen` décrit la forme sans la valider.
|
|
49
|
+
*/
|
|
43
50
|
on<K extends string>(channel: K, handler: K extends EventNames<Listen> ? (payload: EventPayload<Listen, K>) => void : RealtimeHandler): () => void;
|
|
44
|
-
/** Retire un handler d'un canal. */
|
|
51
|
+
/** Retire un handler d'un canal — même frontière que {@link on}. */
|
|
45
52
|
off<K extends string>(channel: K, handler: K extends EventNames<Listen> ? (payload: EventPayload<Listen, K>) => void : RealtimeHandler): void;
|
|
46
53
|
/**
|
|
47
|
-
* **Non support
|
|
48
|
-
* (multi-clients)
|
|
49
|
-
* `
|
|
54
|
+
* **Non supporté** : rejette toujours. Un handle au-dessus du hub n'a pas de pair
|
|
55
|
+
* unique (multi-clients), et le contrat commun `IRealtimeSocket` ne promet plus
|
|
56
|
+
* `request`. Comportement inchangé pour toute la série 10.
|
|
57
|
+
*
|
|
58
|
+
* @deprecated Retrait en 11.0. Pour un RPC serveur→client 1-1, utiliser
|
|
59
|
+
* `RealtimeController.requestClient` (par connexion, L1).
|
|
50
60
|
*/
|
|
51
61
|
request<K extends string, T = unknown>(_method: K, _params?: ContractParams<Actions, K>, _timeoutMs?: number): Promise<ContractResult<Actions, K, T>>;
|
|
52
62
|
/** Vue par-canal ({@link IRealtimeChannel}) — fine liaison sur les primitives. */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trame d'acquittement d'abonnement — `realtime:subscribed`, écrite d'un bloc.
|
|
3
|
+
*
|
|
4
|
+
* Tout ce qui précède le nom du canal ne dépend que du protocole : il est
|
|
5
|
+
* calculé une fois au chargement du module. Seul le canal se sérialise par
|
|
6
|
+
* trame. On évite ainsi l'enveloppe allouée et le `JSON.stringify` générique
|
|
7
|
+
* de `JsonRpcPeer.notify` — mesuré à ~0,5 µs par acquittement, soit l'essentiel
|
|
8
|
+
* du surcoût d'un `subscribe` acquitté.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ C'est une SECONDE écriture du format de notification : son égalité avec
|
|
11
|
+
* `JsonRpcPeer.buildNotification` est verrouillée par `subscribedAck.test.ts`,
|
|
12
|
+
* sur des noms de canal hostiles (guillemets, barres obliques inverses,
|
|
13
|
+
* caractères de contrôle, Unicode).
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Sérialise la notification `realtime:subscribed { channel }`.
|
|
19
|
+
*
|
|
20
|
+
* @param channel - le canal acquitté (entrée réseau : échappé par `JSON.stringify`).
|
|
21
|
+
* @returns la trame JSON-RPC 2.0 prête à écrire sur le transport.
|
|
22
|
+
*/
|
|
23
|
+
export declare function subscribedAckFrame(channel: string): string;
|
|
@@ -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";
|
|
@@ -6,7 +6,7 @@ import type { IRealtimeConnProbe } from "../../interfaces/IRealtimeProbe.js";
|
|
|
6
6
|
* `ctx.connection` (WebsocketContext) la fournit.
|
|
7
7
|
*/
|
|
8
8
|
export interface RawWsConnection {
|
|
9
|
-
send(data: string, cb?: (err?: Error) => void): void;
|
|
9
|
+
send(data: string | ArrayBuffer | ArrayBufferView, cb?: (err?: Error) => void): void;
|
|
10
10
|
close(code?: number, reason?: string): void;
|
|
11
11
|
readonly readyState: number;
|
|
12
12
|
/**
|
|
@@ -46,6 +46,8 @@ export declare class WsConnectionTransport implements IRealtimeTransport, IRealt
|
|
|
46
46
|
private _bytesSent;
|
|
47
47
|
private _messagesSent;
|
|
48
48
|
private _dropped;
|
|
49
|
+
private _binaryDiscarded;
|
|
50
|
+
private _malformedDiscarded;
|
|
49
51
|
private readonly _max;
|
|
50
52
|
private readonly _policy;
|
|
51
53
|
private readonly _closeAfterDrops;
|
|
@@ -56,6 +58,29 @@ export declare class WsConnectionTransport implements IRealtimeTransport, IRealt
|
|
|
56
58
|
});
|
|
57
59
|
connect(): void;
|
|
58
60
|
send(raw: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Émet des octets tels quels — trame binaire RFC 6455, hors JSON-RPC (#617).
|
|
63
|
+
* Même contre-pression (`decideSend`, la règle unique) et mêmes compteurs que
|
|
64
|
+
* {@link send}. Écrite à part, et non en factorisant `send` : `send` est le
|
|
65
|
+
* chemin de CHAQUE frame JSON-RPC, et n'y paie pas un appel de plus.
|
|
66
|
+
*
|
|
67
|
+
* @param data - les octets à émettre.
|
|
68
|
+
* @returns `true` si la trame est partie, `false` si la connexion était
|
|
69
|
+
* fermée ou la contre-pression l'a jetée.
|
|
70
|
+
*/
|
|
71
|
+
sendBinary(data: Uint8Array): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Compte une trame ENTRANTE écartée et rend le cumul de sa nature — `1` dit
|
|
74
|
+
* « première », le seul instant où l'appelant journalise.
|
|
75
|
+
*
|
|
76
|
+
* @param kind - `binary` (sans lecteur) ou `malformed` (texte non JSON).
|
|
77
|
+
* @returns le cumul de cette nature, après incrément.
|
|
78
|
+
*/
|
|
79
|
+
discard(kind: "binary" | "malformed"): number;
|
|
80
|
+
/** Cumul de trames BINAIRES reçues et écartées faute de lecteur — monotone. */
|
|
81
|
+
get binaryDiscarded(): number;
|
|
82
|
+
/** Cumul de trames TEXTE reçues et écartées parce que non JSON — monotone. */
|
|
83
|
+
get malformedDiscarded(): number;
|
|
59
84
|
close(code?: number, reason?: string): void;
|
|
60
85
|
get readyState(): number;
|
|
61
86
|
/** Octets en file d'envoi non drainés (`ws.bufferedAmount`) — risque #1. `0` si mock. */
|
|
@@ -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";
|
|
@@ -130,7 +130,10 @@ export interface IRealtimeHarness<C extends RealtimeController> {
|
|
|
130
130
|
* échec) : lire `closes` pour en connaître la cause.
|
|
131
131
|
*/
|
|
132
132
|
connect(): Promise<IRealtimeFrame>;
|
|
133
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Demande un canal, comme le ferait un client, et attend la réponse du
|
|
135
|
+
* serveur (acquittement `realtime:subscribed` ou refus `realtime:denied`).
|
|
136
|
+
*/
|
|
134
137
|
subscribe(channel: string): Promise<void>;
|
|
135
138
|
/** Rend un canal — le provider est disposé au dernier abonné. */
|
|
136
139
|
unsubscribe(channel: string): Promise<void>;
|
|
@@ -149,6 +152,8 @@ export interface IRealtimeHarness<C extends RealtimeController> {
|
|
|
149
152
|
messages(channel: string): readonly unknown[];
|
|
150
153
|
/** Les refus d'abonnement notifiés (`realtime:denied`). */
|
|
151
154
|
denials(): readonly IHarnessDenied[];
|
|
155
|
+
/** Les canaux dont l'abonnement a été acquitté (`realtime:subscribed`), dans l'ordre. */
|
|
156
|
+
subscriptions(): readonly string[];
|
|
152
157
|
/** Ferme la socket côté client : la base dispose alors tous ses providers. */
|
|
153
158
|
close(): void;
|
|
154
159
|
/** `close()` puis remise à zéro du hub. À appeler en fin de cas. */
|