@nodefony/realtime 10.0.0-beta.5 → 10.0.0-beta.7
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/README.md +55 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +91 -12
- package/dist/nodefony/config/config.js +7 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +341 -43
- package/dist/nodefony/interfaces/IRealtimeConnection.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +32 -4
- package/dist/nodefony/src/backplane/RedisBackplane.js +146 -41
- package/dist/nodefony/src/backplane/controlOrder.js +86 -0
- package/dist/nodefony/src/backplane/envelope.js +505 -27
- package/dist/nodefony/src/backplane/ingress.js +93 -0
- package/dist/nodefony/src/backplane/presenceMessage.js +205 -0
- package/dist/nodefony/src/backplane/publishQueue.js +97 -3
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +2 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +23 -2
- package/dist/nodefony/src/server/ContractRealtimeController.js +75 -0
- package/dist/nodefony/src/server/RealtimeConnection.js +135 -0
- package/dist/nodefony/src/server/RealtimeController.js +409 -107
- package/dist/nodefony/src/server/RealtimeHub.js +1158 -111
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +2 -0
- package/dist/nodefony/src/server/admissionBudget.js +176 -0
- package/dist/nodefony/src/server/channelPolicyRegistry.js +95 -0
- package/dist/nodefony/src/server/channelPolicyResolution.js +191 -0
- package/dist/nodefony/src/server/channelPresence.js +488 -0
- package/dist/nodefony/src/server/connectionAdmission.js +242 -0
- package/dist/nodefony/src/server/contractRoutes.js +75 -0
- package/dist/nodefony/src/server/deniedDetail.js +10 -3
- package/dist/nodefony/src/server/frameAdmission.js +160 -0
- package/dist/nodefony/src/server/presenceBus.js +486 -0
- package/dist/nodefony/src/server/realtimeShutdown.js +31 -0
- package/dist/nodefony/src/server/remotePresence.js +541 -0
- package/dist/nodefony/src/server/subscribedAck.js +13 -1
- package/dist/nodefony/src/server/syslogUplink.js +3 -4
- package/dist/nodefony/src/server/welcomeIdentity.js +37 -0
- package/dist/nodefony/src/service/RealtimeService.js +51 -1
- package/dist/nodefony/src/service/publicOrigin.js +40 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +27 -4
- package/dist/nodefony/testing/backplaneConformance.js +347 -0
- package/dist/nodefony/testing/index.js +43 -12
- package/dist/nodefony/testing/presenceClient.js +54 -0
- package/dist/types/index.d.ts +39 -7
- package/dist/types/nodefony/config/config.d.ts +9 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +198 -17
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +46 -51
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +5 -25
- package/dist/types/nodefony/interfaces/IRealtimeConnection.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeConnectionState.d.ts +41 -2
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +64 -3
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +7 -166
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +21 -0
- package/dist/types/nodefony/interfaces/index.d.ts +2 -1
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +22 -4
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +89 -18
- package/dist/types/nodefony/src/backplane/controlOrder.d.ts +75 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +77 -0
- package/dist/types/nodefony/src/backplane/ingress.d.ts +48 -0
- package/dist/types/nodefony/src/backplane/presenceMessage.d.ts +150 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +25 -1
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +10 -0
- package/dist/types/nodefony/src/server/ContractRealtimeController.d.ts +138 -0
- package/dist/types/nodefony/src/server/RealtimeConnection.d.ts +20 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +110 -12
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +448 -42
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +1 -1
- package/dist/types/nodefony/src/server/admissionBudget.d.ts +71 -0
- package/dist/types/nodefony/src/server/channelPolicyRegistry.d.ts +41 -0
- package/dist/types/nodefony/src/server/channelPolicyResolution.d.ts +112 -0
- package/dist/types/nodefony/src/server/channelPresence.d.ts +155 -0
- package/dist/types/nodefony/src/server/connectionAdmission.d.ts +68 -0
- package/dist/types/nodefony/src/server/contractRoutes.d.ts +42 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +6 -0
- package/dist/types/nodefony/src/server/frameAdmission.d.ts +66 -0
- package/dist/types/nodefony/src/server/presenceBus.d.ts +112 -0
- package/dist/types/nodefony/src/server/realtimeShutdown.d.ts +21 -0
- package/dist/types/nodefony/src/server/remotePresence.d.ts +151 -0
- package/dist/types/nodefony/src/server/subscribedAck.d.ts +10 -0
- package/dist/types/nodefony/src/server/welcomeIdentity.d.ts +19 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +17 -2
- package/dist/types/nodefony/src/service/publicOrigin.d.ts +19 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +16 -30
- package/dist/types/nodefony/testing/backplaneConformance.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +13 -2
- package/dist/types/nodefony/testing/presenceClient.d.ts +20 -0
- package/docs/actions.md +420 -18
- package/docs/architecture.md +101 -11
- package/docs/cadence-et-contre-pression.md +87 -11
- package/docs/canaux-a-membres.md +277 -0
- package/docs/cas-d-usage.md +1703 -0
- package/docs/configuration.md +175 -19
- package/docs/cookbook-chat.md +701 -224
- package/docs/index.md +11 -1
- package/docs/observabilite.md +97 -21
- package/docs/protocole.md +142 -33
- package/docs/securite.md +347 -41
- package/docs/vocabulaire.md +13 -12
- package/package.json +16 -13
package/docs/index.md
CHANGED
|
@@ -46,7 +46,9 @@ Quatre parcours selon ce que tu viens faire. L'ordre compte : chaque étape supp
|
|
|
46
46
|
2. [Ce que le module apporte](#-ce-que-le-module-apporte) — pourquoi une couche de plus au-dessus de
|
|
47
47
|
la WebSocket, et ce qu'elle t'évite d'écrire.
|
|
48
48
|
3. [Architecture](./architecture.md) — le trajet complet d'une frame, du câble jusqu'à ta méthode.
|
|
49
|
-
4. [
|
|
49
|
+
4. [Cas d'usage](./cas-d-usage.md) — jeu, salon privé, sonde, notification, tâche : l'exemple
|
|
50
|
+
court qui correspond à ton besoin, et le piège qui l'accompagne.
|
|
51
|
+
5. [Cookbook — un chat](./cookbook-chat.md) — l'exemple intégrateur, de bout en bout.
|
|
50
52
|
|
|
51
53
|
**Je passe d'un processus à plusieurs** — le moment où le temps réel casse sans prévenir. En
|
|
52
54
|
mono-processus tout marchait ; à deux répliques, un message publié sur l'une n'atteint jamais un
|
|
@@ -93,6 +95,8 @@ Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce q
|
|
|
93
95
|
| [Actions RPC](./actions.md) | appeler le serveur et attendre une réponse | tu veux savoir si l'appel a marché |
|
|
94
96
|
| [Observabilité](./observabilite.md) | la sonde, les canaux de santé, les écrans | tu te demandes si ta socket va bien |
|
|
95
97
|
| [Cadence et contre-pression](./cadence-et-contre-pression.md) | un client trop lent : ralentir, refuser, couper | un écran prend du retard, ou la mémoire monte |
|
|
98
|
+
| [Canal à membres](./canaux-a-membres.md) | abonner d'office, éjecter, terminer un canal | le serveur décide qui est dans le canal |
|
|
99
|
+
| [Cas d'usage](./cas-d-usage.md) | un exemple court par besoin, et son piège | tu sais ce que tu veux construire |
|
|
96
100
|
| [Cookbook — un chat](./cookbook-chat.md) | l'exemple complet, client et serveur | tu veux du code qui marche tout de suite |
|
|
97
101
|
|
|
98
102
|
```nodefony-cards
|
|
@@ -121,6 +125,12 @@ Le tableau pour choisir en cinq secondes ; les cards en dessous pour savoir ce q
|
|
|
121
125
|
{ "icon": "🚦", "title": "cadence-et-contre-pression", "href": "./cadence-et-contre-pression.md",
|
|
122
126
|
"desc": "Deux étages qui ne se parlent pas et se rencontrent quand même : le client ralentit la cadence de ses canaux d'état (AIMD), le serveur refuse d'empiler au-delà de 4 Mio puis ferme en 1013. Avec les courbes calculées par le vrai code, et les sondes qui le montrent en production.",
|
|
123
127
|
"meta": "un écran prend du retard, ou la mémoire du serveur monte" },
|
|
128
|
+
{ "icon": "👥", "title": "canaux-a-membres", "href": "./canaux-a-membres.md",
|
|
129
|
+
"desc": "Une partie à quatre, un salon fermé, un appel : le serveur abonne une connexion d'office, en éjecte une, termine le canal pour tous — et allow reste seul juge de qui entre.",
|
|
130
|
+
"meta": "le serveur décide qui est dans le canal" },
|
|
131
|
+
{ "icon": "🎯", "title": "cas-d-usage", "href": "./cas-d-usage.md",
|
|
132
|
+
"desc": "Un jeu, un salon sur invitation, des capteurs, une notification personnelle, la progression d'une tâche : l'exemple de chaque besoin, exécuté par la suite de tests, avec le choix qui le justifie et l'erreur qui ne se voit qu'en production.",
|
|
133
|
+
"meta": "tu sais ce que tu veux construire" },
|
|
124
134
|
{ "icon": "🍳", "title": "cookbook-chat", "href": "./cookbook-chat.md",
|
|
125
135
|
"desc": "Un salon de discussion complet : configuration, service métier, contrôleur, client navigateur, puis le passage en cluster.",
|
|
126
136
|
"meta": "du code qui tourne, à déformer vers ton besoin" }
|
package/docs/observabilite.md
CHANGED
|
@@ -247,26 +247,29 @@ socket.subscribe("shop:health");
|
|
|
247
247
|
|
|
248
248
|
## 🧰 Ce que la sonde expose, champ par champ
|
|
249
249
|
|
|
250
|
-
Le contrat est `IRealtimeProbe` (`
|
|
250
|
+
Le contrat est `IRealtimeProbe` (`nodefony/src/realtime/platformHealth.ts#IRealtimeProbe`). Quatre familles, un discriminant.
|
|
251
251
|
|
|
252
252
|
### Canaux — qui vit, qui écoute
|
|
253
253
|
|
|
254
|
-
| Champ | Ce qu'il dit
|
|
255
|
-
| ------------------------ |
|
|
256
|
-
| `channels[]` | Une entrée par canal **actif** (au moins un abonné), cf `IRealtimeChannelStat` (`
|
|
257
|
-
| `channels[].channel` | Le nom exact souscrit, **suffixe de cadence compris** (`nodefony:orm:health:2000`).
|
|
258
|
-
| `channels[].subscribers` | Abonnés locaux vivants. Instantané, pas un cumul.
|
|
259
|
-
| `channels[].messages` | Publications cumulées sur ce canal.
|
|
260
|
-
| `channelCount` | Nombre de canaux actifs sur ce processus.
|
|
254
|
+
| Champ | Ce qu'il dit |
|
|
255
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
256
|
+
| `channels[]` | Une entrée par canal **actif** (au moins un abonné), cf `IRealtimeChannelStat` (`nodefony/src/realtime/platformHealth.ts#IRealtimeChannelStat`). |
|
|
257
|
+
| `channels[].channel` | Le nom exact souscrit, **suffixe de cadence compris** (`nodefony:orm:health:2000`). |
|
|
258
|
+
| `channels[].subscribers` | Abonnés locaux vivants. Instantané, pas un cumul. |
|
|
259
|
+
| `channels[].messages` | Publications cumulées sur ce canal. |
|
|
260
|
+
| `channelCount` | Nombre de canaux actifs sur ce processus. |
|
|
261
261
|
|
|
262
262
|
### Fan-out — l'amplification réelle du broker
|
|
263
263
|
|
|
264
|
-
| Champ
|
|
265
|
-
|
|
|
266
|
-
| `publishTotal`
|
|
267
|
-
| `fanoutTotal`
|
|
268
|
-
| `inboundTotal`
|
|
269
|
-
| `ingressRejectedTotal`
|
|
264
|
+
| Champ | Ce qu'il dit |
|
|
265
|
+
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
266
|
+
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
|
|
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#RealtimeHub.recordInbound`). |
|
|
269
|
+
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
|
|
270
|
+
| `sealRejectedTotal` | Messages venus du **backplane** et jetés parce que leur sceau ne se vérifie pas. `0` sans bus scellé. |
|
|
271
|
+
| `ingressUnknownDiscardedTotal` | Messages venus du **backplane** et jetés parce que d'une version **plus récente** : canal `nodefony:bus:*` ou type d'ordre inconnus. |
|
|
272
|
+
| `localOnlyTotal` | Publications restées sur ce serveur alors qu'un bus est branché (canal non diffusable). `0` sans bus. |
|
|
270
273
|
|
|
271
274
|
Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
|
|
272
275
|
moyenne d'un salon**. S'il vaut 1, chaque publication ne touche qu'une personne — un canal par client,
|
|
@@ -296,14 +299,83 @@ lectures possibles quand il grimpe :
|
|
|
296
299
|
Le canal visé n'est délibérément pas exposé : un compteur d'observabilité ne doit pas devenir un
|
|
297
300
|
moyen de sonder ce que le système accepte.
|
|
298
301
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
+
### Messages du bus d'une version plus récente — compté à part
|
|
303
|
+
|
|
304
|
+
Une version suivante peut ajouter des messages au bus : un canal du territoire `nodefony:bus:`, ou un
|
|
305
|
+
nouveau type d'ordre sur le canal de contrôle. Pendant un déploiement progressif, un serveur resté sur
|
|
306
|
+
l'ancienne version les reçoit sans les connaître. Il les **jette** — aucun abonné n'en reçoit rien —
|
|
307
|
+
et les compte dans `ingressUnknownDiscardedTotal`, **jamais** dans `ingressRejectedTotal` : sinon
|
|
308
|
+
chaque déploiement déclencherait de fausses alertes d'injection, et une vraie attaque passerait
|
|
309
|
+
inaperçue dans ce bruit. Le journal le signale une seule fois par motif, en `INFO`, sans recopier le
|
|
310
|
+
nom reçu.
|
|
311
|
+
|
|
312
|
+
Il monte pendant un déploiement, puis s'arrête quand tous les serveurs portent la même version. S'il
|
|
313
|
+
continue de monter ensuite, un serveur est resté sur une version plus ancienne. Ce qui reste
|
|
314
|
+
suspect reste compté comme une injection : un autre canal de la plateforme, une variante de casse du
|
|
315
|
+
territoire, un ordre d'un type connu mais de forme fausse, ou qui vise un canal de la plateforme.
|
|
316
|
+
|
|
317
|
+
Un message **mal scellé** n'entre pas dans ce compteur : le transport l'écarte avant le hub, et le
|
|
318
|
+
compte à part dans `sealRejectedTotal` (relayé au même niveau de la sonde, et dans
|
|
319
|
+
`backplane.sealRejectedTotal`). Quand celui-là grimpe, trois lectures : une rotation du secret en
|
|
320
|
+
cours (des exemplaires n'ont pas encore le même), un producteur mal configuré, ou quelqu'un qui
|
|
321
|
+
écrit sur votre bus sans connaître le secret.
|
|
322
|
+
|
|
323
|
+
### Restés sur ce serveur — l'oubli de diffusion rendu visible
|
|
324
|
+
|
|
325
|
+
`localOnlyTotal` compte les publications restées sur **ce** serveur alors qu'un bus est branché :
|
|
326
|
+
leur canal n'est pas déclaré diffusable (`@RealtimeBroadcast`). Sans bus, il vaut toujours zéro —
|
|
327
|
+
la question ne se pose pas. Coût : un `++` sur un chemin déjà froid, aucune allocation.
|
|
328
|
+
|
|
329
|
+
Deux lectures quand il grimpe :
|
|
330
|
+
|
|
331
|
+
- **voulue** — un canal propre au serveur par nature : le forage de Studio, la barre de débogage,
|
|
332
|
+
la sonde d'un exemplaire ;
|
|
333
|
+
- **oubli** — un canal applicatif (`chat:`, `alertes:`) dont les abonnés des AUTRES serveurs ne
|
|
334
|
+
reçoivent rien, sans erreur. Ajouter `@RealtimeBroadcast("chat:")` sur son contrôleur.
|
|
335
|
+
|
|
336
|
+
Au démarrage, la ligne `realtime diffusion entre serveurs : …` liste les préfixes diffusés (le
|
|
337
|
+
canal personnel `user:` y figure d'office dès qu'un bus est branché) : tout ce qui n'y est pas
|
|
338
|
+
reste local.
|
|
339
|
+
|
|
340
|
+
### Admission — les refus par cause
|
|
341
|
+
|
|
342
|
+
Quatre compteurs monotones disent POURQUOI un abonné n'est pas entré, quand la cause n'est pas son
|
|
343
|
+
droit. Un refus de droit (`forbidden`) n'est pas compté ici : c'est le comportement normal d'un canal
|
|
344
|
+
gardé. Ce que comptent ceux-ci, c'est une infrastructure qui ne suit plus, ou une configuration
|
|
345
|
+
incomplète (`nodefony/src/realtime/platformHealth.ts#IRealtimeProbe`).
|
|
346
|
+
|
|
347
|
+
<!-- prettier-ignore -->
|
|
348
|
+
| Champ | Unité | Ce qu'il compte | Quand il monte |
|
|
349
|
+
| --- | --- | --- | --- |
|
|
350
|
+
| `authenticateTimedOutTotal` | une poignée de main | connexions fermées en `1013` faute d'authentification dans `authenticateTimeoutMs` | la base ou le service qui porte les identités ne répond plus ; les clients reculent puis reviennent |
|
|
351
|
+
| `allowTimedOutTotal` | un appel d'`allow` | règles d'admission restées sans réponse dans `allowTimeoutMs` ; l'abonné a reçu `unavailable` et réessaie | la base que lisent les règles ne suit plus ; 50 en 5 s ouvrent le coupe-circuit |
|
|
352
|
+
| `allowSaturatedTotal` | une demande | demandes rendues `unavailable` SANS appeler la règle : plafond `maxPendingAllowsTotal` atteint, coupe-circuit ouvert, ou délai épuisé dans la file d'une connexion | une tempête de reconnexion, ou une base lente qui fait déborder les places |
|
|
353
|
+
| `systemFloorDeniedTotal` | un abonnement | canaux de plateforme (`syslog:`, `orm:`…) refusés faute de module de sécurité chargé | une configuration incomplète autant qu'une tentative d'accès — dans les deux cas, quelque chose est à regarder |
|
|
354
|
+
|
|
355
|
+
Les trois premiers vont ensemble : `allowTimedOutTotal` qui monte puis `allowSaturatedTotal` qui
|
|
356
|
+
s'envole, c'est la base qui a cessé de répondre et le serveur qui se protège — la base reçoit au plus
|
|
357
|
+
`maxPendingAllowsTotal` appels à la fois, jamais une rafale proportionnelle au nombre de clients. Un
|
|
358
|
+
`WARNING` limité à un par fenêtre de 10 s accompagne chaque famille dans le journal. Les réglages et
|
|
359
|
+
leur raison : [`configuration.md`](./configuration.md) ; le comportement vu par l'abonné :
|
|
360
|
+
[`securite.md`](./securite.md).
|
|
361
|
+
|
|
362
|
+
### Trames écartées à l'entrée
|
|
363
|
+
|
|
364
|
+
Deux compteurs monotones disent combien de trames reçues n'ont pas pu être lues. Elles ne reçoivent
|
|
365
|
+
aucune réponse et la connexion reste ouverte : répondre à une trame illisible offrirait à un client
|
|
366
|
+
défectueux une boucle, et à un curieux un écho de ce que le décodeur accepte. Le journal en dit la
|
|
367
|
+
première de chaque connexion, jamais les suivantes.
|
|
368
|
+
|
|
369
|
+
<!-- prettier-ignore -->
|
|
370
|
+
| Champ | Unité | Ce qu'il compte | Quand il monte |
|
|
371
|
+
| --- | --- | --- | --- |
|
|
372
|
+
| `binaryDiscardedTotal` | une trame reçue | trames binaires reçues sur une connexion texte dont le contrôleur ne lit pas les octets bruts (`onRealtimeBinary`) | un client envoie un flux que personne n'attend, ou le lecteur a été oublié côté serveur |
|
|
373
|
+
| `malformedDiscardedTotal` | une trame reçue | trames illisibles pour le codec de la connexion : JSON invalide, CBOR tronqué | un client défectueux, ou quelqu'un qui sonde le décodeur |
|
|
302
374
|
|
|
303
375
|
### Connexions et volume
|
|
304
376
|
|
|
305
377
|
`connectionCount`, `bytesSentTotal`, `messagesSentTotal` — vivants pour la première, cumulés pour les
|
|
306
|
-
deux autres. Le débit en octets par seconde se dérive de deux photos.
|
|
378
|
+
deux autres, connexions fermées comprises. Le débit en octets par seconde se dérive de deux photos.
|
|
307
379
|
|
|
308
380
|
### Back-pressure — le risque numéro un
|
|
309
381
|
|
|
@@ -353,12 +425,15 @@ Trois champs n'apparaissent que lorsqu'ils ont un sens :
|
|
|
353
425
|
signale une perte **volontaire** : la mémoire du processus a été préservée au prix de messages non
|
|
354
426
|
relayés aux autres répliques. Il doit rester à zéro ; s'il bouge, regarder l'état du bus avant de
|
|
355
427
|
relever le plafond (`backplane.maxQueueBytes`) — un plafond plus haut ne rattrape pas un bus en
|
|
356
|
-
retard, il repousse l'échéance.
|
|
428
|
+
retard, il repousse l'échéance. Les **ordres** (terminer un canal, éjecter un compte) ne sont
|
|
429
|
+
jamais abandonnés pour faire place : ils passent hors plafond. Un ordre que le bus refuse (bus
|
|
430
|
+
injoignable) est compté à part, `controlLostTotal`, et annoncé une fois par épisode — s'il bouge,
|
|
431
|
+
une éjection n'a pas été appliquée par les autres répliques.
|
|
357
432
|
|
|
358
433
|
### La couche d'identité, au-dessus
|
|
359
434
|
|
|
360
435
|
`buildOwnHealth()` (`RealtimeAdminApi.ts#buildOwnHealth`) enrichit ce snapshot pur pour produire
|
|
361
|
-
`IRealtimeHealth` (`
|
|
436
|
+
`IRealtimeHealth` (`nodefony/src/realtime/platformHealth.ts#IRealtimeHealth`) : `instanceId`, la santé **process** du worker
|
|
362
437
|
(CPU, mémoire, boucle d'événements), et — si elles ont été branchées — la santé ORM et les compteurs
|
|
363
438
|
d'erreurs du journal. Ces trois derniers champs sont **additifs et optionnels** : un consommateur qui
|
|
364
439
|
ne les connaît pas les ignore.
|
|
@@ -475,10 +550,11 @@ mérite d'être connue :
|
|
|
475
550
|
| ---------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------- |
|
|
476
551
|
| `publishTotal`, `fanoutTotal`, `connectionCount`, octets, frames | **somme** | Le pod fait la somme de ce que font ses workers. |
|
|
477
552
|
| `ingressRejectedTotal` | somme | Un refus reste un refus, quel que soit le worker qui l'a opposé. |
|
|
553
|
+
| `localOnlyTotal` | somme | Une publication restée locale l'est sur le worker qui l'a émise. |
|
|
478
554
|
| `backpressure.maxBufferedAmount` | **maximum** | La santé d'une flotte se juge sur son **pire** membre, pas sur sa moyenne. |
|
|
479
555
|
| `slowConsumers`, `drops` | somme | Ce sont des dénombrements. |
|
|
480
556
|
|
|
481
|
-
Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`
|
|
557
|
+
Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`nodefony/src/realtime/platformHealth.ts#IRealtimeClusterHealth`) :
|
|
482
558
|
un consommateur sait immédiatement s'il lit une vue pod ou une vue per-instance, et `instances[]`
|
|
483
559
|
garde le détail par worker pour le forage.
|
|
484
560
|
|
package/docs/protocole.md
CHANGED
|
@@ -274,6 +274,18 @@ production** (`welcomeEnv()`, `welcomeEnv.ts#welcomeEnv`) : c'est lui que porte
|
|
|
274
274
|
en développement. Émis par le contrôleur au handshake (`peer.notify("realtime:welcome")`,
|
|
275
275
|
`RealtimeController.ts#RealtimeController.onHandshake`). Il n'y a pas de champ `version`.
|
|
276
276
|
|
|
277
|
+
Un septième champ, **`codec`**, est réservé au codec de trame retenu pour les frames qui suivent
|
|
278
|
+
(`RealtimeEventMap.ts#IRealtimeWelcome.codec`, ADR-0022 §2) : un des sous-protocoles que le client a
|
|
279
|
+
offerts au handshake. **Absent, il vaut `nodefony.jsonrpc.v1`** — JSON-RPC texte —, et le codec ne se
|
|
280
|
+
déduit jamais de l'écho `Sec-WebSocket-Protocol`, que `ws` renvoie qu'il parle le sous-protocole ou
|
|
281
|
+
non. Le client sait l'offrir et l'adopter (`frameCodecNegotiation.ts#adoptFrameCodec`) ; le serveur le
|
|
282
|
+
choisit quand l'application active `codec: "cbor"`, et renvoie ce choix dans le handshake.
|
|
283
|
+
|
|
284
|
+
Un huitième, **`bytes`**, dit qu'une connexion restée en texte reçoit les octets sous la convention
|
|
285
|
+
`{"$bytes":"<base64url>"}` (`RealtimeEventMap.ts#IRealtimeWelcome.bytes`) : le serveur l'émet quand il a
|
|
286
|
+
activé un codec binaire que le client n'a pas pris, et le client relit alors des `Uint8Array` sans rien
|
|
287
|
+
importer. Absent : JSON natif. La valeur nomme la convention ; une valeur inconnue laisse le JSON natif.
|
|
288
|
+
|
|
277
289
|
## 🔌 Anatomie d'une frame, champ par champ
|
|
278
290
|
|
|
279
291
|
### Le tronc commun
|
|
@@ -282,7 +294,7 @@ en développement. Émis par le contrôleur au handshake (`peer.notify("realtime
|
|
|
282
294
|
{
|
|
283
295
|
"jsonrpc": "2.0", // signature de protocole — OBLIGATOIRE, sinon la frame est `invalid`
|
|
284
296
|
"method": "subscribe", // ce qu'on demande — sa PRÉSENCE fait de la frame un appel
|
|
285
|
-
"params": { "channel": "nodefony:orm:health" }, // la charge
|
|
297
|
+
"params": { "channel": "nodefony:orm:health" }, // la charge : objet ou tableau, jamais fiable côté serveur
|
|
286
298
|
"id": 42, // PRÉSENT = requête (réponse due) · ABSENT = notification
|
|
287
299
|
}
|
|
288
300
|
```
|
|
@@ -291,9 +303,25 @@ en développement. Émis par le contrôleur au handshake (`peer.notify("realtime
|
|
|
291
303
|
| --------- | ------------------------- | -------------------- | ----------------------------------------------------------------------------------------- |
|
|
292
304
|
| `jsonrpc` | oui, toujours | `"2.0"` littéral | Sans lui, `JsonRpcPeer.receive()` classe `invalid` (`JsonRpcPeer.ts#JsonRpcPeer.receive`) |
|
|
293
305
|
| `method` | sur un appel | chaîne | Décide de la nature ET de l'aiguillage (canal, action, verbe pub/sub) |
|
|
294
|
-
| `params` | non |
|
|
306
|
+
| `params` | non | objet ou tableau | Charge applicative. Vient du réseau : **jamais** digne de confiance |
|
|
295
307
|
| `id` | sur une requête / réponse | nombre **ou** chaîne | Corrèle l'aller et le retour |
|
|
296
308
|
|
|
309
|
+
> [!IMPORTANT]
|
|
310
|
+
> **La charge d'un canal est un objet ou un tableau, ou absente.** JSON-RPC 2.0 (§4.2) exige un
|
|
311
|
+
> `params` structuré : une bibliothèque JSON-RPC stricte, dans un autre langage, refuserait la trame
|
|
312
|
+
> d'un scalaire. `publish` lève donc une `TypeError` qui nomme le canal et le type reçu, à la ligne
|
|
313
|
+
> fautive, des deux côtés de la socket (`payload.ts#isRealtimePayload`) :
|
|
314
|
+
>
|
|
315
|
+
> ```ts
|
|
316
|
+
> const temperature = 42;
|
|
317
|
+
> hub.publish("capteur:temp", temperature); // TypeError — un scalaire
|
|
318
|
+
> hub.publish("capteur:temp", { value: temperature }); // conforme
|
|
319
|
+
> hub.publish("build:done"); // conforme : part sans `params`, arrive `undefined`
|
|
320
|
+
> ```
|
|
321
|
+
>
|
|
322
|
+
> Des octets restent admis. Une charge absente arrive `undefined` chez l'abonné, quel que soit le bus
|
|
323
|
+
> entre les serveurs, scellé ou non.
|
|
324
|
+
|
|
297
325
|
> [!IMPORTANT]
|
|
298
326
|
> Le pair accepte un `id` **chaîne** en entrée (conforme à la norme), mais il n'attribue jamais que
|
|
299
327
|
> des `id` **numériques** à ses propres appels sortants. Une réponse portant un `id` chaîne est donc
|
|
@@ -334,6 +362,60 @@ obtenue par la socket est **identique** à ce que rendrait la même route en RES
|
|
|
334
362
|
dans le `result` casserait cette égalité. Un pair qui ne connaît pas `meta` l'ignore — le champ est
|
|
335
363
|
rétro-compatible par construction.
|
|
336
364
|
|
|
365
|
+
### Des octets dans la charge — une frame comme les autres
|
|
366
|
+
|
|
367
|
+
**Une trame binaire routée est une notification JSON-RPC dont la charge est, ou contient, un
|
|
368
|
+
`Uint8Array` ; le codec dit seulement comment il la porte sur le fil** (ADR-0022 §3). Il n'existe
|
|
369
|
+
pas de seconde grammaire, ni d'en-tête d'octets maison du type `[n° de canal][charge]` : un tel
|
|
370
|
+
en-tête aurait demandé un second verrou et une seconde sonde, qui finissent toujours par diverger
|
|
371
|
+
du premier.
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
// côté serveur — la même publication, quel que soit le codec de chaque abonné
|
|
375
|
+
const frame = new Uint8Array([0x01, 0x02, 0x03]);
|
|
376
|
+
hub.publish("capteur:raw", { seq: 42, frame });
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
| Connexion de l'abonné | Ce qui passe sur le fil | Ce que reçoit `on("capteur:raw")` |
|
|
380
|
+
| --------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
|
381
|
+
| CBOR | trame binaire : `frame` en chaîne d'octets native (type majeur 2) | `{ seq: 42, frame: Uint8Array }` |
|
|
382
|
+
| texte, serveur en `codec: "cbor"` | trame texte : `"frame":{"$bytes":"AQID"}` (base64url) | `{ seq: 42, frame: Uint8Array }` |
|
|
383
|
+
| texte, serveur en `codec: "json"` | trame texte, sans convention d'octets : `"frame":{"0":1,"1":2,"2":3}` | un objet déformé, plus un `Uint8Array` (`jsonFrameCodec.ts#JSON_FRAME_CODEC`) — d'où `codec: "cbor"` dès que des octets circulent |
|
|
384
|
+
|
|
385
|
+
Parce que c'est la même frame, elle passe par **les mêmes portes** que le texte : verrou de frame,
|
|
386
|
+
politique de canal, audit des refus, sonde du hub ([sécurité](./securite.md)). Le codec se négocie
|
|
387
|
+
au handshake et s'annonce dans l'accueil (champs `codec` et `bytes`, voir plus haut) ; il se règle
|
|
388
|
+
côté serveur par la clé [`codec`](./configuration.md#codec--la-forme-des-frames-sur-le-fil), côté
|
|
389
|
+
navigateur par l'option `codec` de la socket.
|
|
390
|
+
|
|
391
|
+
**La règle du `$bytes` forgé.** Sur une connexion texte, le récepteur relit en octets tout objet
|
|
392
|
+
dont la **seule** clé est `$bytes` et dont la valeur est une chaîne. Un objet applicatif qui a
|
|
393
|
+
exactement cette forme sans venir d'octets changerait donc de nature selon le codec de l'abonné :
|
|
394
|
+
objet pour un client CBOR, octets pour un client texte. L'encodeur texte le **refuse** plutôt que
|
|
395
|
+
d'envoyer une donnée qui serait mal relue (`jsonFrameCodec.ts#JSON_BYTES_FRAME_CODEC`) :
|
|
396
|
+
|
|
397
|
+
```ts ignore
|
|
398
|
+
socket.publish("prix", { montant: { $bytes: "12 euros" } });
|
|
399
|
+
// TypeError : codec JSON : objet {"$bytes": …} non issu d'octets sous la clé "montant"
|
|
400
|
+
// — le récepteur le relirait en octets ; envoyer un Uint8Array, ou ajouter une autre clé à l'objet
|
|
401
|
+
|
|
402
|
+
socket.publish("prix", { montant: { $bytes: "12 euros", devise: "EUR" } }); // conforme : deux clés
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- **Côté navigateur**, l'émission lève à la ligne fautive.
|
|
406
|
+
- **Côté serveur**, une publication diffusée qui contient un tel objet n'atteint pas les abonnés
|
|
407
|
+
texte ; une seule ligne `ERROR` est écrite par publication, pas une par abonné
|
|
408
|
+
(`RealtimeHub.ts#ChannelFrame.claimFailure`). La clé citée est tronquée à 64 caractères et
|
|
409
|
+
échappée, pour qu'un client ne puisse pas forger de ligne de journal (CWE-117).
|
|
410
|
+
- **Limite assumée** : un tel objet **conservé** par l'application, par exemple l'état initial
|
|
411
|
+
qu'un canal renvoie à chaque nouvel abonné, sera refusé aux abonnés texte **à chaque envoi**.
|
|
412
|
+
Renommer la clé ou en ajouter une seconde règle le cas une fois pour toutes.
|
|
413
|
+
|
|
414
|
+
Le refus est préféré à un échappement (envelopper l'objet dans une seconde clé réservée) : échapper
|
|
415
|
+
coûterait un parcours de chaque trame, et imposerait une seconde convention aux deux côtés du fil.
|
|
416
|
+
Le refus ne coûte qu'une recherche de `$bytes` dans le texte déjà produit. Il ne vaut que sous la
|
|
417
|
+
convention d'octets : avec `codec: "json"`, `$bytes` est une clé comme une autre.
|
|
418
|
+
|
|
337
419
|
## 🧰 Les méthodes nominales
|
|
338
420
|
|
|
339
421
|
Ce que le serveur comprend **sans qu'aucun contrôleur ne l'ait déclaré**, plus les deux formes
|
|
@@ -381,7 +463,7 @@ sequenceDiagram
|
|
|
381
463
|
participant P as JsonRpcPeer (serveur)
|
|
382
464
|
participant H as RealtimeHub
|
|
383
465
|
C->>P: handshake WebSocket
|
|
384
|
-
P-->>C: realtime:welcome — ts, protocol, channels, methods, identity
|
|
466
|
+
P-->>C: realtime:welcome — ts, protocol, channels, methods, identity, codec?, bytes?
|
|
385
467
|
C->>P: subscribe — params.channel = "nodefony:orm:health"
|
|
386
468
|
Note over P,H: le hub crée le producteur au 1er abonné
|
|
387
469
|
P-->>C: nodefony:orm:health — params { queries: 42 }
|
|
@@ -402,16 +484,16 @@ Voici **tout** ce que le moteur produit. Les codes de la plage `-32000`…`-3209
|
|
|
402
484
|
la norme aux erreurs applicatives serveur ; Nodefony y place son refus d'autorisation et le défaut
|
|
403
485
|
de `RpcError`.
|
|
404
486
|
|
|
405
|
-
| Code | Émis par | Déclencheur
|
|
406
|
-
| -------- | -------------------------------------------------------------------------------------------- |
|
|
407
|
-
| `-32601` | `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | requête vers une action non enregistrée
|
|
408
|
-
| `-32603` | même méthode (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | le handler a levé une exception **ordinaire**
|
|
409
|
-
| `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts#JsonRpcPeer.receive`) | `beforeDispatch` a dit non **sur une requête**
|
|
410
|
-
| `-32000` | défaut du constructeur de `RpcError` (`JsonRpcPeer.ts#RpcError`) | le handler expose volontairement son refus
|
|
411
|
-
| `-32602` | le pont API, via `RpcError` (`RealtimeController.ts#RealtimeController.actionParamsWrapper`) | `api.request` appelé avec un `params.path` invalide
|
|
412
|
-
| `-32000` | le pont API (`RealtimeController.ts#RealtimeController.invokeApiRequest`) | la route invoquée REFUSE : 401, 403, 404, 405, **422
|
|
487
|
+
| Code | Émis par | Déclencheur | Ce que voit le pair |
|
|
488
|
+
| -------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
489
|
+
| `-32601` | `JsonRpcPeer.handleRequest()` (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | requête vers une action non enregistrée | `method not found: <nom>` |
|
|
490
|
+
| `-32603` | même méthode (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | le handler a levé une exception **ordinaire** | `internal error` — générique, rien d'autre |
|
|
491
|
+
| `-32001` | le refus du verrou de frame (`JsonRpcPeer.ts#JsonRpcPeer.receive`) | `beforeDispatch` a dit non **sur une requête** | `unauthorized`, sans jamais dire pourquoi |
|
|
492
|
+
| `-32000` | défaut du constructeur de `RpcError` (`JsonRpcPeer.ts#RpcError`) | le handler expose volontairement son refus | le message ET le `data` choisis par le handler |
|
|
493
|
+
| `-32602` | le pont API, via `RpcError` (`RealtimeController.ts#RealtimeController.actionParamsWrapper`) | `api.request` appelé avec un `params.path` invalide | message explicite (l'appel est malformé) |
|
|
494
|
+
| `-32000` | le pont API (`RealtimeController.ts#RealtimeController.invokeApiRequest`) | la route invoquée REFUSE : 401, 403, 404, 405, **422**, 429 | le message, `data.status` — `data.fields` sur un 422, `data.retryAfter` si l'action a posé `Retry-After` |
|
|
413
495
|
|
|
414
|
-
Un refus de la route invoquée par le pont garde la forme d'un `fetch` dont `ok` est faux : le statut HTTP voyage dans `data.status`. Une erreur de validation — `ZodError` levée par l'action, ou entrée refusée par le `schema` de la route — est un **422** dont `data.fields` nomme chaque champ et sa partie (`{ field, message, in }`), exactement le `error.fields` du même appel en HTTP. Tout autre `throw` reste opaque (`-32603`).
|
|
496
|
+
Un refus de la route invoquée par le pont garde la forme d'un `fetch` dont `ok` est faux : le statut HTTP voyage dans `data.status`. Une erreur de validation — `ZodError` levée par l'action, ou entrée refusée par le `schema` de la route — est un **422** dont `data.fields` nomme chaque champ et sa partie (`{ field, message, in }`), exactement le `error.fields` du même appel en HTTP. Un refus passager (429, 503) dont l'action a posé `Retry-After` (RFC 9110 §10.2.3) porte l'attente dans `data.retryAfter`, en secondes — une date HTTP y est ramenée à l'écart : une socket ouverte n'a plus d'en-tête, et le même code de contrôleur (`this.response?.setHeader("Retry-After", n)` puis le refus) sert les deux transports. Seul cet en-tête traverse : rien d'autre posé par l'action ne fuit sur une trame. Tout autre `throw` reste opaque (`-32603`).
|
|
415
497
|
|
|
416
498
|
Et un échec qui n'est **pas** une frame : l'expiration. `startCall()` ne reçoit rien dans le délai
|
|
417
499
|
imparti, supprime l'entrée en attente et rejette localement avec `RPC timeout: <méthode>`
|
|
@@ -443,7 +525,7 @@ qui alimente le journal d'audit avec le pair concerné.
|
|
|
443
525
|
### Le refus d'une notification n'est pas une erreur
|
|
444
526
|
|
|
445
527
|
C'est la subtilité la plus importante de la page. Le verrou de frame s'applique aux requêtes **et**
|
|
446
|
-
aux notifications (`beforeDispatch`, `JsonRpcPeer.ts#
|
|
528
|
+
aux notifications (`beforeDispatch`, `JsonRpcPeer.ts#IJsonRpcPeerHooks.beforeDispatch`), mais leurs conséquences diffèrent
|
|
447
529
|
radicalement :
|
|
448
530
|
|
|
449
531
|
| La frame refusée est… | Ce qui part | Pourquoi |
|
|
@@ -453,11 +535,25 @@ radicalement :
|
|
|
453
535
|
|
|
454
536
|
`IRealtimeDenied` (`RealtimeEventMap.ts#IRealtimeDenied`) porte `channel` et `reason` — plus un `detail` optionnel, posé hors production seulement — et le motif
|
|
455
537
|
est **générique**. Jamais « il te manque `ROLE_ADMIN` » : ce serait un oracle d'autorisation, un
|
|
456
|
-
attaquant y lirait la carte des droits.
|
|
457
|
-
`
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
538
|
+
attaquant y lirait la carte des droits. Quatre motifs circulent, et l'union est FERMÉE
|
|
539
|
+
(`RealtimeEventMap.ts#RealtimeDeniedReason`) — un motif inconnu reçu d'un serveur plus récent est
|
|
540
|
+
replié sur `forbidden` :
|
|
541
|
+
|
|
542
|
+
<!-- prettier-ignore -->
|
|
543
|
+
| Motif | Sens | Le client réessaie ? |
|
|
544
|
+
| --- | --- | --- |
|
|
545
|
+
| `forbidden` | le verrou ou la règle d'admission `allow` a dit non | jamais — le droit ne changera pas en réessayant |
|
|
546
|
+
| `limit` | le plafond de canaux de la connexion est atteint (`RealtimeController.ts#RealtimeController.onRealtimeNotification`) | jamais |
|
|
547
|
+
| `unknown` | aucun producteur ne sert ce nom — jamais un oracle : un canal gardé rend `forbidden`, qu'il existe ou non | jamais |
|
|
548
|
+
| `unavailable` | la règle `allow` n'a pas répondu à temps, ou le serveur protège sa base (plafond, coupe-circuit) | **oui**, le seul : trois fois au plus |
|
|
549
|
+
|
|
550
|
+
`unavailable` peut porter `retryAfterMs` : le délai, en millisecondes, pendant lequel le serveur
|
|
551
|
+
n'appellera plus la règle (coupe-circuit ouvert). Le client attend ce délai, puis ajoute un recul
|
|
552
|
+
exponentiel à **gigue complète** — un tirage uniforme entre zéro et 1 s, 2 s, puis 4 s — pour que
|
|
553
|
+
mille clients refusés au même instant ne reviennent pas au même instant. Il ne rejoue que le
|
|
554
|
+
`subscribe` concerné, jamais la connexion. Côté client,
|
|
555
|
+
`onDenied()` (`NodefonySocket.ts#NodefonySocket.onDenied`) branche un handler dessus ; il ne voit que le
|
|
556
|
+
refus DÉFINITIF, après le dernier réessai.
|
|
461
557
|
|
|
462
558
|
### L'abonnement accepté s'acquitte
|
|
463
559
|
|
|
@@ -481,6 +577,18 @@ antérieur — répute l'abonnement actif dès le rejeu, au lieu d'attendre pour
|
|
|
481
577
|
> n'est réservé par aucune norme. Le refus d'autorisation est `-32001` sur une requête, et une
|
|
482
578
|
> notification `realtime:denied` sur une notification.
|
|
483
579
|
|
|
580
|
+
### La fermeture qui dit « reviens plus tard »
|
|
581
|
+
|
|
582
|
+
Une frame refusée laisse la connexion ouverte ; certaines causes, elles, la ferment. Le code de
|
|
583
|
+
fermeture dit alors ce que le client doit faire (`closeCodes.ts`, le registre complet est dans
|
|
584
|
+
[`securite.md`](./securite.md)). Un seul est une invitation à revenir : `1013` « try again later »
|
|
585
|
+
(RFC 6455 §7.4.1). Le serveur l'émet quand l'authentification n'a pas répondu à temps à la poignée
|
|
586
|
+
de main (`authenticateTimeoutMs`), ou quand le client ne vide plus sa file d'envoi. Le client le
|
|
587
|
+
range parmi les fermetures reconnectables (`notice.ts#isReconnectableCloseCode`) : il recule, puis
|
|
588
|
+
rouvre une connexion neuve, rejoue son `welcome` et ses abonnements. Un navigateur ne peut pas
|
|
589
|
+
émettre `1013` lui-même (`WebSocket.close()` n'accepte que `1000` et `3000`-`4999`) : c'est
|
|
590
|
+
toujours le serveur qui ferme.
|
|
591
|
+
|
|
484
592
|
## 🔐 Ce que le protocole ne décide pas
|
|
485
593
|
|
|
486
594
|
Le protocole transporte et classe. Il ne décide ni de l'identité, ni des droits — trois règles qui
|
|
@@ -501,21 +609,22 @@ décrits dans [la page sécurité](./securite.md).
|
|
|
501
609
|
|
|
502
610
|
## ⚠️ Pièges
|
|
503
611
|
|
|
504
|
-
| Symptôme | Cause
|
|
505
|
-
| ---------------------------------------------------------------- |
|
|
506
|
-
| `subscribe` répond `-32601 method not found` | Envoyé **avec un `id`** : classé requête, or c'est une notification (`RealtimeController.ts#RealtimeController.onHandshake`)
|
|
507
|
-
| Le handler passé à `subscribe` n'est jamais appelé | `NodefonySocket.subscribe()` prend **un seul** argument (`NodefonySocket.ts#NodefonySocket.subscribe`)
|
|
508
|
-
| `request()` expire immédiatement, ou ignore le délai | Signature **positionnelle** `(méthode, params, ms)` (`NodefonySocket.ts#NodefonySocket.request`) — un objet d'options n'est pas lu
|
|
509
|
-
| Un tableau de frames n'obtient aucune réponse | Le batch n'est pas implémenté : un tableau n'a pas de `jsonrpc` → `invalid` (`jsonrpc/index.ts#classifyJsonRpcFrame`)
|
|
510
|
-
| Une frame malformée ne renvoie **aucune** erreur | Ni `-32700` ni `-32600` ne sont émis — silence + audit (`JsonRpcPeer.ts#JsonRpcPeer.receive`)
|
|
511
|
-
| Une trame binaire envoyée au client n'arrive à aucun handler |
|
|
512
|
-
| Une trame binaire envoyée au serveur n'arrive à aucune action | Même règle côté serveur : écartée, comptée (`WsConnectionTransport.ts#WsConnectionTransport.binaryDiscarded`), journalisée une fois en WARNING
|
|
513
|
-
|
|
|
514
|
-
|
|
|
515
|
-
|
|
|
516
|
-
|
|
|
517
|
-
|
|
|
518
|
-
|
|
|
612
|
+
| Symptôme | Cause | Correction |
|
|
613
|
+
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
614
|
+
| `subscribe` répond `-32601 method not found` | Envoyé **avec un `id`** : classé requête, or c'est une notification (`RealtimeController.ts#RealtimeController.onHandshake`) | L'émettre sans `id` — `socket.subscribe(canal)` |
|
|
615
|
+
| Le handler passé à `subscribe` n'est jamais appelé | `NodefonySocket.subscribe()` prend **un seul** argument (`NodefonySocket.ts#NodefonySocket.subscribe`) | `subscribe(canal)` **et** `on(canal, handler)`, deux gestes distincts |
|
|
616
|
+
| `request()` expire immédiatement, ou ignore le délai | Signature **positionnelle** `(méthode, params, ms)` (`NodefonySocket.ts#NodefonySocket.request`) — un objet d'options n'est pas lu | `request(m, p, 5000)` ; le défaut est 30 000 ms |
|
|
617
|
+
| Un tableau de frames n'obtient aucune réponse | Le batch n'est pas implémenté : un tableau n'a pas de `jsonrpc` → `invalid` (`jsonrpc/index.ts#classifyJsonRpcFrame`) | Une frame = un objet ; le multiplexage remplace le batch |
|
|
618
|
+
| Une frame malformée ne renvoie **aucune** erreur | Ni `-32700` ni `-32600` ne sont émis — silence + audit (`JsonRpcPeer.ts#JsonRpcPeer.receive`) | Lire le motif `invalid` côté serveur, pas la réponse |
|
|
619
|
+
| Une trame binaire envoyée au client n'arrive à aucun handler | Sur une connexion JSON-RPC texte, une trame binaire n'a pas de canal : le client l'écarte, la compte et émet une notice `-32700` (`NodefonySocket.ts#NodefonySocket.binaryFramesDiscarded`) | Un flux binaire se lit par `onBinary()` (`NodefonySocket.ts#NodefonySocket.onBinary`), hors JSON-RPC ; des octets routés par canal passent par un codec binaire (`codec`) |
|
|
620
|
+
| Une trame binaire envoyée au serveur n'arrive à aucune action | Même règle côté serveur : écartée, comptée (`WsConnectionTransport.ts#WsConnectionTransport.binaryDiscarded`), journalisée une fois en WARNING | Des octets **par canal** : `codec: "cbor"` et `publish`. Un protocole binaire propre à l'application : surcharger `onRealtimeBinary()` (`RealtimeController.ts#RealtimeController.onRealtimeBinary`), émettre par `sendBinaryToClient()` — ⚠️ hors du verrou de frame et des gardes d'action et de canal : toute décision d'accès sur ces octets appartient à l'application |
|
|
621
|
+
| `TypeError : codec JSON : objet {"$bytes": …} non issu d'octets` | Un objet applicatif a la forme exacte du marqueur d'octets ; un client texte le relirait en octets (`jsonFrameCodec.ts#JSON_BYTES_FRAME_CODEC`) | Envoyer un vrai `Uint8Array`, renommer la clé, ou en ajouter une seconde — [la règle](#des-octets-dans-la-charge--une-frame-comme-les-autres) |
|
|
622
|
+
| L'exception du serveur n'arrive jamais au client | Zero Trust : tout throw ordinaire devient `-32603` générique (`JsonRpcPeer.ts#JsonRpcPeer.handleRequest`) | Lever une `RpcError` pour exposer volontairement code et `data` |
|
|
623
|
+
| Une notification refusée disparaît sans trace côté client | Sans `id`, aucune réponse possible (`beforeDispatch`, `JsonRpcPeer.ts#IJsonRpcPeerHooks.beforeDispatch`) | Écouter `realtime:denied` via `onDenied()` (`NodefonySocket.ts#NodefonySocket.onDenied`) |
|
|
624
|
+
| `nodefony:kernel:ping` répond `-32601` sur mon endpoint | L'action `nodefony:kernel:ping` est déclarée par `@nodefony/studio` (`StudioRealtimeController.ts#StudioRealtimeController.realtimeActions`) | Déclarer la sienne, ou lire `serverMethods` avant d'appeler |
|
|
625
|
+
| Le battement de cœur ne renvoie aucun pong | La notification `ping` est un no-op serveur (`RealtimeController.ts#RealtimeController.onHandshake`) | Pour mesurer un RTT, utiliser une action RPC — `ping()` (`NodefonySocket.ts#NodefonySocket.ping`) |
|
|
626
|
+
| Une réponse reçue est ignorée sans message | Corrélation sur `id` **numériques** seulement (`JsonRpcPeer.ts#JsonRpcPeer.handleResponse`) | Ne pas fabriquer soi-même de réponse à `id` chaîne |
|
|
627
|
+
| Les premières frames envoyées après `connect()` semblent perdues | Le transport n'est branché qu'une fois le handshake terminé | Attendre `realtime:welcome` — le client le fait déjà |
|
|
519
628
|
|
|
520
629
|
## 🧪 Tests & couverture
|
|
521
630
|
|