@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,165 @@
|
|
|
1
|
+
import { type RpcActionHandler, type EventsMap, type ActionsMap, type DefaultEventsMap, type DefaultActionsMap, type EventNames, type EventPayload, type ActionNames, type ActionParams, type ActionResult } from "nodefony";
|
|
2
|
+
import { Controller } from "@nodefony/framework";
|
|
3
|
+
import type { IRealtimeController, RealtimePublish, RealtimeInboundHandler } from "../../interfaces/IRealtimeController.js";
|
|
4
|
+
/**
|
|
5
|
+
* RealtimeController — base d'un endpoint WebSocket temps réel SERVEUR (JSON-RPC 2.0).
|
|
6
|
+
*
|
|
7
|
+
* Factorise TOUT le protocole (handshake/welcome, discrimination request/notification/
|
|
8
|
+
* response via {@link JsonRpcPeer}, actions `result`/`error`, pub/sub par canal, cleanup)
|
|
9
|
+
* — écrit UNE fois, partagé par tous les modules. Un contrôleur concret ne déclare que
|
|
10
|
+
* son métier : {@link createRealtimeChannel} (providers de canaux) + {@link realtimeActions}.
|
|
11
|
+
*
|
|
12
|
+
* Chaque connexion compose un `JsonRpcPeer` (le MÊME que `RealtimeClient` côté navigateur
|
|
13
|
+
* — symétrie isomorphe) branché sur un {@link WsConnectionTransport}.
|
|
14
|
+
*
|
|
15
|
+
* Usage : le sous-classe garde sa route WS et délègue —
|
|
16
|
+
* ```ts
|
|
17
|
+
* @route("ws", { path: "/realtime", requirements: { methods: ["WEBSOCKET"] } })
|
|
18
|
+
* async realtime(message: string | Buffer | null) { this.handleRealtime(message); }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Perf : les canaux sont **partagés** via {@link RealtimeHub} (1 provider/canal/pod, pas
|
|
22
|
+
* 1 par connexion) ; chaque connexion n'ajoute/retire qu'un *sink* (fan-out). Provider
|
|
23
|
+
* créé au 1ᵉʳ abonné, `dispose()` au dernier (`unsubscribe` ou `onFinish`). Le dispatch
|
|
24
|
+
* d'action n'est payé que sur une requête `id`. C'est aussi le seam du backplane Redis.
|
|
25
|
+
*/
|
|
26
|
+
export declare abstract class RealtimeController<Emit extends EventsMap = DefaultEventsMap, Actions extends ActionsMap = DefaultActionsMap> extends Controller implements IRealtimeController {
|
|
27
|
+
/**
|
|
28
|
+
* Map des factories de canaux EXACT, lue depuis les décorateurs `@RealtimeChannel`
|
|
29
|
+
* au handshake (cold-path) et mémoïsée par instance. `null` = pas de décorateur
|
|
30
|
+
* sur cette classe → la base court-circuite la lecture metadata par frame.
|
|
31
|
+
*/
|
|
32
|
+
private _decoratedChannels;
|
|
33
|
+
/**
|
|
34
|
+
* Crée le provider d'un canal (listener/ticker → `publish`) et renvoie son `dispose`.
|
|
35
|
+
* `null` si le canal est inconnu — la base ne souscrit pas.
|
|
36
|
+
*
|
|
37
|
+
* **Deux façons de déclarer un canal** (coexistent sans casse) :
|
|
38
|
+
* 1. **Décorateur `@RealtimeChannel(name)`** sur une méthode (match EXACT, déclaratif).
|
|
39
|
+
* 2. **Override de cette méthode** (pattern/regex, suffixe `:<ms>`, drill `@<pid>`).
|
|
40
|
+
*
|
|
41
|
+
* La base CONSULTE D'ABORD les décorateurs ; si aucun match, elle appelle
|
|
42
|
+
* cette méthode (fallback). Défaut : `null`. Un controller qui n'utilise QUE
|
|
43
|
+
* des décorateurs peut donc se passer d'override.
|
|
44
|
+
*/
|
|
45
|
+
createRealtimeChannel(_channel: string, _publish: RealtimePublish): (() => void) | null;
|
|
46
|
+
/** Actions RPC exposées (requête→réponse). À surcharger ; défaut : aucune. */
|
|
47
|
+
protected realtimeActions(): Record<string, RpcActionHandler>;
|
|
48
|
+
/** Canaux annoncés au handshake. À surcharger ; défaut : aucun. */
|
|
49
|
+
protected realtimeChannels(): string[];
|
|
50
|
+
/**
|
|
51
|
+
* Préfixes de canaux **broadcast** (cross-process) de cet endpoint. À surcharger ;
|
|
52
|
+
* défaut : **aucun** → tous les canaux restent **instance-local** (observabilité,
|
|
53
|
+
* état du pod). Un canal listé ici traverse le {@link IBackplane} (cluster IPC /
|
|
54
|
+
* Redis) : chat, présence, notifications… Cf {@link RealtimeHub.markBroadcastChannel}
|
|
55
|
+
* (défaut sûr : pas de fuite cross-pod de données per-instance sans intention explicite).
|
|
56
|
+
*/
|
|
57
|
+
protected realtimeBroadcastChannels(): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Canaux FULL-DUPLEX acceptant une entrée client (`method` = nom du canal). À
|
|
60
|
+
* surcharger ; défaut : **aucun** (sûr — un client ne peut rien pousser au serveur
|
|
61
|
+
* tant qu'un canal n'est pas explicitement déclaré ici). Seam des backings entrants
|
|
62
|
+
* (SIP, bridge). Cf {@link RealtimeInboundHandler} (params NON FIABLES).
|
|
63
|
+
*/
|
|
64
|
+
protected realtimeInbound(): Record<string, RealtimeInboundHandler>;
|
|
65
|
+
/**
|
|
66
|
+
* Opt-in du **pont API** (« API souveraine », POC Ph.3) : `true` → la
|
|
67
|
+
* connexion expose la méthode RPC `api.request {path}` qui re-route le path
|
|
68
|
+
* porté par le message vers l'action controller correspondante — la MÊME
|
|
69
|
+
* action que le GET REST (`socket.request("/nodefony/kernel/api/modules")`
|
|
70
|
+
* côté client). Le pont n'atteint QUE les routes déclarant le transport
|
|
71
|
+
* `WEBSOCKET` dans leurs `methods` (zéro bypass : une action dit à quels
|
|
72
|
+
* transports elle répond). À surcharger ; défaut : `false` — aucune surface
|
|
73
|
+
* d'invocation ajoutée à un hub sans intention explicite.
|
|
74
|
+
*/
|
|
75
|
+
protected realtimeApiRequest(): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Point d'entrée à appeler depuis la route WS du contrôleur. `message === null`
|
|
78
|
+
* = handshake (1ʳᵉ invocation) ; sinon = frame entrante.
|
|
79
|
+
*
|
|
80
|
+
* Le handshake est désormais ASYNC (seams sécu #2/#4 : origin check + run
|
|
81
|
+
* d'authenticator au handshake). Les frames texte entrantes pendant
|
|
82
|
+
* l'authentification sont DROP silencieusement — le transport JSON-RPC n'est pas
|
|
83
|
+
* encore branché, il n'existe donc AUCUN canal pour porter un refus. Le silence
|
|
84
|
+
* est structurel, pas un choix : c'est au client d'attendre `realtime:welcome`
|
|
85
|
+
* avant de pousser.
|
|
86
|
+
*
|
|
87
|
+
* ⚠️ Cette phrase a longtemps ajouté « ce que `RealtimeClient` fait nativement ».
|
|
88
|
+
* C'était FAUX : le client rejouait ses abonnements dès l'ouverture de la socket,
|
|
89
|
+
* si bien qu'un `subscribe` posé avant le démarrage et TOUS ceux d'après une
|
|
90
|
+
* reconnexion étaient perdus ici même, sans un mot des deux côtés. Un contrat écrit
|
|
91
|
+
* d'un seul côté du fil n'est pas tenu ; il l'est désormais par
|
|
92
|
+
* `RealtimeClient.replaySubscriptions` et les deux cas « la fenêtre où le serveur
|
|
93
|
+
* écoute (welcome) » de `RealtimeClientCoverage.test.ts`.
|
|
94
|
+
*/
|
|
95
|
+
protected handleRealtime(message: string | Buffer | null): void;
|
|
96
|
+
/**
|
|
97
|
+
* **L1 — duplex serveur→client.** Émet une REQUÊTE vers le client de CETTE
|
|
98
|
+
* connexion (sur une action qu'il a exposée via `client.register(...)`) et attend
|
|
99
|
+
* sa réponse. Possible depuis L0 : le client compose le même `JsonRpcPeer`, il
|
|
100
|
+
* est donc un *callee* (avant L0 : `-32601` systématique).
|
|
101
|
+
*
|
|
102
|
+
* Usages : confirmation d'action (le serveur demande au client de valider),
|
|
103
|
+
* invalidation de cache poussée AVEC accusé, health applicatif serveur→client.
|
|
104
|
+
* Pour une notification sans réponse → {@link notifyClient} (ou un canal pub/sub).
|
|
105
|
+
*
|
|
106
|
+
* @returns le `result` du handler client ; rejette sur erreur applicative
|
|
107
|
+
* ({@link RpcError} `code`/`data` préservés), méthode inconnue (`-32601`),
|
|
108
|
+
* timeout, ou connexion fermée (`dispose`).
|
|
109
|
+
* @throws (rejet) si le handshake n'est pas terminé (aucun peer encore).
|
|
110
|
+
*/
|
|
111
|
+
protected requestClient<K extends ActionNames<Actions>>(method: K, params?: ActionParams<Actions, K>, timeoutMs?: number): Promise<ActionResult<Actions, K>>;
|
|
112
|
+
/**
|
|
113
|
+
* Notification CIBLÉE serveur→client (sans réponse) vers CETTE connexion, hors
|
|
114
|
+
* canal pub/sub — pendant *fire-and-forget* de {@link requestClient}. Pour un
|
|
115
|
+
* fan-out à N abonnés, préférer un canal (`publish` via le hub). No-op si le
|
|
116
|
+
* handshake n'est pas terminé.
|
|
117
|
+
*/
|
|
118
|
+
protected notifyClient<K extends EventNames<Emit>>(method: K, params?: EventPayload<Emit, K>): void;
|
|
119
|
+
/**
|
|
120
|
+
* Handshake : seams sécurité #4 (Origin RFC 6455 §10.2) + #2 (authenticator
|
|
121
|
+
* réseau) PUIS crée peer+transport, enregistre les actions, welcome + cleanup.
|
|
122
|
+
*
|
|
123
|
+
* Pipeline (cold path, 1× par connexion) :
|
|
124
|
+
* 1. Origin check via `hub.checkOrigin()` (bypass si `csrf.checkOrigin.enabled=false`).
|
|
125
|
+
* 2. Resolve authenticator via `hub.resolveAuthenticator(handshake)` (matchers
|
|
126
|
+
* ordonnés, 1ʳᵉ qui matche capture) → `authenticate()` async.
|
|
127
|
+
* 3. Aucun match OU `enabled=false` → `ANONYMOUS_REALTIME_TOKEN`.
|
|
128
|
+
* 4. Pose le token sur `hub.peer → token` (lookup voters P6 hot-path).
|
|
129
|
+
*
|
|
130
|
+
* Échecs (close WebSocket, codes plage applicative RFC 6455 §7.4.2) :
|
|
131
|
+
* - Origin refusée → code 4003 (`forbidden`).
|
|
132
|
+
* - `authenticate` throw → code 4001 (`unauthorized`).
|
|
133
|
+
*/
|
|
134
|
+
private onHandshake;
|
|
135
|
+
/**
|
|
136
|
+
* Notifications entrantes : pub/sub (subscribe/unsubscribe), heartbeat (ping), puis
|
|
137
|
+
* canaux FULL-DUPLEX déclarés (entrée client → handler `realtimeInbound`).
|
|
138
|
+
*/
|
|
139
|
+
private onRealtimeNotification;
|
|
140
|
+
/** Abonne la connexion à un canal via le hub partagé (idempotent par connexion). */
|
|
141
|
+
private startChannel;
|
|
142
|
+
/**
|
|
143
|
+
* Handler de la méthode RPC `api.request {path}` — le **pont API souverain**
|
|
144
|
+
* (POC Ph.3) : résout le path porté par le message via le Router
|
|
145
|
+
* (`cleanPathOverride` — l'URL de la connexion n'est jamais mutée) puis
|
|
146
|
+
* exécute l'action SANS la rendre (`executeAction`) : la valeur nue est
|
|
147
|
+
* enveloppée `{id, result}` par le peer — snapshot ≡ GET REST par
|
|
148
|
+
* construction.
|
|
149
|
+
*
|
|
150
|
+
* Query : le `?…` du path invoqué est parsé ici (paires plates, clés répétées
|
|
151
|
+
* → array — sémantique `qs` plate) et porté par `resolver.queryOverride`
|
|
152
|
+
* (per-invocation : zéro bleed entre requêtes concurrentes de la même
|
|
153
|
+
* socket). Limitation POC assumée : syntaxe nested `a[b]=c` non supportée
|
|
154
|
+
* par le pont (doc §11 — le parse complet du transport HTTP n'est pas
|
|
155
|
+
* mutualisé tant que Ph.6 n'a pas tranché).
|
|
156
|
+
*
|
|
157
|
+
* Erreurs → {@link RpcError} (`data.status` = statut HTTP équivalent) :
|
|
158
|
+
* `-32602` params invalides, 404 path inconnu, 408 timeout d'action…
|
|
159
|
+
* Tout autre throw de l'action reste opaque (`-32603`, Zero Trust).
|
|
160
|
+
*/
|
|
161
|
+
private invokeApiRequest;
|
|
162
|
+
/** Désabonne la connexion d'un canal (le hub dispose le provider au dernier abonné). */
|
|
163
|
+
private stopChannel;
|
|
164
|
+
}
|
|
165
|
+
export default RealtimeController;
|