@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.
Files changed (81) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +207 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +218 -0
  6. package/dist/nodefony/config/config.js +63 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +60 -0
  8. package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
  9. package/dist/nodefony/interfaces/IBackplane.js +1 -0
  10. package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
  11. package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
  12. package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
  13. package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
  14. package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
  15. package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
  16. package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
  17. package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
  18. package/dist/nodefony/interfaces/index.js +1 -0
  19. package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
  20. package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
  21. package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
  22. package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
  23. package/dist/nodefony/src/backplane/envelope.js +109 -0
  24. package/dist/nodefony/src/backplane/originId.js +35 -0
  25. package/dist/nodefony/src/backplane/publishQueue.js +96 -0
  26. package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
  27. package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
  28. package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
  29. package/dist/nodefony/src/server/RealtimeController.js +601 -0
  30. package/dist/nodefony/src/server/RealtimeHub.js +926 -0
  31. package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
  32. package/dist/nodefony/src/server/deniedDetail.js +29 -0
  33. package/dist/nodefony/src/server/syslogUplink.js +105 -0
  34. package/dist/nodefony/src/server/welcomeEnv.js +25 -0
  35. package/dist/nodefony/src/service/RealtimeService.js +240 -0
  36. package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
  37. package/dist/nodefony/testing/index.js +163 -0
  38. package/dist/types/index.d.ts +99 -0
  39. package/dist/types/nodefony/config/config.d.ts +43 -0
  40. package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
  41. package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
  42. package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
  43. package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
  44. package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
  45. package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
  46. package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
  47. package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
  48. package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
  49. package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
  50. package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
  51. package/dist/types/nodefony/interfaces/index.d.ts +18 -0
  52. package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
  53. package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
  54. package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
  55. package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
  56. package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
  57. package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
  58. package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
  59. package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
  60. package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
  61. package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
  62. package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
  63. package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
  64. package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
  65. package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
  66. package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
  67. package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
  68. package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
  69. package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
  70. package/dist/types/nodefony/testing/index.d.ts +169 -0
  71. package/docs/actions.md +573 -0
  72. package/docs/architecture.md +889 -0
  73. package/docs/configuration.md +803 -0
  74. package/docs/cookbook-chat.md +1619 -0
  75. package/docs/couverture-bancs.md +139 -0
  76. package/docs/index.md +372 -0
  77. package/docs/observabilite.md +592 -0
  78. package/docs/protocole.md +518 -0
  79. package/docs/securite.md +845 -0
  80. package/docs/vocabulaire.md +845 -0
  81. 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>;