@nodefony/realtime 10.0.0-alpha.8 → 10.0.0-beta.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/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/src/backplane/envelope.js +2 -1
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +1 -1
- package/dist/nodefony/src/server/RealtimeController.js +52 -11
- package/dist/nodefony/src/server/RealtimeHub.js +14 -12
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +9 -7
- package/dist/nodefony/src/service/RealtimeService.js +7 -6
- package/dist/nodefony/testing/index.js +4 -2
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +3 -3
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +1 -1
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +1 -1
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +27 -0
- package/dist/types/nodefony/testing/index.d.ts +2 -2
- package/docs/actions.md +14 -13
- package/docs/architecture.md +35 -35
- package/docs/configuration.md +12 -11
- package/docs/cookbook-chat.md +18 -18
- package/docs/index.md +5 -8
- package/docs/observabilite.md +11 -13
- package/docs/protocole.md +28 -26
- package/docs/securite.md +54 -54
- package/docs/vocabulaire.md +35 -35
- package/package.json +12 -13
package/docs/architecture.md
CHANGED
|
@@ -126,7 +126,7 @@ Nodefony étiquette avec **JSON-RPC 2.0**, une norme publique plutôt qu'un form
|
|
|
126
126
|
Le point remarquable : le **même** moteur de protocole tourne des deux côtés du fil. La
|
|
127
127
|
classe `JsonRpcPeer` (`JsonRpcPeer.ts:271`) est du code isomorphe du cœur — le navigateur
|
|
128
128
|
l'exécute dans `RealtimeClient`, le serveur l'instancie une fois par connexion dans
|
|
129
|
-
`RealtimeController.onHandshake()` (`RealtimeController.ts:
|
|
129
|
+
`RealtimeController.onHandshake()` (`RealtimeController.ts:374`). Le serveur peut donc
|
|
130
130
|
appeler le client, pas seulement l'inverse : c'est du vrai duplex, pas un aller-retour
|
|
131
131
|
déguisé.
|
|
132
132
|
|
|
@@ -141,11 +141,11 @@ Trois partis pris distinguent cette pile d'un simple « serveur WebSocket ».
|
|
|
141
141
|
et explicite : `ServerRealtimeSocket.request()` (`ServerRealtimeSocket.ts:131`) **rejette
|
|
142
142
|
toujours** — un handle posé sur le hub n'a pas d'interlocuteur unique, puisque le hub est
|
|
143
143
|
multi-clients. Pour un appel serveur→client ciblé, on passe par la connexion :
|
|
144
|
-
`RealtimeController.requestClient()` (`RealtimeController.ts:
|
|
144
|
+
`RealtimeController.requestClient()` (`RealtimeController.ts:329`).
|
|
145
145
|
|
|
146
146
|
**Un provider par canal, pas un par client.** Si mille onglets s'abonnent au même canal de
|
|
147
147
|
santé, le calcul ne doit tourner qu'une fois. Le hub crée le producteur au **premier**
|
|
148
|
-
abonné et le détruit au **dernier** (`RealtimeHub.subscribe()`, `RealtimeHub.ts:
|
|
148
|
+
abonné et le détruit au **dernier** (`RealtimeHub.subscribe()`, `RealtimeHub.ts:462`) : le
|
|
149
149
|
coût suit le nombre de canaux, pas le nombre de clients.
|
|
150
150
|
|
|
151
151
|
**Le cluster est un détail de configuration.** Ni ton contrôleur ni ton client ne savent
|
|
@@ -325,7 +325,7 @@ La pile se lit de haut en bas ; chaque étage ne connaît que son voisin du dess
|
|
|
325
325
|
| Protocole | `JsonRpcPeer` (`JsonRpcPeer.ts:271`) | étiqueter, corréler, refuser une frame | par où passent les octets |
|
|
326
326
|
| Transport | `WsConnectionTransport` (`WsConnectionTransport.ts:46`) | déplacer des octets, mesurer la file | ce que veut dire un message |
|
|
327
327
|
| Hub | `RealtimeHub` (`RealtimeHub.ts:213`) | table des abonnés locaux + fan-out | qu'il existe d'autres process |
|
|
328
|
-
| Backplane | `IBackplane` (`IBackplane.ts:
|
|
328
|
+
| Backplane | `IBackplane` (`IBackplane.ts:107`) | porter un message aux autres process | ce qu'est un abonné |
|
|
329
329
|
|
|
330
330
|
> [!NOTE]
|
|
331
331
|
> Une part de la documentation historique décrit cette pile en « 5 étages » en comptant
|
|
@@ -338,7 +338,7 @@ La pile se lit de haut en bas ; chaque étage ne connaît que son voisin du dess
|
|
|
338
338
|
dispatch des notifications, gestion des erreurs, `dispose()` propre. Deux points
|
|
339
339
|
d'interception y sont posés :
|
|
340
340
|
|
|
341
|
-
- `beforeDispatch` (`JsonRpcPeer.ts:
|
|
341
|
+
- `beforeDispatch` (`JsonRpcPeer.ts:192`) — un verrou **synchrone** appelé avant tout
|
|
342
342
|
traitement de frame. Il rend `true`/`false`. Un refus sur une requête produit
|
|
343
343
|
`-32001 unauthorized` ; sur une notification, la frame est jetée
|
|
344
344
|
(`JsonRpcPeer.ts:413`).
|
|
@@ -355,14 +355,14 @@ au handshake, puis lue en O(1). C'est le compromis assumé, documenté dans
|
|
|
355
355
|
Particularité contre-intuitive : côté serveur, le transport **n'écoute pas** d'événement
|
|
356
356
|
socket. C'est le pipeline HTTP de `@nodefony/http` qui appelle la route WebSocket du
|
|
357
357
|
contrôleur à chaque message reçu, laquelle pousse la charge dans
|
|
358
|
-
`WsConnectionTransport.feed()` (`WsConnectionTransport.ts:
|
|
358
|
+
`WsConnectionTransport.feed()` (`WsConnectionTransport.ts:150`). La fermeture arrive de la
|
|
359
359
|
même façon, par le hook `onFinish` du contexte, qui déclenche `fireClose()`
|
|
360
|
-
(`WsConnectionTransport.ts:
|
|
360
|
+
(`WsConnectionTransport.ts:155`).
|
|
361
361
|
|
|
362
362
|
Ce transport porte aussi la **back-pressure**. Elle n'est pas câblée en dur : les trois
|
|
363
363
|
leviers sont des clés de configuration du serveur WebSocket, lues par le transport
|
|
364
364
|
(`WsConnectionTransport.ts:63-75`) et documentées dans `@nodefony/http`
|
|
365
|
-
(`http/nodefony/config/config.ts:
|
|
365
|
+
(`http/nodefony/config/config.ts:1044`).
|
|
366
366
|
|
|
367
367
|
| File non drainée (`bufferedAmount`) | Décision | Réglage (défaut) |
|
|
368
368
|
| --------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
@@ -388,7 +388,7 @@ Le schéma ci-dessous rend ces étages vivants : active le temps réel et il res
|
|
|
388
388
|
|
|
389
389
|
## 🔌 Le cycle de vie d'une connexion
|
|
390
390
|
|
|
391
|
-
Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:
|
|
391
|
+
Tout se joue dans `RealtimeController.onHandshake()` (`RealtimeController.ts:374`), appelé
|
|
392
392
|
une seule fois par connexion, en chemin froid.
|
|
393
393
|
|
|
394
394
|
```mermaid
|
|
@@ -419,19 +419,19 @@ sequenceDiagram
|
|
|
419
419
|
|
|
420
420
|
Les étapes, dans l'ordre exact du code :
|
|
421
421
|
|
|
422
|
-
1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:
|
|
422
|
+
1. **Contrôle de l'origine** — `RealtimeHub.checkOrigin()` (`RealtimeHub.ts:986`). Refus →
|
|
423
423
|
fermeture `4003`. La politique vient de la configuration ; sans politique, tout passe.
|
|
424
424
|
2. **Résolution de l'identité** — `RealtimeHub.resolveAuthenticator()`
|
|
425
|
-
(`RealtimeHub.ts:
|
|
425
|
+
(`RealtimeHub.ts:961`) parcourt les authentificateurs enregistrés : **le premier motif
|
|
426
426
|
qui correspond capture**. Aucun ne correspond ? Le jeton anonyme gelé est posé
|
|
427
427
|
(`ANONYMOUS_REALTIME_TOKEN`) — la lecture d'identité ne rend donc jamais `null`. Un
|
|
428
428
|
échec d'authentification ferme en `4001`.
|
|
429
429
|
3. **Création du peer et du transport**, puis association `peer → jeton`
|
|
430
|
-
(`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:
|
|
430
|
+
(`RealtimeHub.setTokenForPeer()`, `RealtimeHub.ts:1018`), stockée dans une `WeakMap` : le
|
|
431
431
|
jeton disparaît avec le peer, sans fuite.
|
|
432
432
|
4. **Enregistrement des actions** — celles des décorateurs `@RealtimeAction`, puis celles
|
|
433
433
|
de la surcharge `realtimeActions()`, qui gagne en cas de conflit. Le pont API
|
|
434
|
-
`api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts:
|
|
434
|
+
`api.request` n'est ajouté que si `realtimeApiRequest()` (`RealtimeController.ts:276`)
|
|
435
435
|
rend `true`.
|
|
436
436
|
5. **Inscription aux registres** : sonde de connexion, révocation périodique si le jeton
|
|
437
437
|
est révocable, préfixes de canaux broadcast, canaux entrants.
|
|
@@ -439,7 +439,7 @@ Les étapes, dans l'ordre exact du code :
|
|
|
439
439
|
l'identité résolue (type, authentifié ou non, rôles, portées). Le client sait **qui il
|
|
440
440
|
est** sans appeler la moindre route.
|
|
441
441
|
|
|
442
|
-
À la fermeture, un unique `onFinish` (`RealtimeController.ts:
|
|
442
|
+
À la fermeture, un unique `onFinish` (`RealtimeController.ts:715`) fait le ménage complet :
|
|
443
443
|
désabonnement de chaque canal tenu, retrait des deux registres, `fireClose()` du transport,
|
|
444
444
|
`dispose()` du peer. C'est ce qui garantit qu'aucun minuteur ni écouteur ne survit à une
|
|
445
445
|
déconnexion.
|
|
@@ -459,26 +459,26 @@ déconnexion.
|
|
|
459
459
|
La seconde mérite une explication. Le verrou de frame est synchrone et lit une identité
|
|
460
460
|
figée au handshake : il ne peut donc pas voir une session qui meurt en cours de route (une
|
|
461
461
|
déconnexion HTTP, par exemple). Un minuteur — démarré au premier inscrit, arrêté dès que le
|
|
462
|
-
registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:
|
|
462
|
+
registre se vide (`RealtimeHub.registerRevocable()`, `RealtimeHub.ts:794`) — relit
|
|
463
463
|
périodiquement ces identités et coupe les sockets orphelines. Seules les identités
|
|
464
464
|
**révocables** y entrent : un visiteur anonyme ne coûte rien.
|
|
465
465
|
|
|
466
466
|
## Le hub — canaux partagés et fan-out
|
|
467
467
|
|
|
468
|
-
Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:
|
|
468
|
+
Le hub est un singleton par process (`getRealtimeHub()`, `RealtimeHub.ts:1451`). Il ne
|
|
469
469
|
dépend de rien : ce sont les fabriques fournies par les contrôleurs qui portent les
|
|
470
470
|
dépendances.
|
|
471
471
|
|
|
472
472
|
### Le provider partagé — un ticker, pas mille
|
|
473
473
|
|
|
474
|
-
`RealtimeHub.subscribe()` (`RealtimeHub.ts:
|
|
474
|
+
`RealtimeHub.subscribe()` (`RealtimeHub.ts:462`) applique une mécanique en trois temps :
|
|
475
475
|
|
|
476
476
|
1. Le canal existe déjà ? On ajoute simplement le sink de cette connexion. Fin.
|
|
477
477
|
2. Sinon, on inscrit le sink **avant** d'appeler la fabrique — de sorte que le tout premier
|
|
478
478
|
paquet du producteur atteigne bien ce premier abonné.
|
|
479
479
|
3. La fabrique du contrôleur rend `null` (canal inconnu de lui) ? Dernier recours : le
|
|
480
480
|
registre des **canaux système** (`RealtimeHub.registerSystemChannel()`,
|
|
481
|
-
`RealtimeHub.ts:
|
|
481
|
+
`RealtimeHub.ts:1297`), qu'un module bas niveau alimente au démarrage. Toujours `null` →
|
|
482
482
|
l'abonnement est refusé et rien n'est alloué.
|
|
483
483
|
|
|
484
484
|
Au dernier désabonnement, `RealtimeHub.unsubscribe()` (`RealtimeHub.ts:576`) appelle le
|
|
@@ -511,7 +511,7 @@ une connexion fautive n'interrompt pas la diffusion aux autres.
|
|
|
511
511
|
|
|
512
512
|
Par défaut, **aucun canal ne traverse le backplane**. Il faut déclarer un préfixe, via
|
|
513
513
|
`@RealtimeBroadcast` sur ton contrôleur (`realtimeDecorators.ts:342`) ou
|
|
514
|
-
directement `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:
|
|
514
|
+
directement `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:668`).
|
|
515
515
|
|
|
516
516
|
Trois raisons à ce choix, qui prend à contre-pied la plupart des bibliothèques temps réel :
|
|
517
517
|
|
|
@@ -547,17 +547,17 @@ coûte chaque abonné supplémentaire.
|
|
|
547
547
|
|
|
548
548
|
### Le contrat, en cinq méthodes
|
|
549
549
|
|
|
550
|
-
`IBackplane` (`IBackplane.ts:
|
|
550
|
+
`IBackplane` (`IBackplane.ts:107`) est délibérément minuscule : le backplane ne connaît ni
|
|
551
551
|
les abonnés ni les canaux logiques. Tout l'état vit dans le hub.
|
|
552
552
|
|
|
553
|
-
| Membre
|
|
554
|
-
|
|
|
555
|
-
| `originId` (`IBackplane.ts:
|
|
556
|
-
| `start()` (`IBackplane.ts:115`)
|
|
557
|
-
| `publish(channel, payload)` (`IBackplane.ts:
|
|
558
|
-
| `onMessage(handler)` (`IBackplane.ts:127`)
|
|
559
|
-
| `stop()` (`IBackplane.ts:130`)
|
|
560
|
-
| `describe()` (`IBackplane.ts:136`)
|
|
553
|
+
| Membre | Rôle |
|
|
554
|
+
| ------------------------------------------------- | --------------------------------------------------------------- |
|
|
555
|
+
| `originId` (`IBackplane.ts:109`) | l'étiquette de CE process, lue par l'anti-écho |
|
|
556
|
+
| `start()` (`IBackplane.ts:115`) | ouvrir le transport. Idempotent, synchrone ou asynchrone |
|
|
557
|
+
| `publish(channel, payload)` (`IBackplane.ts:121`) | propager aux **autres** pairs. Ne refait pas le fan-out local |
|
|
558
|
+
| `onMessage(handler)` (`IBackplane.ts:127`) | recevoir des pairs. Un seul gestionnaire à la fois |
|
|
559
|
+
| `stop()` (`IBackplane.ts:130`) | libérer connexions et écouteurs. Idempotent |
|
|
560
|
+
| `describe()` (`IBackplane.ts:136`) | la carte d'identité : driver, nature, origine, cross-pod, canal |
|
|
561
561
|
|
|
562
562
|
Deux absences volontaires. Il n'y a **pas** de `subscribe`/`unsubscribe` par canal : le
|
|
563
563
|
transport porte **un seul** canal physique, et les canaux logiques voyagent **dans**
|
|
@@ -620,7 +620,7 @@ et non une :
|
|
|
620
620
|
|
|
621
621
|
1. **Côté hub** : l'arrivée passe par `publishLocal`, jamais par `publish`. Rien ne repart.
|
|
622
622
|
2. **Côté backplane** : à la réception, on compare l'`originId` de l'enveloppe au sien et
|
|
623
|
-
on jette si c'est le même (`RedisBackplane.ts:
|
|
623
|
+
on jette si c'est le même (`RedisBackplane.ts:209`, `ClusterBackplane.ts:134`).
|
|
624
624
|
|
|
625
625
|
L'étiquette elle-même est calculée par `resolveBackplaneOriginId()` (`originId.ts:24`), et
|
|
626
626
|
sa recette mérite qu'on s'y arrête :
|
|
@@ -661,7 +661,7 @@ est ignoré, pas propagé (`RedisBackplane.ts:212`).
|
|
|
661
661
|
### Le démarrage, et ce qui se passe quand ça rate
|
|
662
662
|
|
|
663
663
|
Le module valide sa configuration par Zod à l'enregistrement
|
|
664
|
-
(`src/packages/@nodefony/realtime/index.ts:
|
|
664
|
+
(`src/packages/@nodefony/realtime/index.ts:219`), puis câble tout au démarrage du noyau
|
|
665
665
|
(`src/packages/@nodefony/realtime/index.ts:213`) — après l'initialisation des services (le
|
|
666
666
|
service Redis a donc ouvert ses connexions) et avant le trafic.
|
|
667
667
|
|
|
@@ -743,19 +743,19 @@ Deux propriétés architecturales méritent d'être notées ici, parce qu'elles
|
|
|
743
743
|
choix de conception visibles partout dans le module :
|
|
744
744
|
|
|
745
745
|
**Coût nul quand la sécurité est absente.** Le verrou de frame n'est branché sur le peer que
|
|
746
|
-
si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:
|
|
746
|
+
si une politique existe (`RealtimeHub.hasFrameAuthorizer()`, `RealtimeHub.ts:1058`, testé une
|
|
747
747
|
fois au handshake). Sans module de sécurité, `beforeDispatch` reste indéfini et le chemin
|
|
748
748
|
chaud ne paie **rien** du tout.
|
|
749
749
|
|
|
750
750
|
**Échec bruyant plutôt que faux sentiment de sécurité.** Si des canaux déclarent une
|
|
751
751
|
politique sans qu'aucun décideur ne soit câblé, `hasUnenforcedChannelPolicies()`
|
|
752
|
-
(`RealtimeHub.ts:
|
|
752
|
+
(`RealtimeHub.ts:1117`) le détecte et un avertissement est émis une fois par process
|
|
753
753
|
(`RealtimeController.ts:487`). Un canal qui **se croit** gardé alors qu'il est ouvert est
|
|
754
754
|
bien plus dangereux qu'un canal ouvertement public.
|
|
755
755
|
|
|
756
756
|
## 📡 Observabilité — la sonde et Studio
|
|
757
757
|
|
|
758
|
-
`RealtimeHub.probe()` (`RealtimeHub.ts:
|
|
758
|
+
`RealtimeHub.probe()` (`RealtimeHub.ts:865`) rend un instantané en **lecture pure** : aucune
|
|
759
759
|
allocation sur le chemin chaud, jamais d'exception. Les compteurs sont des primitives
|
|
760
760
|
incrémentées en O(1) ; ils sont **monotones**, ce qui laisse au lecteur le soin de dériver
|
|
761
761
|
un débit.
|
|
@@ -774,7 +774,7 @@ process, plus — si elles sont disponibles — les sondes ORM et les compteurs
|
|
|
774
774
|
chemins de lecture, un seul producteur :
|
|
775
775
|
|
|
776
776
|
- **HTTP** : `GET /nodefony/realtime/api/health`, exposé par `createRealtimeAdminApi()`
|
|
777
|
-
(`RealtimeAdminApi.ts:
|
|
777
|
+
(`RealtimeAdminApi.ts:98`) ;
|
|
778
778
|
- **Canal temps réel** : `nodefony:socket`, poussé par un ticker.
|
|
779
779
|
|
|
780
780
|
### En cluster : la vue « pod » plutôt que « worker »
|
|
@@ -808,7 +808,7 @@ révocables, authentificateurs, politiques de canal, association peer→jeton :
|
|
|
808
808
|
`null`. Un process sans abonné n'alloue **rien** (`RealtimeHub.ts:141` et suivants).
|
|
809
809
|
|
|
810
810
|
**Aucun minuteur au repos.** Le tick de révocation démarre au premier inscrit et s'arrête
|
|
811
|
-
dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:
|
|
811
|
+
dès que le registre se vide — `RealtimeHub.unregisterRevocable()` (`RealtimeHub.ts:809`).
|
|
812
812
|
Les minuteurs sont détachés de la boucle d'événements : ils ne retiennent jamais l'arrêt du
|
|
813
813
|
process.
|
|
814
814
|
|
package/docs/configuration.md
CHANGED
|
@@ -125,7 +125,7 @@ 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:259`) 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
130
|
(`realtimeConfigSchema.parse({})`, `config.ts:231`) : 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
|
|
@@ -252,7 +252,7 @@ regarder quand un message ne traverse pas.
|
|
|
252
252
|
|
|
253
253
|
> [!TIP]
|
|
254
254
|
> Ces quatre lignes viennent du même endroit du code (`Realtime.#wireBackplane()`,
|
|
255
|
-
> `src/packages/@nodefony/realtime/index.ts:
|
|
255
|
+
> `src/packages/@nodefony/realtime/index.ts:279`) et de la carte d'identité que chaque driver publie.
|
|
256
256
|
> Aucun repli n'est silencieux : si le fan-out est dégradé, c'est écrit.
|
|
257
257
|
|
|
258
258
|
## ⚙️ Le schéma, clé par clé
|
|
@@ -283,7 +283,7 @@ valeurs sont des constantes du code, pas des clés.
|
|
|
283
283
|
|
|
284
284
|
Poser `false` charge le module sans rien activer. Concrètement : pas d'API d'administration
|
|
285
285
|
enregistrée, pas de backplane câblé, pas de sonde de pod
|
|
286
|
-
(`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:
|
|
286
|
+
(`Realtime.onKernelBoot()`, `src/packages/@nodefony/realtime/index.ts:239`), et le service devient
|
|
287
287
|
inerte (`RealtimeService.init()`, `RealtimeService.ts:77`).
|
|
288
288
|
|
|
289
289
|
> [!WARNING]
|
|
@@ -344,7 +344,7 @@ canal exotique.
|
|
|
344
344
|
Ne concerne, là aussi, que les transports **partagés**. Un bus pub/sub ne dit pas _qui_ a publié :
|
|
345
345
|
sans secret, tout ce qui atterrit sur le canal — venu d'un pod ou d'un tiers qui sait écrire dans ce
|
|
346
346
|
Redis — est traité comme un message légitime. Poser un secret fait porter à chaque message un sceau
|
|
347
|
-
HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts:
|
|
347
|
+
HMAC-SHA256, vérifié à l'arrivée (`openBackplaneEnvelope()`, `envelope.ts:112`) : un message non
|
|
348
348
|
scellé, altéré ou repointé vers un autre canal est **ignoré**, sans exception possible.
|
|
349
349
|
|
|
350
350
|
Le secret doit être **identique sur tous les pods** — c'est lui qui les reconnaît entre eux. En
|
|
@@ -378,7 +378,7 @@ d'éteindre une sonde sur un pod en incident sans redéployer une configuration.
|
|
|
378
378
|
### `slowConsumer.bytes` — un compteur, pas un frein
|
|
379
379
|
|
|
380
380
|
Seuil de `bufferedAmount` — les octets en attente d'envoi sur une socket — au-delà duquel la sonde
|
|
381
|
-
compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:
|
|
381
|
+
compte la connexion comme « lente » (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
|
|
382
382
|
|
|
383
383
|
> [!IMPORTANT]
|
|
384
384
|
> **Cette clé observe, elle n'agit pas.** Elle ne change que le compteur `slowConsumers` de la
|
|
@@ -434,7 +434,7 @@ des canaux d'état, où seul le dernier instantané compte.
|
|
|
434
434
|
Chaque canal ouvert coûte un producteur côté hub, un minuteur le plus souvent, et une entrée de table
|
|
435
435
|
côté connexion. Sans borne, **un seul** client peut s'abonner jusqu'à épuiser la mémoire du
|
|
436
436
|
processus. Le plafond est vérifié à chaque abonnement (`RealtimeController.startChannel()`,
|
|
437
|
-
`RealtimeController.ts:
|
|
437
|
+
`RealtimeController.ts:815`).
|
|
438
438
|
|
|
439
439
|
Quatre propriétés qui décident du bon réglage :
|
|
440
440
|
|
|
@@ -457,7 +457,7 @@ les clients — un tableau de bord interne qui compose des dizaines de flux, par
|
|
|
457
457
|
### `csrf.checkOrigin` — qui a le droit d'ouvrir la socket
|
|
458
458
|
|
|
459
459
|
Trois clés qui forment une seule politique, appliquée à l'ouverture de la connexion
|
|
460
|
-
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:
|
|
460
|
+
(`RealtimeHub.checkOrigin()`, `RealtimeHub.ts:986`). Une origine refusée ferme la socket avec le code
|
|
461
461
|
`4003`.
|
|
462
462
|
|
|
463
463
|
Le défaut est **désactivé**. C'est le seul défaut du module qui n'est pas le réglage recommandé :
|
|
@@ -705,7 +705,7 @@ Deux fonctions publiques, utiles surtout aux tests et aux outils.
|
|
|
705
705
|
|
|
706
706
|
`defineRealtimeConfig(config?, options?)` (`defineModuleConfig.ts:35`) analyse, applique la variable
|
|
707
707
|
de driver et **gèle**. Le module l'appelle lui-même à l'enregistrement
|
|
708
|
-
(`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:
|
|
708
|
+
(`Realtime.onKernelRegister()`, `src/packages/@nodefony/realtime/index.ts:213`) : tu n'as pas à
|
|
709
709
|
l'invoquer dans une application. En revanche, c'est l'outil qui permet de vérifier une configuration
|
|
710
710
|
sans démarrer un serveur.
|
|
711
711
|
|
|
@@ -722,9 +722,10 @@ champ portant sa description. C'est ce que consomme la page module de Studio pou
|
|
|
722
722
|
configuration attendue. L'instance de backplane éventuelle en est absente : une classe n'a rien à
|
|
723
723
|
faire dans un schéma sérialisable.
|
|
724
724
|
|
|
725
|
-
Un message d'erreur de validation est reformaté par le
|
|
726
|
-
apparaît sous la forme `chemin: message
|
|
727
|
-
(`src/
|
|
725
|
+
Un message d'erreur de validation est reformaté par le cœur avant d'être levé (`parseModuleConfig`,
|
|
726
|
+
appelé par `defineRealtimeConfig`) : chaque problème apparaît sous la forme `chemin : message`
|
|
727
|
+
(`src/nodefony/src/kernel/moduleConfig.ts:42`), séparés par des points médians
|
|
728
|
+
(`src/nodefony/src/kernel/moduleConfig.ts:58`). Tu lis quel champ est fautif, pas une trace Zod
|
|
728
729
|
brute.
|
|
729
730
|
|
|
730
731
|
## 📡 Observabilité — relire ce qui s'applique vraiment
|
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
|
-
npx nodefony create app mon-chat --complete
|
|
137
|
+
npx nodefony create app mon-chat --preset complete
|
|
138
138
|
cd mon-chat
|
|
139
139
|
|
|
140
140
|
# 2. Créer le module qui portera le salon
|
|
@@ -1597,23 +1597,23 @@ Pour qui veut lire l'implémentation derrière chaque étape :
|
|
|
1597
1597
|
<!-- prettier-ignore -->
|
|
1598
1598
|
| Ce que tu as utilisé | Où c'est écrit |
|
|
1599
1599
|
| --- | --- |
|
|
1600
|
-
| Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:
|
|
1601
|
-
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:
|
|
1602
|
-
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:
|
|
1603
|
-
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:
|
|
1604
|
-
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:
|
|
1605
|
-
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:
|
|
1600
|
+
| Les trois décorateurs | `RealtimeAction()` (`realtimeDecorators.ts:101`), `RealtimeChannel()` (`realtimeDecorators.ts:192`), `RealtimeInbound()` (`realtimeDecorators.ts:231`) |
|
|
1601
|
+
| La porte WebSocket | `RealtimeController.handleRealtime()` (`RealtimeController.ts:299`) |
|
|
1602
|
+
| Le pont HTTP ⇄ WebSocket | `RealtimeController.realtimeApiRequest()` (`RealtimeController.ts:276`) |
|
|
1603
|
+
| L'abonnement et son cycle de vie | `RealtimeHub.subscribe()` (`RealtimeHub.ts:462`), `RealtimeController.startChannel()` (`RealtimeController.ts:815`) |
|
|
1604
|
+
| La publication et le fan-out | `RealtimeHub.publish()` (`RealtimeHub.ts:604`), réinjection locale `RealtimeHub.publishLocal()` (`RealtimeHub.ts:623`) |
|
|
1605
|
+
| Les canaux diffusables | `RealtimeBroadcast` (`realtimeDecorators.ts:342`), `RealtimeHub.markBroadcastChannel()` (`RealtimeHub.ts:668`) |
|
|
1606
1606
|
| Le branchement du backplane | `RealtimeHub.setBackplane()` (`RealtimeHub.ts:714`), registre `registerBackplaneDriver()` (`backplaneRegistry.ts:55`), cloison `resolveBackplaneOriginId()` (`originId.ts:24`) |
|
|
1607
|
-
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:
|
|
1608
|
-
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:
|
|
1609
|
-
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:
|
|
1610
|
-
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:
|
|
1607
|
+
| Les politiques de canal | `IChannelPolicy` (`IChannelPolicy.ts:20`), `RealtimeHub.registerChannelPolicy()` (`RealtimeHub.ts:1143`), garde-fou `RealtimeHub.hasUnenforcedChannelPolicies()` (`RealtimeHub.ts:1117`) |
|
|
1608
|
+
| Le branchement automatique du firewall | `Firewall.#wireRealtime()` (`firewall.ts:279`) |
|
|
1609
|
+
| Les refus | `IRealtimeDenied` (`RealtimeEventMap.ts:269`), erreur `unauthorized` (`JsonRpcPeer.ts:417`), fermetures `origin not allowed` (`RealtimeController.ts:327`) et `unauthorized` (`RealtimeController.ts:397`) |
|
|
1610
|
+
| La révocation en cours de session | `RealtimeHub.revalidateRevocable()` (`RealtimeHub.ts:826`) |
|
|
1611
1611
|
| Le consommateur lent | seuil `SLOW_CONSUMER_BYTES` (`RealtimeHub.ts:56`), fermeture `slow consumer` (`RealtimeHub.ts:63`) |
|
|
1612
|
-
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:
|
|
1612
|
+
| Le plafond de canaux | `RealtimeHub.maxChannelsPerConnection` (`RealtimeHub.ts:1000`) |
|
|
1613
1613
|
| L'accueil et l'identité | `IRealtimeWelcome` (`RealtimeEventMap.ts:231`) |
|
|
1614
|
-
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:
|
|
1615
|
-
| Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:
|
|
1616
|
-
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:
|
|
1617
|
-
| Les hooks React | `useNodefony()` (`client/react/index.ts:
|
|
1618
|
-
| Le client navigateur | `RealtimeClient.connect()` (`RealtimeClient.ts:
|
|
1619
|
-
| L'upload | `UploadedFile` (`routerDecorators.ts:
|
|
1614
|
+
| La sonde | `RealtimeHub.probe()` (`RealtimeHub.ts:865`), `IRealtimeProbe` (`IRealtimeProbe.ts:61`), `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`) |
|
|
1615
|
+
| Le point de mesure d'administration | `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`), `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`), `buildOwnHealth()` (`RealtimeAdminApi.ts:52`) |
|
|
1616
|
+
| Le service injectable | `RealtimeService.publish()` (`RealtimeService.ts:161`), `RealtimeService.probe()` (`RealtimeService.ts:213`), `RealtimeService.markBroadcastChannel()` (`RealtimeService.ts:222`) |
|
|
1617
|
+
| Les hooks React | `useNodefony()` (`client/react/index.ts:145`), `useNodefonyState()` (`client/react/index.ts:163`), `useNodefonyIdentity()` (`client/react/index.ts:182`), `useNodefonyChannel()` (`client/react/index.ts:198`), `useNodefonyChannelData()` (`client/react/index.ts:221`), `useNodefonyAdaptiveChannel()` (`client/react/index.ts:255`) |
|
|
1618
|
+
| Le client navigateur | `RealtimeClient.connect()` (`RealtimeClient.ts:389`), `RealtimeClient.subscribe()` (`RealtimeClient.ts:530`), `RealtimeClient.request()` (`RealtimeClient.ts:729`), `RealtimeClient.onDenied()` (`RealtimeClient.ts:471`) |
|
|
1619
|
+
| L'upload | `UploadedFile` (`routerDecorators.ts:1286`), `IUploadedFile` (`IUpload.ts:49`), `queryFile` (`Request.ts:200`) |
|
package/docs/index.md
CHANGED
|
@@ -125,9 +125,9 @@ pas deux applications à écrire deux fois, mais deux entrées du même pipeline
|
|
|
125
125
|
|
|
126
126
|
**Une connexion, N canaux, dans les deux sens.** Le client s'abonne à autant de canaux qu'il veut sur
|
|
127
127
|
la même socket. Trois formes de trafic coexistent : le serveur diffuse (`@RealtimeChannel`,
|
|
128
|
-
`realtimeDecorators.ts:
|
|
129
|
-
`realtimeDecorators.ts:
|
|
130
|
-
`realtimeDecorators.ts:
|
|
128
|
+
`realtimeDecorators.ts:192`), le client appelle et attend une réponse (`@RealtimeAction`,
|
|
129
|
+
`realtimeDecorators.ts:142`), le client pousse sans attendre (`@RealtimeInbound`,
|
|
130
|
+
`realtimeDecorators.ts:231`). Rien n'est ouvert qui n'ait été déclaré.
|
|
131
131
|
|
|
132
132
|
**Le travail est fait une fois par processus, pas une fois par client.** Le `RealtimeHub`
|
|
133
133
|
(`RealtimeHub.ts:213`) tient **un seul producteur par canal** : le premier abonné le démarre, le
|
|
@@ -304,7 +304,7 @@ jamais recopiées ici : elles divergeraient en silence.
|
|
|
304
304
|
## ⚙️ Configuration
|
|
305
305
|
|
|
306
306
|
Un seul point d'entrée : `use("@nodefony/realtime", { … })` dans `nodefony.config.ts`, validé au boot
|
|
307
|
-
contre le schéma du module (`realtimeConfigSchema`, `config.ts:
|
|
307
|
+
contre le schéma du module (`realtimeConfigSchema`, `config.ts:259`). Cinq blocs :
|
|
308
308
|
|
|
309
309
|
- `backplane` — le driver de fan-out et son espace de nommage. Ce cloisonnement devient
|
|
310
310
|
indispensable dès que **deux déploiements partagent le même Redis** : sans lui, leurs publications
|
|
@@ -325,7 +325,7 @@ vue de tous les workers d'un pod.
|
|
|
325
325
|
|
|
326
326
|
Le data plane admin expose `/nodefony/realtime/api/health` — canaux et abonnés, compteurs de
|
|
327
327
|
diffusion, connexions, octets et frames, pression d'écriture. C'est la même donnée que celle rendue à
|
|
328
|
-
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:
|
|
328
|
+
l'écran, servie par la sonde du hub (`RealtimeHub.probe()`, `RealtimeHub.ts:865`).
|
|
329
329
|
|
|
330
330
|
## 🧪 Tests & couverture
|
|
331
331
|
|
|
@@ -367,6 +367,3 @@ l'infrastructure sous la main.
|
|
|
367
367
|
- 🏛️ **Transverse** : [vue d'ensemble du framework](../../../../../docs/architecture/vue-ensemble.md) ·
|
|
368
368
|
[le pipeline d'une requête](../../../../../docs/architecture/pipeline-requete.md)
|
|
369
369
|
- 📖 [Lexique général](../../../../../docs/lexique.md) du framework.
|
|
370
|
-
</content>
|
|
371
|
-
|
|
372
|
-
</invoke>
|
package/docs/observabilite.md
CHANGED
|
@@ -96,7 +96,7 @@ temps de réponse — elle mesure **ce qui n'est pas encore parti**.
|
|
|
96
96
|
Trois partis pris, tous lisibles dans le code.
|
|
97
97
|
|
|
98
98
|
**1. La sonde est une lecture pure, jamais un collecteur.** `RealtimeHub.probe()`
|
|
99
|
-
(`RealtimeHub.ts:
|
|
99
|
+
(`RealtimeHub.ts:865`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
|
|
100
100
|
Aucune entrée-sortie, aucune exception possible, aucun état de lecture conservé. Appeler la sonde
|
|
101
101
|
mille fois par seconde ne changerait rien à ce que le hub fait par ailleurs.
|
|
102
102
|
|
|
@@ -265,7 +265,7 @@ Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts:61`). Quatre familles, un di
|
|
|
265
265
|
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
266
266
|
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
|
|
267
267
|
| `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
|
|
268
|
-
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:
|
|
268
|
+
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:854`). |
|
|
269
269
|
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
|
|
270
270
|
|
|
271
271
|
Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
|
|
@@ -316,7 +316,7 @@ deux autres. Le débit en octets par seconde se dérive de deux photos.
|
|
|
316
316
|
|
|
317
317
|
La source par connexion est `IRealtimeConnProbe` (`IRealtimeProbe.ts:25`), implémentée par le
|
|
318
318
|
transport. Les seuils d'ACTION sont distincts du seuil de comptage, et ils sont
|
|
319
|
-
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:
|
|
319
|
+
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:1044`) : au-delà de
|
|
320
320
|
`websocket.maxBackpressure` (4 MiB par défaut) la politique `websocket.backpressurePolicy`
|
|
321
321
|
s'applique — `drop` par défaut, la frame est **jetée** puisque les canaux d'état sont « le
|
|
322
322
|
dernier gagne » et que le prochain instantané la remplacera. La connexion est **fermée** en
|
|
@@ -356,7 +356,7 @@ Trois champs n'apparaissent que lorsqu'ils ont un sens :
|
|
|
356
356
|
### La couche d'identité, au-dessus
|
|
357
357
|
|
|
358
358
|
`buildOwnHealth()` (`RealtimeAdminApi.ts:52`) enrichit ce snapshot pur pour produire
|
|
359
|
-
`IRealtimeHealth` (`IRealtimeProbe.ts:
|
|
359
|
+
`IRealtimeHealth` (`IRealtimeProbe.ts:121`) : `instanceId`, la santé **process** du worker
|
|
360
360
|
(CPU, mémoire, boucle d'événements), et — si elles ont été branchées — la santé ORM et les compteurs
|
|
361
361
|
d'erreurs du journal. Ces trois derniers champs sont **additifs et optionnels** : un consommateur qui
|
|
362
362
|
ne les connaît pas les ignore.
|
|
@@ -368,14 +368,14 @@ resterait vide jusqu'au premier tick.
|
|
|
368
368
|
|
|
369
369
|
### L'instantané HTTP
|
|
370
370
|
|
|
371
|
-
`GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:
|
|
371
|
+
`GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:98`)
|
|
372
372
|
sous le namespace `realtime` du data plane admin. Sert le premier affichage, les sondes de liveness,
|
|
373
373
|
et tout script qui ne veut pas ouvrir une socket.
|
|
374
374
|
|
|
375
375
|
### Le canal temps réel
|
|
376
376
|
|
|
377
377
|
`nodefony:socket`. Le producteur est un minuteur qui rappelle **le même endpoint** à travers le
|
|
378
|
-
courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts:
|
|
378
|
+
courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts:212`,
|
|
379
379
|
via `createBrokerTicker()` (`providers.ts:465`)). Rien n'est dupliqué : le canal est un endpoint
|
|
380
380
|
rejoué.
|
|
381
381
|
|
|
@@ -406,8 +406,9 @@ refusée.
|
|
|
406
406
|
| `nodefony:syslog` | `createSyslogBridge()` (`providers.ts:145`) | **événementiel** | — (coalescé) |
|
|
407
407
|
|
|
408
408
|
> [!WARNING]
|
|
409
|
-
> **`nodefony:dashboard` n'
|
|
410
|
-
> nom
|
|
409
|
+
> **`nodefony:dashboard` n'est servi par aucun fournisseur.** Le canal de supervision s'appelle
|
|
410
|
+
> `nodefony:supervision`. Le nom survit pourtant comme constante exportée, dépréciée
|
|
411
|
+
> (`PLATFORM_CHANNELS.dashboard`, `platformChannels.ts:58`, retrait en 11.0.0) — c'est ce qui le rend piégeux. S'abonner à un canal inconnu ne lève **aucune
|
|
411
412
|
> erreur** — le provider rend `null` et rien n'arrive jamais. C'est le piège numéro un de la page.
|
|
412
413
|
|
|
413
414
|
`nodefony:syslog` est le seul de la liste à ne pas être cadencé : il **relaie** au lieu de sonder. Comme
|
|
@@ -462,7 +463,7 @@ sur deux workers et sembler se contredire.
|
|
|
462
463
|
**La réponse de Nodefony est un modèle _push_.** Chaque worker remonte périodiquement sa santé au
|
|
463
464
|
maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts:170`) ; le maître fusionne et
|
|
464
465
|
rediffuse ; chaque worker met le résultat en cache. **N'importe lequel** sert alors la vue pod en temps
|
|
465
|
-
constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:
|
|
466
|
+
constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:291`).
|
|
466
467
|
|
|
467
468
|
La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:46`), est une fonction pure — et sa règle
|
|
468
469
|
mérite d'être connue :
|
|
@@ -474,7 +475,7 @@ mérite d'être connue :
|
|
|
474
475
|
| `backpressure.maxBufferedAmount` | **maximum** | La santé d'une flotte se juge sur son **pire** membre, pas sur sa moyenne. |
|
|
475
476
|
| `slowConsumers`, `drops` | somme | Ce sont des dénombrements. |
|
|
476
477
|
|
|
477
|
-
Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`IRealtimeProbe.ts:
|
|
478
|
+
Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`IRealtimeProbe.ts:178`) :
|
|
478
479
|
un consommateur sait immédiatement s'il lit une vue pod ou une vue per-instance, et `instances[]`
|
|
479
480
|
garde le détail par worker pour le forage.
|
|
480
481
|
|
|
@@ -587,6 +588,3 @@ npm run coverage # rapport vitest — jamais de pourcentage figé dans cette p
|
|
|
587
588
|
- [Sécurité](securite.md) — qui a le droit de s'abonner à un canal de santé, et pourquoi ça compte.
|
|
588
589
|
- [Cookbook — un chat](cookbook-chat.md) — le patron appliqué de bout en bout sur une vraie fonctionnalité.
|
|
589
590
|
- [Vocabulaire](vocabulaire.md) — les douze mots de la socket.
|
|
590
|
-
</content>
|
|
591
|
-
|
|
592
|
-
</invoke>
|