@nodefony/realtime 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +207 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +218 -0
- package/dist/nodefony/config/config.js +63 -0
- package/dist/nodefony/config/defineModuleConfig.js +60 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
- package/dist/nodefony/interfaces/IBackplane.js +1 -0
- package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
- package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
- package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
- package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
- package/dist/nodefony/src/backplane/envelope.js +109 -0
- package/dist/nodefony/src/backplane/originId.js +35 -0
- package/dist/nodefony/src/backplane/publishQueue.js +96 -0
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
- package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
- package/dist/nodefony/src/server/RealtimeController.js +601 -0
- package/dist/nodefony/src/server/RealtimeHub.js +926 -0
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
- package/dist/nodefony/src/server/deniedDetail.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +105 -0
- package/dist/nodefony/src/server/welcomeEnv.js +25 -0
- package/dist/nodefony/src/service/RealtimeService.js +240 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
- package/dist/nodefony/testing/index.js +163 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/nodefony/config/config.d.ts +43 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
- package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
- package/dist/types/nodefony/interfaces/index.d.ts +18 -0
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
- package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
- package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
- package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
- package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
- package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
- package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +169 -0
- package/docs/actions.md +573 -0
- package/docs/architecture.md +889 -0
- package/docs/configuration.md +803 -0
- package/docs/cookbook-chat.md +1619 -0
- package/docs/couverture-bancs.md +139 -0
- package/docs/index.md +372 -0
- package/docs/observabilite.md +592 -0
- package/docs/protocole.md +518 -0
- package/docs/securite.md +845 -0
- package/docs/vocabulaire.md +845 -0
- package/package.json +83 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le mode d'exécution annoncé — ou non — dans `realtime:welcome`.
|
|
3
|
+
*
|
|
4
|
+
* Le client ne dispose sinon que d'`import.meta.env.DEV`, qui dit le mode du
|
|
5
|
+
* BUNDLE : une application bâtie pour la production mais servie par un serveur
|
|
6
|
+
* de développement (banc, cluster local, image essayée sur le poste) restait
|
|
7
|
+
* muette dans la console alors qu'on avait tout intérêt à la faire parler.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ **En production, le champ est ABSENT** — et c'est la raison d'être de cette
|
|
10
|
+
* fonction, isolée pour être éprouvée dans les deux sens. Ce n'est pas un
|
|
11
|
+
* réglage que le client lit, c'est une permission de parler dans la console de
|
|
12
|
+
* quelqu'un : on ne l'accorde pas par défaut, et une absence vaut production,
|
|
13
|
+
* jamais l'inverse.
|
|
14
|
+
*
|
|
15
|
+
* @param env - le mode du kernel serveur (`kernel.environment`), s'il est connu.
|
|
16
|
+
* @returns le fragment à étaler dans le welcome : `{ env }` hors production,
|
|
17
|
+
* sinon un objet vide.
|
|
18
|
+
*/
|
|
19
|
+
export declare function welcomeEnv(env: string | undefined | null): {
|
|
20
|
+
env?: string;
|
|
21
|
+
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Service, Module, type JsonRpcPeer } from "nodefony";
|
|
2
|
+
import { type RealtimeHub, type ChannelFactory, type ChannelSink, type FrameAuthorizer } from "../server/RealtimeHub.js";
|
|
3
|
+
import type { IBackplane } from "../../interfaces/IBackplane.js";
|
|
4
|
+
import type { IRealtimeProbe } from "../../interfaces/IRealtimeProbe.js";
|
|
5
|
+
import type { IRealtimeConfig } from "../../config/defineModuleConfig.js";
|
|
6
|
+
import type { IRealtimeAuthenticator } from "../../interfaces/IRealtimeAuthenticator.js";
|
|
7
|
+
import type { IRealtimeAuthenticatorMatcher } from "../../interfaces/IRealtimeAuthenticatorMatcher.js";
|
|
8
|
+
import type { IRealtimeToken } from "../../interfaces/IRealtimeToken.js";
|
|
9
|
+
import type { IChannelPolicy } from "../../interfaces/IChannelPolicy.js";
|
|
10
|
+
/**
|
|
11
|
+
* Façade DI publique de la **socket Nodefony** côté serveur.
|
|
12
|
+
*
|
|
13
|
+
* Wrapper mince autour du singleton {@link RealtimeHub} : expose
|
|
14
|
+
* `publish`/`subscribe`/`probe` à travers le container DI, applique au boot le
|
|
15
|
+
* backplane custom éventuel (config `backplane.instance` OU service DI nommé
|
|
16
|
+
* `realtimeBackplane`), expose la config gelée pour Studio / introspection.
|
|
17
|
+
*
|
|
18
|
+
* Ce service ne **remplace pas** le hub singleton (consommé par les
|
|
19
|
+
* controllers, l'admin API et les WS handlers via `getRealtimeHub()`) ; il en
|
|
20
|
+
* est l'API stable consommable côté userland. La Module class garde la main
|
|
21
|
+
* sur le wiring `ClusterBackplane` (lié au lifecycle `onCluster` kernel,
|
|
22
|
+
* pré-`onPreBoot`) — orthogonal au backplane custom userland.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* class MyService extends Service {
|
|
27
|
+
* constructor(public module: Module) {
|
|
28
|
+
* super(...);
|
|
29
|
+
* this.realtime = this.get<RealtimeService>("realtimeService");
|
|
30
|
+
* }
|
|
31
|
+
* notify(payload: unknown) {
|
|
32
|
+
* this.realtime?.publish("my-app:event", payload);
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
declare class RealtimeService extends Service {
|
|
38
|
+
#private;
|
|
39
|
+
module: Module;
|
|
40
|
+
constructor(module: Module);
|
|
41
|
+
/**
|
|
42
|
+
* Initialise le service au moment où le Module l'enregistre via `addService`
|
|
43
|
+
* (phase `onPreBoot`). Pose la config validée + applique l'éventuel backplane
|
|
44
|
+
* custom userland. Idempotent.
|
|
45
|
+
*
|
|
46
|
+
* Source du backplane custom (priorité décroissante) :
|
|
47
|
+
* 1. `realtimeConfig.backplane.instance` posée par la Module class via le
|
|
48
|
+
* container (mode "config typée" : `defineRealtimeConfig({}, { backplane })`)
|
|
49
|
+
* 2. Service DI nommé `realtimeBackplane` (mode "userland enregistre une
|
|
50
|
+
* instance dans son module via container.set")
|
|
51
|
+
*
|
|
52
|
+
* Si aucun n'est fourni → le hub reste avec son backplane par défaut (null =
|
|
53
|
+
* Loopback implicite, ou ClusterBackplane si le worker IPC a déjà été câblé
|
|
54
|
+
* par la Module class). Aucun warn — c'est le cas commun.
|
|
55
|
+
*/
|
|
56
|
+
init(_module: Module): Promise<this>;
|
|
57
|
+
/** Configuration realtime gelée (lue par Studio / introspection). */
|
|
58
|
+
getConfig(): IRealtimeConfig;
|
|
59
|
+
/** Hub singleton (broker fan-out canaux PARTAGÉS du process courant). */
|
|
60
|
+
getHub(): RealtimeHub;
|
|
61
|
+
/** Backplane actuellement branché sur le hub (ou null = Loopback implicite). */
|
|
62
|
+
getBackplane(): IBackplane | null;
|
|
63
|
+
/**
|
|
64
|
+
* Publie une charge sur un canal — fan-out local + propagation backplane si
|
|
65
|
+
* canal broadcast. Délégué à `RealtimeHub.publish`.
|
|
66
|
+
*/
|
|
67
|
+
publish(channel: string, payload: unknown): void;
|
|
68
|
+
/**
|
|
69
|
+
* Abonne un sink à un canal partagé — factory invoquée au 1ᵉʳ abonné, dispose
|
|
70
|
+
* appelé au dernier désabonné. Délégué à `RealtimeHub.subscribe`.
|
|
71
|
+
*
|
|
72
|
+
* @returns `true` si abonné, `false` si la factory a refusé (canal inconnu).
|
|
73
|
+
*/
|
|
74
|
+
subscribe(channel: string, sink: ChannelSink, factory: ChannelFactory): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Désabonne un sink — appelle `dispose` du provider partagé au dernier abonné.
|
|
77
|
+
* Délégué à `RealtimeHub.unsubscribe`. No-op si non abonné.
|
|
78
|
+
*/
|
|
79
|
+
unsubscribe(channel: string, sink: ChannelSink): void;
|
|
80
|
+
/**
|
|
81
|
+
* Enregistre la factory d'un **canal système** (plateforme) — délégué à
|
|
82
|
+
* {@link RealtimeHub.registerSystemChannel}. Un module bas niveau
|
|
83
|
+
* (`@nodefony/security` → `nodefony:audit`) déclare au boot comment produire le
|
|
84
|
+
* provider, sans qu'aucun `RealtimeController` ne le connaisse ; tout endpoint
|
|
85
|
+
* le sert alors (lazy : créé au 1ᵉʳ abonné, disposé au dernier).
|
|
86
|
+
*/
|
|
87
|
+
registerSystemChannel(channel: string, factory: ChannelFactory): void;
|
|
88
|
+
/**
|
|
89
|
+
* Namespaces de canaux **réservés à la plateforme** — la liste que le hub ferme
|
|
90
|
+
* lui-même en l'absence de module de sécurité.
|
|
91
|
+
*
|
|
92
|
+
* Exposée pour qu'un module de sécurité y attache ses politiques **sans
|
|
93
|
+
* redéclarer la liste** : deux inventaires auraient divergé au premier namespace
|
|
94
|
+
* ajouté, et la divergence serait passée inaperçue (un namespace neuf resterait
|
|
95
|
+
* simplement sans politique). Ici, le hub possède la liste, la sécurité possède
|
|
96
|
+
* les rôles.
|
|
97
|
+
*/
|
|
98
|
+
reservedSystemPrefixes(): readonly string[];
|
|
99
|
+
/** Snapshot d'observabilité du hub local (consommé par `/nodefony/realtime/api/health`). */
|
|
100
|
+
probe(): IRealtimeProbe;
|
|
101
|
+
/**
|
|
102
|
+
* Marque un préfixe de canal comme **broadcast** (cross-process via le
|
|
103
|
+
* backplane). Sans appel, tout canal reste instance-local. Délégué à
|
|
104
|
+
* `RealtimeHub.markBroadcastChannel`.
|
|
105
|
+
*/
|
|
106
|
+
markBroadcastChannel(prefix: string): void;
|
|
107
|
+
/**
|
|
108
|
+
* **Seam #2/#3 (P13 → P6)** — enregistre un authenticator pour les
|
|
109
|
+
* handshakes WS dont l'URL matche `matcher`. Ordre d'enregistrement
|
|
110
|
+
* préservé (1ʳᵉ match capture). Cold path (boot via P6/userland).
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* // À brancher par @nodefony/security (P6) au boot, depuis les areas
|
|
115
|
+
* // de `defineSecurityConfig()` filtrées sur `realtime: true`.
|
|
116
|
+
* realtimeService.useAuthenticator(
|
|
117
|
+
* { pattern: "/admin/", host: "admin.example.com" },
|
|
118
|
+
* jwtRealtimeAuthenticator,
|
|
119
|
+
* );
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
useAuthenticator(matcher: IRealtimeAuthenticatorMatcher, authenticator: IRealtimeAuthenticator): void;
|
|
123
|
+
/**
|
|
124
|
+
* **Seam #1 (P13 → P6)** — pose le verrou de frame (hot-path `beforeDispatch`).
|
|
125
|
+
* `null` retire la politique. À brancher 1× au boot par `@nodefony/security`
|
|
126
|
+
* depuis les zones `realtime: true`. L'authorizer lit le token déjà résolu au
|
|
127
|
+
* handshake (0 lecture base par frame). Délégué à `RealtimeHub.setFrameAuthorizer`.
|
|
128
|
+
*
|
|
129
|
+
* `options.silentProbe` porte la MÊME décision sans rapporter de refus : elle
|
|
130
|
+
* répond « ce visiteur pourrait-il s'abonner ici ? » pour l'annonce des canaux
|
|
131
|
+
* du `realtime:welcome`, qui interroge N canaux par connexion sans que
|
|
132
|
+
* personne n'ait rien demandé. Elle se pose par CE MÊME appel pour qu'aucun
|
|
133
|
+
* câblage ne puisse poser l'un sans l'autre.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* // @nodefony/security au boot :
|
|
138
|
+
* realtimeService.setFrameAuthorizer((frame, token) => {
|
|
139
|
+
* // api.request {path} ≤ GET {path} : zone protégée → exige authentifié
|
|
140
|
+
* return isFrameAllowed(frame, token);
|
|
141
|
+
* });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
setFrameAuthorizer(authorizer: FrameAuthorizer | null, options?: {
|
|
145
|
+
readonly silentProbe?: FrameAuthorizer | null;
|
|
146
|
+
}): void;
|
|
147
|
+
/**
|
|
148
|
+
* **Seam #1b** — politique d'autorisation déclarée pour un canal/méthode
|
|
149
|
+
* (`@RealtimeChannel`/`@RealtimeInbound` avec opts), agrégée par le hub au
|
|
150
|
+
* handshake. Lu par `@nodefony/security` au moment de la frame `subscribe`/
|
|
151
|
+
* inbound pour appliquer rôles/scopes. `null` = aucune politique métier (le
|
|
152
|
+
* canal est alors soumis à la seule politique plateforme de security).
|
|
153
|
+
*
|
|
154
|
+
* @param channel - nom EXACT du canal ou de la méthode inbound.
|
|
155
|
+
* @returns la politique déclarée, ou `null`.
|
|
156
|
+
*/
|
|
157
|
+
resolveChannelPolicy(channel: string): IChannelPolicy | null;
|
|
158
|
+
/**
|
|
159
|
+
* Renvoie le token associé à un peer (posé au handshake par le hub). Jamais
|
|
160
|
+
* `null` (Zero Trust : `ANONYMOUS_REALTIME_TOKEN` si aucun authenticator n'a
|
|
161
|
+
* capturé la connexion). Lookup O(1) via WeakMap interne. Branchement P6 :
|
|
162
|
+
* voters / `@IsGranted` realtime lisent l'identité par cette méthode.
|
|
163
|
+
*/
|
|
164
|
+
getTokenForPeer(peer: JsonRpcPeer): IRealtimeToken;
|
|
165
|
+
}
|
|
166
|
+
export default RealtimeService;
|
|
167
|
+
export { RealtimeService };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type IRealtimeTransport } from "nodefony";
|
|
2
|
+
import { type WsBackpressurePolicy } from "@nodefony/http";
|
|
3
|
+
import type { IRealtimeConnProbe } from "../../interfaces/IRealtimeProbe.js";
|
|
4
|
+
/**
|
|
5
|
+
* Connexion ws brute, typée structurellement (évite d'importer le package `ws`).
|
|
6
|
+
* `ctx.connection` (WebsocketContext) la fournit.
|
|
7
|
+
*/
|
|
8
|
+
export interface RawWsConnection {
|
|
9
|
+
send(data: string, cb?: (err?: Error) => void): void;
|
|
10
|
+
close(code?: number, reason?: string): void;
|
|
11
|
+
readonly readyState: number;
|
|
12
|
+
/**
|
|
13
|
+
* Octets en file d'envoi non drainés (`ws.bufferedAmount`). Optionnel : absent
|
|
14
|
+
* des connexions mockées en test → la sonde lit `0`. Sur une vraie socket `ws`,
|
|
15
|
+
* c'est le signal du blocker mémoire #1 (slow-consumer).
|
|
16
|
+
*/
|
|
17
|
+
readonly bufferedAmount?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Contre-pression WS — **la règle vit dans `@nodefony/http`**, ce transport ne
|
|
21
|
+
* fait que l'appliquer (`decideSend`). Deux implémentations de la même
|
|
22
|
+
* protection avaient déjà divergé en silence : 4 MiB côté `http`, 1 MiB ici.
|
|
23
|
+
*
|
|
24
|
+
* Réglage = config du serveur WebSocket (`websocket.maxBackpressure`,
|
|
25
|
+
* `.backpressurePolicy`, `.backpressureCloseAfterDrops`) — une seule source pour
|
|
26
|
+
* `send`/`broadcast` HTTP **et** pour le realtime.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ NE PAS confondre avec `slowConsumer.bytes` (config realtime) : celle-ci ne
|
|
29
|
+
* règle QUE le seuil de COMPTAGE de la sonde, jamais l'action.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* WsConnectionTransport — transport {@link IRealtimeTransport} CÔTÉ SERVEUR :
|
|
33
|
+
* wrap d'une connexion `ws` (1 par client connecté). Pendant serveur du
|
|
34
|
+
* `BrowserWsTransport` → la connexion serveur compose le MÊME `JsonRpcPeer` que
|
|
35
|
+
* le client (symétrie isomorphe).
|
|
36
|
+
*
|
|
37
|
+
* Particularité serveur : l'inbound n'est PAS piloté par un event socket mais par
|
|
38
|
+
* le **pipeline framework** (la route WS appelle `feed(raw)` à chaque message) ;
|
|
39
|
+
* la fermeture est signalée par `fireClose()` (hook `onFinish` du Context). Le
|
|
40
|
+
* `connect`/`onOpen` sont des no-op (la connexion est déjà ouverte au handshake).
|
|
41
|
+
*/
|
|
42
|
+
export declare class WsConnectionTransport implements IRealtimeTransport, IRealtimeConnProbe {
|
|
43
|
+
private readonly conn;
|
|
44
|
+
private _onMessage;
|
|
45
|
+
private _onClose;
|
|
46
|
+
private _bytesSent;
|
|
47
|
+
private _messagesSent;
|
|
48
|
+
private _dropped;
|
|
49
|
+
private readonly _max;
|
|
50
|
+
private readonly _policy;
|
|
51
|
+
private readonly _closeAfterDrops;
|
|
52
|
+
constructor(conn: RawWsConnection, limits?: {
|
|
53
|
+
max?: number;
|
|
54
|
+
policy?: WsBackpressurePolicy;
|
|
55
|
+
closeAfterDrops?: number;
|
|
56
|
+
});
|
|
57
|
+
connect(): void;
|
|
58
|
+
send(raw: string): void;
|
|
59
|
+
close(code?: number, reason?: string): void;
|
|
60
|
+
get readyState(): number;
|
|
61
|
+
/** Octets en file d'envoi non drainés (`ws.bufferedAmount`) — risque #1. `0` si mock. */
|
|
62
|
+
get bufferedAmount(): number;
|
|
63
|
+
/** Cumul d'octets envoyés sur cette connexion (monotone). */
|
|
64
|
+
get bytesSent(): number;
|
|
65
|
+
/** Cumul de frames envoyées sur cette connexion (monotone). */
|
|
66
|
+
get messagesSent(): number;
|
|
67
|
+
/** Cumul de frames jetées par back-pressure (drop + close slow-consumer) — monotone. */
|
|
68
|
+
get dropped(): number;
|
|
69
|
+
onOpen(): void;
|
|
70
|
+
onMessage(cb: (raw: string) => void): void;
|
|
71
|
+
onClose(cb: (code: number, reason: string) => void): void;
|
|
72
|
+
onError(): void;
|
|
73
|
+
/** Pump entrant : le pipeline framework pousse ici chaque message reçu. */
|
|
74
|
+
feed(raw: string): void;
|
|
75
|
+
/** Fermeture : appelée par le owner sur `onFinish` (close WS). */
|
|
76
|
+
fireClose(code?: number, reason?: string): void;
|
|
77
|
+
}
|
|
78
|
+
export default WsConnectionTransport;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nodefony/realtime/testing` — **harnais de test d'un controller temps réel**.
|
|
3
|
+
*
|
|
4
|
+
* Écrire un test de socket demandait jusqu'ici une centaine de lignes de
|
|
5
|
+
* plomberie FRAMEWORK avant la première assertion : un faux `ContextType` qui ne
|
|
6
|
+
* fournit que ce que la base touche, un pont vers `handleRealtime` (qui est
|
|
7
|
+
* `protected`), la remise à zéro du hub — singleton par process — entre deux
|
|
8
|
+
* cas, et la pose d'une identité au handshake. Cette plomberie se périme au
|
|
9
|
+
* premier changement de signature de la base ; recopiée dans chaque application,
|
|
10
|
+
* elle y aurait vieilli sans que personne ne le sache. Elle vit donc ICI, avec
|
|
11
|
+
* le code dont elle dépend.
|
|
12
|
+
*
|
|
13
|
+
* Le harnais pilote le **côté serveur** : il injecte des frames JSON-RPC 2.0
|
|
14
|
+
* telles qu'un client les enverrait, et lit celles qui sortent. Il n'embarque
|
|
15
|
+
* PAS `RealtimeClient` — le client est un artefact navigateur du cœur, et le
|
|
16
|
+
* tirer ici le ferait entrer dans le paquet serveur publié. Ce qu'on veut
|
|
17
|
+
* prouver d'un controller (ses canaux, ses actions, son plein-duplex, son
|
|
18
|
+
* nettoyage) s'observe entièrement aux frames.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { createRealtimeHarness } from "@nodefony/realtime/testing";
|
|
22
|
+
*
|
|
23
|
+
* const h = createRealtimeHarness((ctx) => new ChatController(ctx));
|
|
24
|
+
* await h.connect();
|
|
25
|
+
* await h.subscribe("chat:ticker");
|
|
26
|
+
* expect(h.messages("chat:ticker")).to.have.length.greaterThan(0);
|
|
27
|
+
* h.dispose();
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @packageDocumentation
|
|
31
|
+
*/
|
|
32
|
+
import type { IRealtimeDenied } from "nodefony";
|
|
33
|
+
import type { ContextType } from "@nodefony/http";
|
|
34
|
+
import type { RealtimeController } from "../src/server/RealtimeController.js";
|
|
35
|
+
import { type RealtimeHub, type FrameAuthorizer } from "../src/server/RealtimeHub.js";
|
|
36
|
+
import type { IRealtimeToken } from "../interfaces/IRealtimeToken.js";
|
|
37
|
+
/**
|
|
38
|
+
* Frame JSON-RPC 2.0 telle qu'elle circule sur la socket, dans les deux sens.
|
|
39
|
+
*
|
|
40
|
+
* Volontairement permissive : un test lit `method`, `params`, `result` ou
|
|
41
|
+
* `error` selon ce qu'il éprouve, et le harnais ne présume pas de la forme du
|
|
42
|
+
* métier transporté.
|
|
43
|
+
*/
|
|
44
|
+
export interface IRealtimeFrame {
|
|
45
|
+
readonly jsonrpc?: string;
|
|
46
|
+
readonly id?: number | string;
|
|
47
|
+
readonly method?: string;
|
|
48
|
+
readonly params?: unknown;
|
|
49
|
+
readonly result?: unknown;
|
|
50
|
+
readonly error?: {
|
|
51
|
+
code: number;
|
|
52
|
+
message: string;
|
|
53
|
+
data?: unknown;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Fermeture demandée par le serveur (code RFC 6455 + raison). */
|
|
57
|
+
export interface IHarnessClose {
|
|
58
|
+
readonly code?: number;
|
|
59
|
+
readonly reason?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Refus d'abonnement notifié au client (`realtime:denied`) — le contrat de
|
|
63
|
+
* protocole lui-même, en lecture seule. Le recopier ici le ferait diverger : un
|
|
64
|
+
* motif ajouté au protocole doit apparaître dans le banc sans qu'on y pense.
|
|
65
|
+
*/
|
|
66
|
+
export type IHarnessDenied = Readonly<IRealtimeDenied>;
|
|
67
|
+
/** Écart au décor par défaut du harnais. */
|
|
68
|
+
export interface IRealtimeHarnessOptions {
|
|
69
|
+
/**
|
|
70
|
+
* URL de la connexion, telle que la verraient les matchers d'authenticator
|
|
71
|
+
* (seam #3). Défaut `/realtime`.
|
|
72
|
+
*/
|
|
73
|
+
readonly url?: string;
|
|
74
|
+
/** `Origin` du handshake — ce que juge la politique CSRF (seam #4). Défaut `""`. */
|
|
75
|
+
readonly origin?: string;
|
|
76
|
+
/** En-têtes de la requête d'upgrade. */
|
|
77
|
+
readonly headers?: Record<string, string | undefined>;
|
|
78
|
+
/** Cookies de la requête d'upgrade, à la forme du contexte HTTP. */
|
|
79
|
+
readonly cookies?: Record<string, {
|
|
80
|
+
value: string;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Identité posée au handshake (seam #2). Sans elle, la base pose le token
|
|
84
|
+
* anonyme — c'est le repli Zero Trust, et c'est ce qu'il faut pour éprouver
|
|
85
|
+
* qu'un canal refuse un visiteur non identifié.
|
|
86
|
+
*/
|
|
87
|
+
readonly identity?: IRealtimeToken;
|
|
88
|
+
/**
|
|
89
|
+
* Verrou de frame (seam #1). Neutre par construction : `@nodefony/realtime`
|
|
90
|
+
* ne connaît pas `@nodefony/security`. Une application qui charge la sécurité
|
|
91
|
+
* passe ici son `buildFrameAuthorizer(...)` pour éprouver une politique de
|
|
92
|
+
* canal ou une action gardée par rôle — sans lui, une `policy` déclarée n'est
|
|
93
|
+
* appliquée par personne, exactement comme au runtime.
|
|
94
|
+
*/
|
|
95
|
+
readonly frameAuthorizer?: FrameAuthorizer;
|
|
96
|
+
/**
|
|
97
|
+
* Remet le hub à zéro à la création (défaut `true`). Le hub est un singleton
|
|
98
|
+
* par process : sans ce reset, un canal ouvert par un cas précédent fausse le
|
|
99
|
+
* suivant. Passer `false` pour monter deux connexions sur le MÊME hub — c'est
|
|
100
|
+
* ainsi qu'on éprouve un fan-out.
|
|
101
|
+
*/
|
|
102
|
+
readonly resetHub?: boolean;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Vue de test d'un controller temps réel connecté : ce qu'il a reçu, ce qu'il a
|
|
106
|
+
* émis, et de quoi le piloter frame par frame.
|
|
107
|
+
*
|
|
108
|
+
* @typeParam C - le type du controller éprouvé, rendu tel quel par `controller`.
|
|
109
|
+
*/
|
|
110
|
+
export interface IRealtimeHarness<C extends RealtimeController> {
|
|
111
|
+
/** L'instance éprouvée, du type rendu par la fabrique. */
|
|
112
|
+
readonly controller: C;
|
|
113
|
+
/** Le hub du process — pour poser une politique de canal ou lire une sonde. */
|
|
114
|
+
readonly hub: RealtimeHub;
|
|
115
|
+
/** Toutes les frames sorties, dans l'ordre d'émission. */
|
|
116
|
+
readonly received: readonly IRealtimeFrame[];
|
|
117
|
+
/** Les fermetures demandées par le serveur (un refus d'Origin y laisse 4003). */
|
|
118
|
+
readonly closes: readonly IHarnessClose[];
|
|
119
|
+
/**
|
|
120
|
+
* Les avertissements de plateforme émis DEPUIS le handshake — dont celui qui
|
|
121
|
+
* dit qu'un canal servi dynamiquement ne porte aucune politique.
|
|
122
|
+
*/
|
|
123
|
+
readonly notices: readonly string[];
|
|
124
|
+
/**
|
|
125
|
+
* Ouvre la socket (message `null` → `realtime:welcome`).
|
|
126
|
+
*
|
|
127
|
+
* @returns la frame de bienvenue — elle annonce les canaux, les méthodes et
|
|
128
|
+
* l'identité retenue.
|
|
129
|
+
* @throws Si aucun welcome n'arrive (Origin refusée, authentification en
|
|
130
|
+
* échec) : lire `closes` pour en connaître la cause.
|
|
131
|
+
*/
|
|
132
|
+
connect(): Promise<IRealtimeFrame>;
|
|
133
|
+
/** Demande un canal, comme le ferait un client. */
|
|
134
|
+
subscribe(channel: string): Promise<void>;
|
|
135
|
+
/** Rend un canal — le provider est disposé au dernier abonné. */
|
|
136
|
+
unsubscribe(channel: string): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Appelle une action (requête → réponse), appariée par identifiant.
|
|
139
|
+
*
|
|
140
|
+
* @returns le `result` de l'action.
|
|
141
|
+
* @throws L'erreur JSON-RPC rendue, ou un délai dépassé si rien ne répond.
|
|
142
|
+
*/
|
|
143
|
+
call<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
144
|
+
/** Envoie une notification cliente (plein-duplex entrant). */
|
|
145
|
+
notify(method: string, params?: unknown): Promise<void>;
|
|
146
|
+
/** Injecte une frame brute — pour éprouver un protocole malformé. */
|
|
147
|
+
send(frame: Record<string, unknown>): Promise<void>;
|
|
148
|
+
/** Les charges utiles reçues sur un canal, dans l'ordre. */
|
|
149
|
+
messages(channel: string): readonly unknown[];
|
|
150
|
+
/** Les refus d'abonnement notifiés (`realtime:denied`). */
|
|
151
|
+
denials(): readonly IHarnessDenied[];
|
|
152
|
+
/** Ferme la socket côté client : la base dispose alors tous ses providers. */
|
|
153
|
+
close(): void;
|
|
154
|
+
/** `close()` puis remise à zéro du hub. À appeler en fin de cas. */
|
|
155
|
+
dispose(): void;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Monte un controller temps réel sur une fausse connexion et rend de quoi le
|
|
159
|
+
* piloter.
|
|
160
|
+
*
|
|
161
|
+
* La fabrique reçoit le contexte à passer au constructeur : c'est la seule
|
|
162
|
+
* chose que le harnais ne peut pas deviner, puisque le nom du service et les
|
|
163
|
+
* dépendances injectées appartiennent au controller.
|
|
164
|
+
*
|
|
165
|
+
* @param create - construit le controller à partir du contexte fourni.
|
|
166
|
+
* @param options - écarts au décor par défaut (identité, Origin, verrou…).
|
|
167
|
+
* @returns le harnais, prêt pour `connect()`.
|
|
168
|
+
*/
|
|
169
|
+
export declare function createRealtimeHarness<C extends RealtimeController>(create: (context: ContextType) => C, options?: IRealtimeHarnessOptions): IRealtimeHarness<C>;
|