@nodefony/realtime 10.0.0-beta.3 → 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/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +4 -6
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/configSchema.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/decorators/realtimeDecorators.js +1 -2
- 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 +1 -2
- package/dist/nodefony/src/backplane/backplaneRegistry.js +1 -2
- package/dist/nodefony/src/backplane/bootNotices.js +1 -2
- 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 +0 -1
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -2
- package/dist/nodefony/src/guards.js +1 -2
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +1 -2
- package/dist/nodefony/src/server/RealtimeAdminApi.js +1 -2
- package/dist/nodefony/src/server/RealtimeController.js +154 -80
- package/dist/nodefony/src/server/RealtimeHub.js +3 -3
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +18 -9
- 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 +1 -2
- package/dist/nodefony/src/server/welcomeEnv.js +1 -2
- package/dist/nodefony/src/service/RealtimeService.js +1 -2
- package/dist/nodefony/src/transport/WsConnectionTransport.js +42 -2
- package/dist/nodefony/testing/index.js +77 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +40 -0
- 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/server/RealtimeController.d.ts +58 -0
- 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/transport/WsConnectionTransport.d.ts +26 -1
- package/dist/types/nodefony/testing/index.d.ts +6 -1
- package/docs/actions.md +44 -43
- package/docs/architecture.md +125 -119
- package/docs/cadence-et-contre-pression.md +31 -22
- package/docs/configuration.md +106 -82
- package/docs/cookbook-chat.md +37 -34
- package/docs/index.md +35 -24
- package/docs/observabilite.md +49 -49
- package/docs/protocole.md +91 -68
- package/docs/securite.md +125 -114
- package/docs/vocabulaire.md +117 -114
- package/package.json +10 -10
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -226,18 +226,27 @@ Deux réglages, et le second est celui qu'on oublie :
|
|
|
226
226
|
`atelier:load:2000` ouvert à n'importe qui.
|
|
227
227
|
|
|
228
228
|
```ts
|
|
229
|
+
// nodefony/config/http.ts — le fragment du module, importé par le manifeste
|
|
230
|
+
import type { IHttpConfigInput } from "@nodefony/http";
|
|
231
|
+
|
|
232
|
+
export const httpConfig = () =>
|
|
233
|
+
({
|
|
234
|
+
websocket: { maxBackpressure: 1048576, backpressureCloseAfterDrops: 200 },
|
|
235
|
+
websocketSecure: {
|
|
236
|
+
maxBackpressure: 1048576,
|
|
237
|
+
backpressureCloseAfterDrops: 200,
|
|
238
|
+
},
|
|
239
|
+
}) satisfies IHttpConfigInput;
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```ts ignore
|
|
229
243
|
// nodefony.config.ts — extrait
|
|
230
244
|
import { defineConfig, use } from "nodefony";
|
|
245
|
+
import { httpConfig } from "./nodefony/config/http";
|
|
231
246
|
|
|
232
247
|
export default defineConfig(() => ({
|
|
233
248
|
modules: [
|
|
234
|
-
use("@nodefony/http",
|
|
235
|
-
websocket: { maxBackpressure: 1048576, backpressureCloseAfterDrops: 200 },
|
|
236
|
-
websocketSecure: {
|
|
237
|
-
maxBackpressure: 1048576,
|
|
238
|
-
backpressureCloseAfterDrops: 200,
|
|
239
|
-
},
|
|
240
|
-
}),
|
|
249
|
+
use("@nodefony/http", httpConfig()),
|
|
241
250
|
"@nodefony/framework",
|
|
242
251
|
use("@nodefony/security", {
|
|
243
252
|
// Préfixe : garde atelier:load ET atelier:load:2000, atelier:load:4000…
|
|
@@ -270,8 +279,8 @@ curl -sk https://127.0.0.1:5152/nodefony/realtime/api/health | jq '.backpressure
|
|
|
270
279
|
### La cadence vit dans le nom du canal
|
|
271
280
|
|
|
272
281
|
Un canal d'état cadencé s'appelle `base` (cadence par défaut) ou `base:<ms>` (cadence explicite).
|
|
273
|
-
Le client fabrique le nom avec `rateChannel()` (`channelRate.ts
|
|
274
|
-
**borne** avec `parseRate()` (`channelRate.ts
|
|
282
|
+
Le client fabrique le nom avec `rateChannel()` (`channelRate.ts#rateChannel`), le serveur le relit et le
|
|
283
|
+
**borne** avec `parseRate()` (`channelRate.ts#parseRate`). Une même fonction pour les deux bords : la
|
|
275
284
|
convention ne peut pas diverger.
|
|
276
285
|
|
|
277
286
|
Conséquences voulues :
|
|
@@ -279,7 +288,7 @@ Conséquences voulues :
|
|
|
279
288
|
- **1 canal = 1 cadence = 1 ticker.** Deux clients à 2 s partagent `base:2000` ; un troisième à 4 s
|
|
280
289
|
ouvre `base:4000`. Le hub compte les abonnés et arrête le ticker au dernier.
|
|
281
290
|
- **Le serveur garde la main.** Un client qui demande `base:10` obtient la borne basse : Studio
|
|
282
|
-
borne ses statistiques à 250 ms minimum (`RATE_BOUNDS`, `StudioRealtimeController.ts
|
|
291
|
+
borne ses statistiques à 250 ms minimum (`RATE_BOUNDS`, `StudioRealtimeController.ts#RATE_BOUNDS`).
|
|
283
292
|
- **Aucun message de contrôle** : changer de cadence, c'est changer d'abonnement. Le protocole
|
|
284
293
|
reste celui des canaux, rien de plus.
|
|
285
294
|
|
|
@@ -292,7 +301,7 @@ Conséquences voulues :
|
|
|
292
301
|
|
|
293
302
|
### Ce que le client mesure
|
|
294
303
|
|
|
295
|
-
À chaque trame reçue, `AdaptiveRate.noteFrame()` (`AdaptiveRate.ts
|
|
304
|
+
À chaque trame reçue, `AdaptiveRate.noteFrame()` (`AdaptiveRate.ts#AdaptiveRate.noteFrame`) compare l'écart depuis
|
|
296
305
|
la trame précédente à la cadence courante. Trois zones, et une seule décision par trame :
|
|
297
306
|
|
|
298
307
|
```mermaid
|
|
@@ -310,7 +319,7 @@ flowchart LR
|
|
|
310
319
|
| **Bande morte** | entre les deux | ne change rien, et **remet à zéro** le compte des bons échantillons |
|
|
311
320
|
|
|
312
321
|
Les seuils et la fenêtre sont des options (`starvationFactor`, `healthyFactor`, `recoveryWindow`),
|
|
313
|
-
avec ces défauts (`AdaptiveRate.ts
|
|
322
|
+
avec ces défauts (`AdaptiveRate.ts#AdaptiveRate.constructor`).
|
|
314
323
|
|
|
315
324
|
> [!TIP]
|
|
316
325
|
> La **bande morte** est ce qui empêche l'oscillation : un réseau « moyen » ne fait ni monter ni
|
|
@@ -320,7 +329,7 @@ avec ces défauts (`AdaptiveRate.ts:100`).
|
|
|
320
329
|
### Ralentir vite, accélérer prudemment
|
|
321
330
|
|
|
322
331
|
- **L'échelle** est géométrique : de la cadence désirée, doublée jusqu'à 60 s par défaut — `1 s,
|
|
323
|
-
2 s, 4 s, 8 s, …, 60 s` (`deriveLadder()`, `AdaptiveRate.ts
|
|
332
|
+
2 s, 4 s, 8 s, …, 60 s` (`deriveLadder()`, `AdaptiveRate.ts#deriveLadder`). Une échelle sur mesure se passe
|
|
324
333
|
par l'option `ladder`.
|
|
325
334
|
- **Ralentir** = monter d'un cran (×2) **dès la première famine**. Plusieurs famines successives
|
|
326
335
|
doublent plusieurs fois : c'est ce qui rend la décroissance multiplicative.
|
|
@@ -329,15 +338,15 @@ avec ces défauts (`AdaptiveRate.ts:100`).
|
|
|
329
338
|
- **Après chaque changement**, la mesure repart de zéro : la première trame du nouveau canal ne sert
|
|
330
339
|
que de référence.
|
|
331
340
|
- **Famine totale** : si plus aucune trame n'arrive, `noteFrame()` ne serait jamais appelé. Un chien
|
|
332
|
-
de garde, réarmé à chaque cadence, appelle `checkStarvation()` (`AdaptiveRate.ts
|
|
341
|
+
de garde, réarmé à chaque cadence, appelle `checkStarvation()` (`AdaptiveRate.ts#AdaptiveRate.checkStarvation`) et ralentit
|
|
333
342
|
quand même.
|
|
334
343
|
|
|
335
344
|
### Changer de cadence sans trou
|
|
336
345
|
|
|
337
346
|
**L'image** : on change de voie sur l'autoroute en s'insérant dans la nouvelle **avant** de quitter
|
|
338
|
-
l'ancienne, jamais l'inverse. `bindAdaptiveChannel()` (`AdaptiveRate.ts
|
|
347
|
+
l'ancienne, jamais l'inverse. `bindAdaptiveChannel()` (`AdaptiveRate.ts#bindAdaptiveChannel`) fait pareil : il
|
|
339
348
|
**s'abonne au nouveau canal, puis quitte l'ancien**, dans le même geste (`applyDecision()`,
|
|
340
|
-
`AdaptiveRate.ts
|
|
349
|
+
`AdaptiveRate.ts#bindAdaptiveChannel`).
|
|
341
350
|
|
|
342
351
|
```mermaid
|
|
343
352
|
flowchart LR
|
|
@@ -394,12 +403,12 @@ rétabli. Il sonde prudemment, recule s'il s'est trompé, et ne le paie que de q
|
|
|
394
403
|
Quand le serveur envoie une trame, la socket l'accepte et la place dans sa **file d'envoi** :
|
|
395
404
|
`bufferedAmount` compte les octets qui attendent le réseau. Un client qui ne lit plus fait grossir
|
|
396
405
|
cette file sans fin. Nodefony la plafonne à **`maxBackpressure`**, 4 Mio par défaut
|
|
397
|
-
(`@nodefony/http/nodefony/config/config.ts
|
|
406
|
+
(`@nodefony/http/nodefony/config/config.ts#websocketSchema`).
|
|
398
407
|
|
|
399
408
|
La décision est prise **trame par trame**, par une seule fonction : `decideSend()`
|
|
400
|
-
(`wsBackpressure.ts
|
|
409
|
+
(`wsBackpressure.ts#decideSend`). Elle sert à la fois le `send()`/`broadcast()` d'un contexte WebSocket de
|
|
401
410
|
`@nodefony/http` et chaque connexion temps réel (`WsConnectionTransport.send()`,
|
|
402
|
-
`WsConnectionTransport.ts
|
|
411
|
+
`WsConnectionTransport.ts#WsConnectionTransport.send`). Une seule règle, deux usages.
|
|
403
412
|
|
|
404
413
|
```mermaid
|
|
405
414
|
flowchart LR
|
|
@@ -488,8 +497,8 @@ Le client **n'apprend jamais** les seuils : il ne reçoit que leur effet.
|
|
|
488
497
|
- Une trame jetée **n'existe pas** pour lui : il voit seulement un écart plus long entre deux
|
|
489
498
|
trames.
|
|
490
499
|
- Une fermeture arrive avec le code `1013`. Il le classe **transitoire** : ce n'est pas un des codes
|
|
491
|
-
définitifs (`FATAL_CLOSE_CODES`, `notice.ts
|
|
492
|
-
(`notice.ts
|
|
500
|
+
définitifs (`FATAL_CLOSE_CODES`, `notice.ts#FATAL_CLOSE_CODES`), donc `isReconnectableCloseCode()`
|
|
501
|
+
(`notice.ts#isReconnectableCloseCode`) répond oui, et il se reconnecte avec une attente croissante.
|
|
493
502
|
|
|
494
503
|
## 🔌 Comment les deux étages se rencontrent
|
|
495
504
|
|
|
@@ -514,7 +523,7 @@ sans activer de trace.
|
|
|
514
523
|
|
|
515
524
|
### Ce que la sonde du hub expose
|
|
516
525
|
|
|
517
|
-
`RealtimeHub.probe()` (`RealtimeHub.ts
|
|
526
|
+
`RealtimeHub.probe()` (`RealtimeHub.ts#RealtimeHub.probe`) parcourt les connexions et rend un bloc
|
|
518
527
|
`backpressure` :
|
|
519
528
|
|
|
520
529
|
| Champ | Ce qu'il dit | Lecture |
|
package/docs/configuration.md
CHANGED
|
@@ -125,19 +125,19 @@ fichiers, jamais trois**.
|
|
|
125
125
|
| `nodefony/config/config.ts` | **le quoi** | le schéma Zod commenté, source **unique** des valeurs d'usine, et les défauts matérialisés |
|
|
126
126
|
| `nodefony/config/defineModuleConfig.ts` | **le comment** | le builder pur : analyse, surcharge d'environnement, gel — il ne retape jamais une valeur d'usine |
|
|
127
127
|
|
|
128
|
-
Le schéma `realtimeConfigSchema` (`config.ts
|
|
128
|
+
Le schéma `realtimeConfigSchema` (`config.ts#realtimeConfigSchema`) porte chaque valeur en `.default()` et chaque
|
|
129
129
|
explication en `.describe()`. Les défauts effectifs sont **dérivés** du schéma lui-même
|
|
130
|
-
(`realtimeConfigSchema.parse({})`, `config.ts
|
|
130
|
+
(`realtimeConfigSchema.parse({})`, `config.ts#clientLogsSchema`) : il n'existe pas de second endroit où une valeur
|
|
131
131
|
d'usine serait écrite, donc pas de dérive silencieuse possible entre la doc du champ et son
|
|
132
132
|
comportement.
|
|
133
133
|
|
|
134
|
-
Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts
|
|
134
|
+
Le builder `defineRealtimeConfig()` (`defineModuleConfig.ts#defineRealtimeConfig`) reste **pur** : il analyse l'entrée
|
|
135
135
|
et gèle le résultat. Il ne lit aucune variable d'environnement. Les trois variables du module
|
|
136
136
|
(`NF_REALTIME_DRIVER`, `NF_REALTIME_BACKPLANE_NAMESPACE`, `NF_REALTIME_BACKPLANE_SECRET`) sont
|
|
137
|
-
déclarées sur leur champ dans le schéma (`.meta({ env })`, `config.ts
|
|
137
|
+
déclarées sur leur champ dans le schéma (`.meta({ env })`, `config.ts#backplaneSchema`) et posées
|
|
138
138
|
par le Kernel **avant** l'analyse : le schéma les valide comme une valeur écrite dans le fichier.
|
|
139
139
|
|
|
140
|
-
Le module **augmente le registre de types** (`index.ts
|
|
140
|
+
Le module **augmente le registre de types** (`index.ts#tick`) : `use("@nodefony/realtime", { … })`
|
|
141
141
|
propose ses clés en autocomplétion et **refuse** une clé inconnue à la compilation.
|
|
142
142
|
|
|
143
143
|
> [!IMPORTANT]
|
|
@@ -157,31 +157,42 @@ On garde le fond de panier local, on ferme la porte d'entrée, on abaisse le pla
|
|
|
157
157
|
dont l'application a réellement besoin.
|
|
158
158
|
|
|
159
159
|
```ts
|
|
160
|
+
// nodefony/config/realtime.ts — le fragment du module, importé par le manifeste
|
|
161
|
+
import type { IRealtimeConfigInput } from "@nodefony/realtime";
|
|
162
|
+
|
|
163
|
+
export const realtimeConfig = () =>
|
|
164
|
+
({
|
|
165
|
+
// Un seul processus : le hub diffuse localement, aucun transport n'est ouvert.
|
|
166
|
+
backplane: { driver: "loopback" },
|
|
167
|
+
|
|
168
|
+
// Défense CSWSH (RFC 6455 §10.2). Sans elle, une page tierce ouvre une socket
|
|
169
|
+
// avec les cookies de ton utilisateur : le navigateur n'applique pas CORS ici.
|
|
170
|
+
csrf: {
|
|
171
|
+
checkOrigin: {
|
|
172
|
+
enabled: true,
|
|
173
|
+
allowList: ["https://127.0.0.1:5152"],
|
|
174
|
+
// Un client non-navigateur n'envoie pas d'en-tête Origin. Le laisser à
|
|
175
|
+
// false n'a de sens que si tout ton trafic vient d'un navigateur.
|
|
176
|
+
allowMissingOrigin: false,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Garde anti-saturation : le défaut de 256 est généreux. Un écran qui suit
|
|
181
|
+
// dix canaux n'a aucune raison d'en ouvrir plus de quelques dizaines.
|
|
182
|
+
limits: { maxChannelsPerConnection: 32 },
|
|
183
|
+
}) satisfies IRealtimeConfigInput;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```ts ignore
|
|
160
187
|
// nodefony.config.ts — le manifeste de l'application
|
|
188
|
+
import { defineConfig, use } from "nodefony";
|
|
189
|
+
import { realtimeConfig } from "./nodefony/config/realtime";
|
|
190
|
+
|
|
161
191
|
export default defineConfig(() => ({
|
|
162
192
|
modules: [
|
|
163
193
|
"@nodefony/http",
|
|
164
194
|
"@nodefony/framework",
|
|
165
|
-
use("@nodefony/realtime",
|
|
166
|
-
// Un seul processus : le hub diffuse localement, aucun transport n'est ouvert.
|
|
167
|
-
backplane: { driver: "loopback" },
|
|
168
|
-
|
|
169
|
-
// Défense CSWSH (RFC 6455 §10.2). Sans elle, une page tierce ouvre une socket
|
|
170
|
-
// avec les cookies de ton utilisateur : le navigateur n'applique pas CORS ici.
|
|
171
|
-
csrf: {
|
|
172
|
-
checkOrigin: {
|
|
173
|
-
enabled: true,
|
|
174
|
-
allowList: ["https://127.0.0.1:5152"],
|
|
175
|
-
// Un client non-navigateur n'envoie pas d'en-tête Origin. Le laisser à
|
|
176
|
-
// false n'a de sens que si tout ton trafic vient d'un navigateur.
|
|
177
|
-
allowMissingOrigin: false,
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
// Garde anti-saturation : le défaut de 256 est généreux. Un écran qui suit
|
|
182
|
-
// dix canaux n'a aucune raison d'en ouvrir plus de quelques dizaines.
|
|
183
|
-
limits: { maxChannelsPerConnection: 32 },
|
|
184
|
-
}),
|
|
195
|
+
use("@nodefony/realtime", realtimeConfig()),
|
|
185
196
|
],
|
|
186
197
|
}));
|
|
187
198
|
```
|
|
@@ -191,8 +202,39 @@ export default defineConfig(() => ({
|
|
|
191
202
|
Passer à N pods sur N machines ne change **ni le contrôleur, ni le client, ni un seul canal** : on
|
|
192
203
|
remplace le nom du driver, et on déclare le module qui fournit les connexions pub/sub.
|
|
193
204
|
|
|
194
|
-
```ts
|
|
205
|
+
```ts ignore
|
|
206
|
+
// nodefony/config/realtime.ts — le fragment du module, importé par le manifeste
|
|
207
|
+
import type { ConfigContext } from "nodefony";
|
|
208
|
+
import type { IRealtimeConfigInput } from "@nodefony/realtime";
|
|
209
|
+
import type { env } from "../../env";
|
|
210
|
+
|
|
211
|
+
export const realtimeConfig = (ctx: ConfigContext<typeof env>) =>
|
|
212
|
+
({
|
|
213
|
+
backplane: {
|
|
214
|
+
// En production seulement : en développement, le fond de panier local évite
|
|
215
|
+
// d'exiger une infrastructure pour lancer l'application.
|
|
216
|
+
driver: ctx.isProd ? "redis" : "loopback",
|
|
217
|
+
|
|
218
|
+
// La cloison. Le numéro de base Redis ne cloisonne PAS le pub/sub : sans ce
|
|
219
|
+
// nom, préproduction et production sur un même Redis échangeraient leurs
|
|
220
|
+
// diffusions. À poser explicitement, et différemment par déploiement.
|
|
221
|
+
namespace: ctx.isProd ? "boutique.prod" : "boutique.dev",
|
|
222
|
+
},
|
|
223
|
+
csrf: {
|
|
224
|
+
checkOrigin: {
|
|
225
|
+
enabled: true,
|
|
226
|
+
allowList: ["https://boutique.example.com"],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
limits: { maxChannelsPerConnection: 32 },
|
|
230
|
+
}) satisfies IRealtimeConfigInput;
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```ts ignore
|
|
195
234
|
// nodefony.config.ts — la même application, déployée en plusieurs répliques
|
|
235
|
+
import { defineConfig, use } from "nodefony";
|
|
236
|
+
import { realtimeConfig } from "./nodefony/config/realtime";
|
|
237
|
+
|
|
196
238
|
export default defineConfig((ctx) => ({
|
|
197
239
|
modules: [
|
|
198
240
|
"@nodefony/http",
|
|
@@ -202,25 +244,7 @@ export default defineConfig((ctx) => ({
|
|
|
202
244
|
// module. Sans lui déclaré, le hub reste local — et le dit dans les journaux.
|
|
203
245
|
"@nodefony/redis",
|
|
204
246
|
|
|
205
|
-
use("@nodefony/realtime",
|
|
206
|
-
backplane: {
|
|
207
|
-
// En production seulement : en développement, le fond de panier local évite
|
|
208
|
-
// d'exiger une infrastructure pour lancer l'application.
|
|
209
|
-
driver: ctx.isProd ? "redis" : "loopback",
|
|
210
|
-
|
|
211
|
-
// La cloison. Le numéro de base Redis ne cloisonne PAS le pub/sub : sans ce
|
|
212
|
-
// nom, préproduction et production sur un même Redis échangeraient leurs
|
|
213
|
-
// diffusions. À poser explicitement, et différemment par déploiement.
|
|
214
|
-
namespace: ctx.isProd ? "boutique.prod" : "boutique.dev",
|
|
215
|
-
},
|
|
216
|
-
csrf: {
|
|
217
|
-
checkOrigin: {
|
|
218
|
-
enabled: true,
|
|
219
|
-
allowList: ["https://boutique.example.com"],
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
limits: { maxChannelsPerConnection: 32 },
|
|
223
|
-
}),
|
|
247
|
+
use("@nodefony/realtime", realtimeConfig(ctx)),
|
|
224
248
|
],
|
|
225
249
|
}));
|
|
226
250
|
```
|
|
@@ -252,7 +276,7 @@ regarder quand un message ne traverse pas.
|
|
|
252
276
|
|
|
253
277
|
> [!TIP]
|
|
254
278
|
> Ces quatre lignes viennent du même endroit du code (`Realtime.#wireBackplane()`,
|
|
255
|
-
> `src/packages/@nodefony/realtime/index.ts
|
|
279
|
+
> `src/packages/@nodefony/realtime/index.ts#Realtime.#wireBackplane`) et de la carte d'identité que chaque driver publie.
|
|
256
280
|
> Aucun repli n'est silencieux : si le fan-out est dégradé, c'est écrit.
|
|
257
281
|
|
|
258
282
|
## ⚙️ Le schéma, clé par clé
|
|
@@ -289,8 +313,8 @@ contre-pression, eux, se règlent sur le serveur WebSocket de `@nodefony/http`
|
|
|
289
313
|
|
|
290
314
|
Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
|
|
291
315
|
enregistrée, pas de backplane câblé, pas de sonde de pod
|
|
292
|
-
(`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts
|
|
293
|
-
inerte (`RealtimeService.init()`, `RealtimeService.ts
|
|
316
|
+
(`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts#Realtime.onKernelBoot`), et le service devient
|
|
317
|
+
inerte (`RealtimeService.init()`, `RealtimeService.ts#RealtimeService.init`).
|
|
294
318
|
|
|
295
319
|
> [!WARNING]
|
|
296
320
|
> **`enabled: false` désarme aussi tes gardes.** Le hub garde son plafond interne de 256 canaux,
|
|
@@ -305,7 +329,7 @@ désactiver vraiment, retire le module du manifeste.
|
|
|
305
329
|
|
|
306
330
|
C'est **le** réglage du module. Le champ est une chaîne libre et non une énumération, volontairement :
|
|
307
331
|
la liste réelle est celle du **registre** de drivers (`listBackplaneDrivers()`,
|
|
308
|
-
`backplaneRegistry.ts
|
|
332
|
+
`backplaneRegistry.ts#listBackplaneDrivers`), ouverte aux drivers que tu écris toi-même. Une énumération figée dans le
|
|
309
333
|
schéma fermerait cette porte.
|
|
310
334
|
|
|
311
335
|
Trois noms sont enregistrés par le module au chargement :
|
|
@@ -327,10 +351,10 @@ de frappe dans une variable de déploiement ne doit pas empêcher l'application
|
|
|
327
351
|
### `backplane.namespace` — la cloison sur un bus partagé
|
|
328
352
|
|
|
329
353
|
Ne concerne que les drivers qui traversent les machines. Le canal de transport devient
|
|
330
|
-
`nodefony:realtime:<namespace>` (`resolveRedisChannel()`, `RedisBackplane.ts
|
|
354
|
+
`nodefony:realtime:<namespace>` (`resolveRedisChannel()`, `RedisBackplane.ts#resolveRedisChannel`).
|
|
331
355
|
|
|
332
356
|
Le défaut est **dérivé du nom de l'application**
|
|
333
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
357
|
+
(`src/packages/@nodefony/realtime/index.ts`) — ce qui suffit à séparer deux applications
|
|
334
358
|
distinctes sur un Redis mutualisé, et **ne suffit pas** à séparer deux déploiements de la **même**
|
|
335
359
|
application. Préproduction et production portent le même nom d'application, donc le même canal
|
|
336
360
|
dérivé, donc le même flux.
|
|
@@ -342,7 +366,7 @@ dérivé, donc le même flux.
|
|
|
342
366
|
> utilisateurs. Pose-la dès que deux environnements partagent un Redis.
|
|
343
367
|
|
|
344
368
|
Caractères acceptés : lettres, chiffres, `_`, `.`, `-` — le motif est validé par le schéma
|
|
345
|
-
(`backplaneSchema`, `config.ts
|
|
369
|
+
(`backplaneSchema`, `config.ts#backplaneSchema`), donc un nom fautif arrête le démarrage plutôt que de produire un
|
|
346
370
|
canal exotique.
|
|
347
371
|
|
|
348
372
|
### `backplane.secret` — sceller ce qui circule sur le bus
|
|
@@ -350,7 +374,7 @@ canal exotique.
|
|
|
350
374
|
Ne concerne, là aussi, que les transports **partagés**. Un bus pub/sub ne dit pas _qui_ a publié :
|
|
351
375
|
sans secret, tout ce qui atterrit sur le canal — venu d'un pod ou d'un tiers qui sait écrire dans ce
|
|
352
376
|
Redis — est traité comme un message légitime. Poser un secret fait porter à chaque message un sceau
|
|
353
|
-
HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts
|
|
377
|
+
HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts#openBackplaneEnvelope`) : un message non
|
|
354
378
|
scellé, altéré ou repointé vers un autre canal est **ignoré**, sans exception possible.
|
|
355
379
|
|
|
356
380
|
Le secret doit être **identique sur tous les pods** — c'est lui qui les reconnaît entre eux. En
|
|
@@ -370,7 +394,7 @@ sonde de santé — il reste à zéro en régime normal.
|
|
|
370
394
|
Sans effet hors d'un worker lancé par `nodefony cluster`. Dans ce contexte, la sonde remonte
|
|
371
395
|
périodiquement la santé de **son** processus au maître, qui la consolide, et redistribue
|
|
372
396
|
l'instantané : n'importe quel worker peut alors servir la vue « pod entier » sans interroger les
|
|
373
|
-
autres (`ClusterProbeClient`, `ClusterProbeClient.ts
|
|
397
|
+
autres (`ClusterProbeClient`, `ClusterProbeClient.ts#ClusterProbeClient`).
|
|
374
398
|
|
|
375
399
|
Le coût est faible mais réel : un minuteur (déréférencé, il n'empêche pas le processus de sortir) et
|
|
376
400
|
un écouteur IPC par worker. Poser `false` produit un **contournement complet** — le client n'est
|
|
@@ -378,13 +402,13 @@ jamais instancié, donc zéro minuteur, zéro message IPC — et l'endpoint de s
|
|
|
378
402
|
de l'instance courante.
|
|
379
403
|
|
|
380
404
|
Deux leviers coupent la sonde, et **l'un ou l'autre suffit** : cette clé, ou la variable
|
|
381
|
-
`NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts
|
|
405
|
+
`NF_CLUSTER_PROBE=0` (`src/packages/@nodefony/realtime/index.ts#Realtime.#wireClusterProbe`). C'est ce qui permet
|
|
382
406
|
d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
|
|
383
407
|
|
|
384
408
|
### `slowConsumer.bytes` — un compteur, pas un frein
|
|
385
409
|
|
|
386
410
|
Seuil de `bufferedAmount` — les octets en attente d'envoi sur une socket — au-delà duquel la sonde
|
|
387
|
-
compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts
|
|
411
|
+
compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts#RealtimeHub.probe`).
|
|
388
412
|
|
|
389
413
|
> [!IMPORTANT]
|
|
390
414
|
> **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
|
|
@@ -440,7 +464,7 @@ des canaux d'état, où seul le dernier instantané compte.
|
|
|
440
464
|
Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
|
|
441
465
|
côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
|
|
442
466
|
processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
|
|
443
|
-
`RealtimeController.ts
|
|
467
|
+
`RealtimeController.ts#RealtimeController.startChannel`).
|
|
444
468
|
|
|
445
469
|
Quatre propriétés qui décident du bon réglage :
|
|
446
470
|
|
|
@@ -451,7 +475,7 @@ Quatre propriétés qui décident du bon réglage :
|
|
|
451
475
|
- Un réabonnement à un canal **déjà tenu** ne consomme pas de place — l'idempotence est vérifiée
|
|
452
476
|
avant le plafond. Un client qui redemande son canal n'est pas puni.
|
|
453
477
|
- La garde existe **même sans configuration** : le hub porte le même défaut de 256
|
|
454
|
-
(`RealtimeHub.ts
|
|
478
|
+
(`RealtimeHub.ts#RealtimeHub`), il n'y a pas de fenêtre où elle serait absente.
|
|
455
479
|
|
|
456
480
|
`null` retire la borne. C'est un retrait explicite, à réserver aux déploiements dont tu maîtrises
|
|
457
481
|
les clients — un tableau de bord interne qui compose des dizaines de flux, par exemple.
|
|
@@ -463,13 +487,13 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
|
|
|
463
487
|
### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
|
|
464
488
|
|
|
465
489
|
Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
|
|
466
|
-
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts
|
|
490
|
+
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts#RealtimeHub.checkOrigin`). Une origine refusée ferme la socket avec le code
|
|
467
491
|
`4003`.
|
|
468
492
|
|
|
469
493
|
Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
|
|
470
494
|
active-le dès que ta socket est joignable depuis un navigateur.
|
|
471
495
|
|
|
472
|
-
Le comportement se lit en trois lignes (`buildOriginGuard()`, `RealtimeService.ts
|
|
496
|
+
Le comportement se lit en trois lignes (`buildOriginGuard()`, `RealtimeService.ts#buildOriginGuard`) :
|
|
473
497
|
|
|
474
498
|
| Situation | Résultat |
|
|
475
499
|
| ------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
@@ -528,7 +552,7 @@ use("@nodefony/realtime", { backplane: { driver: "cluster" } });
|
|
|
528
552
|
**Ce qui se passe.** Un worker Node ne peut parler qu'au maître. Il lui envoie ses publications, et le
|
|
529
553
|
maître les redistribue aux autres workers. Le driver ne s'active qu'en **rôle worker** et avec la
|
|
530
554
|
variable `NF_CLUSTER=1`, posée par la commande de cluster
|
|
531
|
-
(`src/packages/@nodefony/realtime/index.ts
|
|
555
|
+
(`src/packages/@nodefony/realtime/index.ts`) ; ailleurs, il rend `null` et le hub reste local.
|
|
532
556
|
|
|
533
557
|
**Pourquoi c'est précieux.** C'est le banc d'essai qui stabilise l'architecture multi-processus avant
|
|
534
558
|
d'ajouter du réseau. Ton contrôleur, tes canaux diffusables et ton client voient un vrai fan-out
|
|
@@ -553,7 +577,7 @@ use("@nodefony/realtime", {
|
|
|
553
577
|
|
|
554
578
|
**Comment il se branche.** Le driver ne dépend pas de la bibliothèque `redis` : il consomme deux
|
|
555
579
|
connexions du module `@nodefony/redis` par un adaptateur purement structurel
|
|
556
|
-
(`createRedisServiceTransport()`, `RedisBackplane.ts
|
|
580
|
+
(`createRedisServiceTransport()`, `RedisBackplane.ts#createRedisServiceTransport`). Deux et non une, parce qu'un client Redis
|
|
557
581
|
abonné ne peut plus émettre de commandes ordinaires — le module fournit précisément des connexions
|
|
558
582
|
nommées `publish` et `subscribe` dans ses défauts.
|
|
559
583
|
|
|
@@ -563,7 +587,7 @@ coupure. C'est acceptable pour un salon de discussion — le message est en base
|
|
|
563
587
|
un bus d'événements critiques. Ne construis pas au-dessus une fiabilité que le support n'offre pas.
|
|
564
588
|
|
|
565
589
|
**Si Redis est injoignable.** Le démarrage du backplane est attendu **explicitement**, et **borné à
|
|
566
|
-
cinq secondes** (`src/packages/@nodefony/realtime/index.ts
|
|
590
|
+
cinq secondes** (`src/packages/@nodefony/realtime/index.ts#BACKPLANE_START_TIMEOUT_MS`). Au-delà, ou en cas d'échec, le
|
|
567
591
|
module renonce, ferme proprement ce qu'il avait ouvert, et laisse le hub local — le démarrage
|
|
568
592
|
continue. Une base de messages en panne ne doit pas empêcher tes serveurs HTTP de monter.
|
|
569
593
|
|
|
@@ -637,21 +661,21 @@ Quatre points que la fabrique doit respecter :
|
|
|
637
661
|
délai.
|
|
638
662
|
2. Elle peut rendre **`null`** pour dire « inactif dans ce contexte » — mauvais rôle, infrastructure
|
|
639
663
|
absente. Le hub reste alors local, sans erreur.
|
|
640
|
-
3. Elle reçoit un contexte complet (`IBackplaneFactoryContext`, `backplaneRegistry.ts
|
|
664
|
+
3. Elle reçoit un contexte complet (`IBackplaneFactoryContext`, `backplaneRegistry.ts#IBackplaneFactoryContext`) : le
|
|
641
665
|
module, l'`originId` déjà résolu, le rôle dans la topologie et la configuration validée.
|
|
642
666
|
4. `describe()` alimente **trois** sorties depuis une source unique : la ligne de journal, la sonde
|
|
643
667
|
de santé et l'écran Studio. Remplis-la honnêtement, en particulier `crossPod`.
|
|
644
668
|
|
|
645
669
|
**L'autre voie : fournir une instance déjà construite.** Déclare un service nommé
|
|
646
670
|
`realtimeBackplane` dans le conteneur d'injection de ton module. `RealtimeService.init()`
|
|
647
|
-
(`RealtimeService.ts
|
|
671
|
+
(`RealtimeService.ts#RealtimeService.init`) le lit et le branche **avant** que le registre soit consulté : une instance
|
|
648
672
|
présente court-circuite la sélection par nom.
|
|
649
673
|
|
|
650
674
|
> [!WARNING]
|
|
651
675
|
> Le builder accepte aussi une instance en second argument (`defineRealtimeConfig(config, {
|
|
652
676
|
backplane })`), et la documentation d'architecture présente cette voie. En pratique elle n'est pas
|
|
653
677
|
> atteignable depuis `nodefony.config.ts` : le module appelle le builder avec la seule configuration
|
|
654
|
-
> fusionnée (`src/packages/@nodefony/realtime/index.ts
|
|
678
|
+
> fusionnée (`src/packages/@nodefony/realtime/index.ts#Realtime.onKernelRegister`), et le schéma Zod est strict : une clé
|
|
655
679
|
> inconnue, dont une instance de classe posée dans `backplane`, fait échouer le boot. Pour brancher un objet déjà construit, utilise
|
|
656
680
|
> le service `realtimeBackplane`.
|
|
657
681
|
|
|
@@ -680,7 +704,7 @@ L'ordre de recouvrement, du plus faible au plus fort :
|
|
|
680
704
|
2. **La configuration de l'application** — ton `use("@nodefony/realtime", { … })`.
|
|
681
705
|
3. **`NF_REALTIME_DRIVER`**, **`NF_REALTIME_BACKPLANE_NAMESPACE`** et
|
|
682
706
|
**`NF_REALTIME_BACKPLANE_SECRET`** — posées par le Kernel après la fusion et **avant** la
|
|
683
|
-
validation Zod (`applyEnvShortcuts`, `Kernel.ts
|
|
707
|
+
validation Zod (`applyEnvShortcuts`, `Kernel.ts#Kernel.applyEnvConfigOverrides`) : une valeur fausse **arrête le démarrage** en nommant la
|
|
684
708
|
variable (un secret de moins de 32 caractères, un espace de noms qui contient `:`). Chacune
|
|
685
709
|
accepte aussi sa forme `<NOM>_FILE` (secret monté par Docker ou Kubernetes).
|
|
686
710
|
4. **`NF__REALTIME__<CHEMIN>`** — même moment, même validation. Un chemin introuvable produit un
|
|
@@ -712,10 +736,10 @@ NF_CLUSTER_PROBE=0 # couper la sonde de pod sur une réplique en incident
|
|
|
712
736
|
|
|
713
737
|
Deux fonctions publiques, utiles surtout aux tests et aux outils.
|
|
714
738
|
|
|
715
|
-
`defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts
|
|
739
|
+
`defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts#defineRealtimeConfig`) analyse et **gèle**. Appelé
|
|
716
740
|
hors Kernel (un test, un outil), il ne voit pas les variables d'environnement : c'est le Kernel
|
|
717
741
|
qui les pose. Le module l'appelle lui-même à l'enregistrement
|
|
718
|
-
(`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts
|
|
742
|
+
(`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts#Realtime.onKernelRegister`) : tu n'as pas à
|
|
719
743
|
l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
|
|
720
744
|
sans démarrer un serveur.
|
|
721
745
|
|
|
@@ -727,15 +751,15 @@ const cfg = defineRealtimeConfig({ backplane: { driver: "redis" } });
|
|
|
727
751
|
// Une valeur invalide ou une clé inconnue lève une BootConfigurationError, avec le chemin exact du champ fautif.
|
|
728
752
|
```
|
|
729
753
|
|
|
730
|
-
`realtimeConfigJsonSchema()` (`defineModuleConfig.ts
|
|
754
|
+
`realtimeConfigJsonSchema()` (`defineModuleConfig.ts#realtimeConfigJsonSchema`) produit le schéma JSON du module, chaque
|
|
731
755
|
champ portant sa description. C'est ce que consomme la page module de Studio pour afficher la
|
|
732
756
|
configuration attendue. L'instance de backplane éventuelle en est absente : une classe n'a rien à
|
|
733
757
|
faire dans un schéma sérialisable.
|
|
734
758
|
|
|
735
759
|
Un message d'erreur de validation est reformaté par le cœur avant d'être levé (`parseModuleConfig`,
|
|
736
760
|
appelé par `defineRealtimeConfig`) : chaque problème apparaît sous la forme `chemin : message`
|
|
737
|
-
(`src/nodefony/src/kernel/moduleConfig.ts
|
|
738
|
-
(`src/nodefony/src/kernel/moduleConfig.ts
|
|
761
|
+
(`src/nodefony/src/kernel/moduleConfig.ts#formatConfigIssue`), séparés par des points médians
|
|
762
|
+
(`src/nodefony/src/kernel/moduleConfig.ts#formatConfigIssues`). Tu lis quel champ est fautif, pas une trace Zod
|
|
739
763
|
brute.
|
|
740
764
|
|
|
741
765
|
## 📡 Observabilité — relire ce qui s'applique vraiment
|
|
@@ -758,16 +782,16 @@ Trois endroits, par ordre de fiabilité décroissante.
|
|
|
758
782
|
|
|
759
783
|
## ⚠️ Pièges
|
|
760
784
|
|
|
761
|
-
| Symptôme | Cause
|
|
762
|
-
| ----------------------------------------------------------------------------------- |
|
|
763
|
-
| Un message n'arrive qu'à la moitié des clients, sans aucune erreur | plusieurs répliques avec `driver: "loopback"`
|
|
764
|
-
| Driver `redis` configuré, journal `RealtimeHub reste local` | module `@nodefony/redis` absent du manifeste, ou connexions `publish`/`subscribe` indisponibles
|
|
765
|
-
| Le fan-out traverse, mais des messages d'un autre environnement arrivent | `backplane.namespace` non posé : deux déploiements de la même application dérivent le même canal
|
|
766
|
-
| Le démarrage échoue avec `clé inconnue : …` | clé absente du schéma strict (faute de frappe, ou clé retirée)
|
|
767
|
-
| Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde
|
|
768
|
-
| Toutes les connexions tombent en `4003` après activation du contrôle d'origine | `allowList` vide, ou origine non identique au caractère près (port, schéma, sous-domaine)
|
|
769
|
-
| `enabled: false` posé « pour désactiver », et la garde d'origine ne s'applique plus | le service devient inerte et ne pose plus les politiques sur le hub
|
|
770
|
-
| Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts
|
|
785
|
+
| Symptôme | Cause | Correction |
|
|
786
|
+
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
787
|
+
| Un message n'arrive qu'à la moitié des clients, sans aucune erreur | plusieurs répliques avec `driver: "loopback"` | passer à `redis` et déclarer `@nodefony/redis` dans le manifeste |
|
|
788
|
+
| Driver `redis` configuré, journal `RealtimeHub reste local` | module `@nodefony/redis` absent du manifeste, ou connexions `publish`/`subscribe` indisponibles | déclarer le module ; vérifier que Redis répond |
|
|
789
|
+
| Le fan-out traverse, mais des messages d'un autre environnement arrivent | `backplane.namespace` non posé : deux déploiements de la même application dérivent le même canal | poser un namespace explicite et distinct par environnement |
|
|
790
|
+
| Le démarrage échoue avec `clé inconnue : …` | clé absente du schéma strict (faute de frappe, ou clé retirée) | corriger ou retirer la clé que nomme le message |
|
|
791
|
+
| Baisser `slowConsumer.bytes` ne ferme pas les clients lents | cette clé ne pilote que le **comptage** de la sonde | régler `websocketSecure.maxBackpressure` / `.backpressureCloseAfterDrops` (@nodefony/http) |
|
|
792
|
+
| Toutes les connexions tombent en `4003` après activation du contrôle d'origine | `allowList` vide, ou origine non identique au caractère près (port, schéma, sous-domaine) | inscrire l'origine exacte ; un client non-navigateur relève de `allowMissingOrigin` |
|
|
793
|
+
| `enabled: false` posé « pour désactiver », et la garde d'origine ne s'applique plus | le service devient inerte et ne pose plus les politiques sur le hub | retirer le module du manifeste plutôt que l'éteindre |
|
|
794
|
+
| Le plafond de canaux semble ignoré en test | il ne l'est pas : le hub porte le même défaut de 256 sans configuration (`RealtimeHub.ts#RealtimeHub`) | vérifier le motif de refus `realtime:denied` côté client |
|
|
771
795
|
|
|
772
796
|
## 🧪 Tests & couverture
|
|
773
797
|
|
package/docs/cookbook-chat.md
CHANGED
|
@@ -134,7 +134,7 @@ le moment venu.
|
|
|
134
134
|
|
|
135
135
|
```bash
|
|
136
136
|
# 1. Créer l'application (profil complet : http, framework, realtime, sécurité, frontend)
|
|
137
|
-
|
|
137
|
+
npm create nodefony@beta mon-chat -- --preset complete
|
|
138
138
|
cd mon-chat
|
|
139
139
|
|
|
140
140
|
# 2. Créer le module qui portera le salon
|
|
@@ -781,20 +781,23 @@ couvre le HTTP **et** le WebSocket qu'elle recouvre — c'est le défaut, et c'e
|
|
|
781
781
|
zone protégée qui laisserait passer le WebSocket serait une porte dérobée.
|
|
782
782
|
|
|
783
783
|
```ts ignore
|
|
784
|
-
// nodefony
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
784
|
+
// nodefony/config/security.ts — (ajout de l'étape 5)
|
|
785
|
+
import type { ISecurityConfigInput } from "@nodefony/security";
|
|
786
|
+
|
|
787
|
+
export const securityConfig = () =>
|
|
788
|
+
({
|
|
789
|
+
areas: {
|
|
790
|
+
chat: {
|
|
791
|
+
pattern: "^/chat",
|
|
792
|
+
// Cookie de session (BFF) : le navigateur n'a aucun jeton à manipuler,
|
|
793
|
+
// et le cookie voyage naturellement avec la poignée de main WebSocket.
|
|
794
|
+
authenticators: ["session"],
|
|
795
|
+
},
|
|
792
796
|
},
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
});
|
|
797
|
+
roleHierarchy: {
|
|
798
|
+
ROLE_ADMIN: ["ROLE_USER"],
|
|
799
|
+
},
|
|
800
|
+
}) satisfies ISecurityConfigInput;
|
|
798
801
|
```
|
|
799
802
|
|
|
800
803
|
> [!NOTE]
|
|
@@ -1603,23 +1606,23 @@ Pour qui veut lire l'implémentation derrière chaque étape :
|
|
|
1603
1606
|
<!-- prettier-ignore -->
|
|
1604
1607
|
| Ce que tu as utilisé | Où c'est écrit |
|
|
1605
1608
|
| --- | --- |
|
|
1606
|
-
| Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts
|
|
1607
|
-
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts
|
|
1608
|
-
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts
|
|
1609
|
-
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts
|
|
1610
|
-
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts
|
|
1611
|
-
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts
|
|
1612
|
-
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts
|
|
1613
|
-
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts
|
|
1614
|
-
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts
|
|
1615
|
-
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts
|
|
1616
|
-
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts
|
|
1617
|
-
| Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts
|
|
1618
|
-
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts
|
|
1619
|
-
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts
|
|
1620
|
-
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts
|
|
1621
|
-
| Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts
|
|
1622
|
-
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts
|
|
1623
|
-
| Les hooks React | `useNodefony()` (`client/react/index.ts
|
|
1624
|
-
| Le client navigateur | `NodefonySocket.connect()` (`NodefonySocket.ts
|
|
1625
|
-
| L'upload | `UploadedFile` (`routerDecorators.ts
|
|
1609
|
+
| Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts#RealtimeAction`), `RealtimeChannel()` (`realtimeDecorators.ts#RealtimeChannel`), `RealtimeInbound()` (`realtimeDecorators.ts#RealtimeInbound`) |
|
|
1610
|
+
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts#RealtimeController.handleRealtime`) |
|
|
1611
|
+
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts#RealtimeController.realtimeApiRequest`) |
|
|
1612
|
+
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts#RealtimeHub.subscribe`), `RealtimeController.startChannel()` (`RealtimeController.ts#RealtimeController.startChannel`) |
|
|
1613
|
+
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts#RealtimeHub.publish`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts#RealtimeHub.publishLocal`) |
|
|
1614
|
+
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts#RealtimeBroadcast`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts#RealtimeHub.markBroadcastChannel`) |
|
|
1615
|
+
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts#RealtimeHub.setBackplane`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts#registerBackplaneDriver`), cloison `resolveBackplaneOriginId()` (`originId.ts#resolveBackplaneOriginId`) |
|
|
1616
|
+
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts#IChannelPolicy`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts#RealtimeHub.registerChannelPolicy`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts#RealtimeHub.hasUnenforcedChannelPolicies`) |
|
|
1617
|
+
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts#Firewall.#wireRealtime`) |
|
|
1618
|
+
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts#IRealtimeDenied`), erreur `unauthorized` (`JsonRpcPeer.ts#JsonRpcPeer.receive`), fermetures `origin not allowed` (`RealtimeController.ts#RealtimeController.notifyClient`) et `unauthorized` (`RealtimeController.ts#RealtimeController.onHandshake`) |
|
|
1619
|
+
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts#RealtimeHub.revalidateRevocable`) |
|
|
1620
|
+
| Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts#SLOW_CONSUMER_BYTES`), fermeture `slow consumer` (`RealtimeHub.ts#SLOW_CONSUMER_BYTES`) |
|
|
1621
|
+
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts#RealtimeHub.maxChannelsPerConnection`) |
|
|
1622
|
+
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts#IRealtimeWelcome`) |
|
|
1623
|
+
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts#RealtimeHub.probe`), `IRealtimeProbe` (`IRealtimeProbe.ts#IRealtimeProbe`), `IRealtimeChannelStat` (`IRealtimeProbe.ts#IRealtimeChannelStat`) |
|
|
1624
|
+
| Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts#createRealtimeAdminApi`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts#buildRealtimeHealth`), `buildOwnHealth()` (`RealtimeAdminApi.ts#buildOwnHealth`) |
|
|
1625
|
+
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts#RealtimeService.publish`), `RealtimeService.probe()` (`RealtimeService.ts#RealtimeService.probe`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts#RealtimeService.markBroadcastChannel`) |
|
|
1626
|
+
| Les hooks React | `useNodefony()` (`client/react/index.ts#useNodefony`), `useNodefonyState()` (`client/react/index.ts#useNodefonyState`), `useNodefonyIdentity()` (`client/react/index.ts#useNodefonyIdentity`), `useNodefonyChannel()` (`client/react/index.ts#useNodefonyChannel`), `useNodefonyChannelData()` (`client/react/index.ts#useNodefonyChannelData`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts#useNodefonyAdaptiveChannel`) |
|
|
1627
|
+
| Le client navigateur | `NodefonySocket.connect()` (`NodefonySocket.ts#NodefonySocket.connect`), `NodefonySocket.subscribe()` (`NodefonySocket.ts#NodefonySocket.subscribe`), `NodefonySocket.request()` (`NodefonySocket.ts#NodefonySocket.request`), `NodefonySocket.onDenied()` (`NodefonySocket.ts#NodefonySocket.onDenied`) |
|
|
1628
|
+
| L'upload | `UploadedFile` (`routerDecorators.ts#UploadedFile`), `IUploadedFile` (`IUpload.ts#IUploadedFile`), `files` (`Request.ts#HttpRequest.files`) |
|