@nodefony/devkit 10.0.0-beta.6 → 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.
Files changed (32) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.157.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/nodefony/controllers/DevkitController.js +1 -1
  5. package/dist/nodefony/controllers/McpController.js +2 -2
  6. package/dist/nodefony/controllers/ProfileSessionController.js +1 -1
  7. package/dist/nodefony/service/DevkitService.js +3 -3
  8. package/package.json +11 -11
  9. package/skills/nodefony-add-grpc-service/SKILL.md +179 -0
  10. package/skills/nodefony-add-grpc-service/scripts/call.mjs +218 -0
  11. package/skills/nodefony-add-realtime-binary/SKILL.md +301 -0
  12. package/skills/nodefony-add-realtime-binary/references/messages.md +20 -0
  13. package/skills/nodefony-add-realtime-binary/references/mesures.md +59 -0
  14. package/skills/nodefony-add-realtime-channel/SKILL.md +408 -326
  15. package/skills/nodefony-add-realtime-channel/references/contrat.md +22 -0
  16. package/skills/nodefony-add-realtime-channel/references/production.md +21 -2
  17. package/skills/nodefony-add-realtime-presence/SKILL.md +328 -0
  18. package/skills/nodefony-authentication/SKILL.md +2 -2
  19. package/skills/nodefony-browser/SKILL.md +11 -1
  20. package/skills/nodefony-browser/references/sondes.md +19 -0
  21. package/skills/nodefony-browser/scripts/audit.mjs +5 -2
  22. package/skills/nodefony-browser/scripts/inspect.mjs +69 -7
  23. package/skills/nodefony-browser/scripts/lib/actions.mjs +54 -3
  24. package/skills/nodefony-browser/scripts/lib/browser.mjs +18 -1
  25. package/skills/nodefony-browser/scripts/lib/probes.mjs +52 -3
  26. package/skills/nodefony-dev/SKILL.md +20 -15
  27. package/skills/nodefony-devops/references/dimensionnement.md +1 -1
  28. package/skills/nodefony-devops/references/frontal.md +5 -2
  29. package/skills/nodefony-devops/references/image.md +23 -15
  30. package/skills/nodefony-migrate-schema/references/verdicts.md +1 -0
  31. package/skills/nodefony-protect-route/SKILL.md +27 -32
  32. package/skills/nodefony-add-realtime-channel/references/binaire.md +0 -191
@@ -0,0 +1,301 @@
1
+ ---
2
+ name: nodefony-add-realtime-binary
3
+ metadata:
4
+ version: 1.1.0
5
+ description: >
6
+ Fait passer des octets sur la socket Nodefony — mesures de capteurs, image, audio, état de
7
+ jeu compact, document partagé — par le codec CBOR négocié et un `Uint8Array` dans la charge
8
+ d'un canal, sous les mêmes gardes que le texte, au lieu de base64, d'un en-tête binaire maison
9
+ ou d'un WebSocket brut. Dit QUAND le binaire gagne et quand il perd, chiffres mesurés à
10
+ l'appui, comment représenter la donnée (tableau typé, bits tassés, réduction côté serveur), et
11
+ ce qui rate sans un message. À charger AVANT de mettre des octets, un tableau typé ou un flux
12
+ audio/vidéo dans un canal, ou de choisir un codec. Le canal lui-même relève de
13
+ `nodefony-add-realtime-channel`.
14
+ Déclencheurs : "envoyer des octets", "binaire", "données binaires", "CBOR",
15
+ "Uint8Array", "Float32Array", "tableau typé", "base64", "envoyer une image", "audio sur la
16
+ socket", "flux de capteurs", "trames trop grosses", "réduire la bande passante", "mes octets
17
+ arrivent en objet", "$bytes", "codec", "onRealtimeBinary".
18
+ ---
19
+
20
+ # add-realtime-binary — des octets dans un canal, sous les mêmes gardes
21
+
22
+ > 🧭 **Arrivé ici directement ? Charge aussi `nodefony-dev`** (la conduite commune) et
23
+ > `nodefony-add-realtime-channel` (le canal qui porte les octets). Cette page ne couvre QUE les
24
+ > octets.
25
+ >
26
+ > Pages installées — **exécute, puis lis** :
27
+ >
28
+ > ```bash
29
+ > node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs --open cbor
30
+ > node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs codec octets
31
+ > ```
32
+ >
33
+ > `nodefony/docs/cbor.md` (le codec, ses mesures), `nodefony/docs/client.md` (« Choisir le codec
34
+ > de trame »), `@nodefony/realtime/docs/protocole.md` (« Des octets dans la charge »),
35
+ > `@nodefony/realtime/docs/configuration.md` (clé `codec`).
36
+
37
+ > **Maintenance** : vérité courante, jamais un journal. Éditer en place ; historique = `git log`.
38
+ > Chaque bloc de code est un EXTRAIT d'un fichier exécuté — la démo binaire de l'application
39
+ > générée (marqueur `excerpt`) : un test refuse qu'il en diverge.
40
+
41
+ ## 1. Quel besoin → quelle voie
42
+
43
+ | Tu veux faire passer… | La voie |
44
+ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
45
+ | des octets qui appartiennent à un canal (le cas normal) | `codec: "cbor"` côté serveur + `Uint8Array` dans la charge + `publish` / `on(canal)` (§4, §5) |
46
+ | des séries numériques denses (mesures, positions, spectre) | `toLittleEndianBytes(série)` à l'envoi, `fromLittleEndianBytes(octets, Int16Array)` à l'arrivée (§2, §6) |
47
+ | beaucoup de petites valeurs booléennes ou bornées | des bits tassés dans un `Uint8Array` (§2) |
48
+ | un fichier, une image lourde, une pièce jointe | une route HTTP (`POST` multipart, flux) — jamais la socket ; puis un message qui la cite |
49
+ | un protocole binaire entièrement à toi, sans canal | le flux brut `onRealtimeBinary()` / `onBinary()` — HORS des gardes (§7) |
50
+ | des objets ordinaires (texte, nombres, statuts) | rien : reste en JSON, le défaut. CBOR y est plus lent (§2) |
51
+
52
+ **N'invente jamais d'en-tête binaire maison** (`[n° de canal][charge]`), ni base64 dans une chaîne :
53
+ le premier contourne toutes les gardes et se maintient des deux côtés ; le second coûte un tiers
54
+ d'octets en plus et un décodage des milliers de fois plus lent (§2).
55
+
56
+ ## 2. Décider avec les mesures — le binaire gagne SUR LES OCTETS, et seulement là
57
+
58
+ Mesures du framework sur trames réelles (Node 26, médiane de 7 séries, mêmes données en JSON avec
59
+ octets en base64 et en CBOR). Le détail, avec conditions et sources : [`references/mesures.md`](references/mesures.md).
60
+
61
+ | Charge | Taille JSON → CBOR | Décodage JSON → CBOR | Verdict |
62
+ | --------------------------------------- | --------------------------- | -------------------- | --------------------- |
63
+ | 64 Ko d'octets | 87 452 o → 65 582 o (−25 %) | 541 µs → 0,9 µs | **CBOR, sans débat** |
64
+ | 1 Mo d'octets | 1,40 Mo → 1,05 Mo (−25 %) | 9 488 µs → 1 µs | **CBOR, sans débat** |
65
+ | 4 Ko d'octets, réception navigateur | −25 % | 26 454 ns → 792 ns | **CBOR** |
66
+ | objet de supervision (nombres, statuts) | 1 541 o → 1 172 o (−24 %) | 7,9 µs → 11,1 µs | CBOR plus LENT à lire |
67
+ | objet de santé d'une socket | 1 526 o → 1 256 o (−18 %) | 9,5 µs → 18,2 µs | CBOR plus LENT à lire |
68
+
69
+ D'où les règles :
70
+
71
+ - **Le serveur passe en `codec: "cbor"` dès que des octets circulent** — sans lui, un `Uint8Array`
72
+ part en `{"0":1,"1":2,…}` sans un message (§9). Le navigateur qui OFFRE le codec en profite ; celui
73
+ qui ne l'offre pas reçoit quand même des `Uint8Array` (convention `$bytes` en texte).
74
+ - **N'offre le codec que si des octets circulent** : sur des objets ordinaires, `JSON.parse` est
75
+ natif au moteur et plus rapide ; le codec pèse ~5,5 Ko compressés à télécharger. CBOR n'est
76
+ jamais le défaut.
77
+ - 🔴 **Seul `Uint8Array` voyage en octets.** Un `Int16Array`, un `Float32Array`, une `DataView` ou
78
+ un `ArrayBuffer` posé tel quel dans la charge est **refusé** par le codec binaire (`cbor`, ou
79
+ `$bytes` côté texte) : le navigateur lève à `publish`, le serveur ne livre pas la trame et le
80
+ journalise une fois. Le message nomme le geste. Une série de 16 à 64 bits (`Int16Array`,
81
+ `Float32Array`, `Float64Array`, `Uint32Array`…) passe par les aides du framework, exportées par
82
+ `nodefony` ET `nodefony/client` : `toLittleEndianBytes(série)` écrit le boutisme (vue sans
83
+ copie sur une machine petit-boutiste), `fromLittleEndianBytes(octets, Int16Array)` relit et
84
+ réaligne la vue reçue — qui peut commencer à un décalage impair. Ne recode pas ces deux boucles.
85
+ - **Des octets ne gagnent pas par nature — mesure ta donnée.** 1 024 compteurs écrits en `u32`
86
+ (4 096 octets) pèsent 4 114 o en CBOR contre 3 128 o en JSON, où de petits entiers s'écrivent
87
+ court. Choisis la largeur la plus ÉTROITE qui tient la plage (16 bits plutôt que 32), ou garde des
88
+ nombres.
89
+ - **Tasse les booléens et les petites valeurs** : 1 000 tirs d'une mesure en bits tassés = 142 o,
90
+ contre 2 025 o en JSON.
91
+ - **Réduis côté serveur avant de publier** : une fenêtre de capteur résumée (min, max, moyenne)
92
+ par période = 176 o en CBOR contre 307 o en JSON — et c'est la cadence du serveur, pas celle du
93
+ capteur, qui fixe le débit (`nodefony-add-realtime-channel`, « état ou événement »).
94
+ - **La bande passante d'un écran JSON se gagne par la donnée, pas par le codec** : réduction côté
95
+ serveur et cadence adaptative (skill `nodefony-add-realtime-channel`, sa page « production », §5). CBOR y gagnerait
96
+ ~20 % d'octets et y perdrait en temps de lecture.
97
+
98
+ ## 3. Le modèle — une trame comme les autres
99
+
100
+ - Une trame qui porte des octets est une **notification JSON-RPC ordinaire** dont la charge est, ou
101
+ contient, un `Uint8Array`. Il n'y a pas de seconde grammaire.
102
+ - Le **codec** ne dit que la forme sur le fil : en CBOR, les octets voyagent tels quels ; sur une
103
+ connexion restée en texte, sous la convention `{"$bytes":"<base64url>"}`, que le client relit en
104
+ `Uint8Array` sans rien importer.
105
+ - Le codec se **négocie** par connexion : le navigateur l'offre, le serveur choisit et l'annonce
106
+ dans `realtime:welcome`. Un client qui ne l'offre pas reste en texte — rien ne casse. Un canal
107
+ suivi par des connexions CBOR et texte paie un encodage par codec, jamais un par abonné.
108
+ - Parce que c'est la même trame, les octets d'un canal passent par **les mêmes gardes** que le
109
+ texte : politique du canal, rôles, audit, sonde.
110
+
111
+ ## 4. Mise en place — une ligne par côté
112
+
113
+ Serveur (`nodefony.config.ts`) : `use("@nodefony/realtime", { codec: "cbor" })` — la variable
114
+ `NF_REALTIME_CODEC` le surcharge.
115
+
116
+ | Serveur | Navigateur offre CBOR | Fil retenu | Octets reçus en |
117
+ | ------------------------ | --------------------- | ---------- | ---------------------------- |
118
+ | `codec: "cbor"` | oui | CBOR | `Uint8Array` |
119
+ | `codec: "cbor"` | non | texte | `Uint8Array` (via `$bytes`) |
120
+ | `codec: "json"` (défaut) | oui ou non | texte | 🔴 objet déformé `{"0":1,…}` |
121
+
122
+ Navigateur — le codec est une **OFFRE**, faite à la création de la socket (`CBOR_FRAME_CODEC`, de
123
+ `nodefony/cbor`). La démo binaire ouvre ainsi la sienne :
124
+
125
+ <!-- excerpt: demo/front/frontend/src/binary/binaryClient.ts.tpl -->
126
+
127
+ ```ts
128
+ export function openBinary(
129
+ cbor: boolean,
130
+ transport?: RealtimeTransportFactory,
131
+ ) {
132
+ return NodefonySocket.fromContract<BinaryContract>(
133
+ {
134
+ url: BINARY_SOCKET,
135
+ autoReconnect: false,
136
+ codec: cbor ? nodefonyCbor.CBOR_FRAME_CODEC : undefined,
137
+ },
138
+ transport,
139
+ );
140
+ }
141
+ ```
142
+
143
+ Avec `NodefonySocket.shared({ url, codec })`, l'offre ne compte qu'à la **PREMIÈRE** création pour
144
+ cette adresse : une seconde demande rend l'instance existante, réglages compris (§9). Importe le
145
+ codec depuis `nodefony/cbor` seulement dans les pages qui en ont besoin : il reste hors du paquet
146
+ client tant qu'on ne l'importe pas.
147
+
148
+ ## 5. Publier et recevoir
149
+
150
+ Le serveur met un `Uint8Array` DANS la charge, et publie comme d'habitude — ici la fabrique d'un
151
+ canal de la démo, qui tourne au premier abonné et s'arrête au départ du dernier :
152
+
153
+ <!-- excerpt: demo/server/nodefony/controllers/BinaryController.ts.tpl -->
154
+
155
+ ```ts
156
+ @RealtimeChannel(VIBRATION_CHANNEL)
157
+ vibrationChannel(
158
+ _channel: string,
159
+ publish: ContractRealtimePublish<BinaryContract>,
160
+ ): () => void {
161
+ let seq = 0;
162
+ const timer = setInterval(() => {
163
+ seq += 1;
164
+ // Petit-boutiste écrit EXPLICITEMENT : la page lira de même, quelle
165
+ // que soit la machine qui l'exécute.
166
+ const samples = new Uint8Array(VIBRATION_SAMPLES * 2);
167
+ const view = new DataView(samples.buffer);
168
+ ```
169
+
170
+ Hors d'une fabrique (service, tâche, minuteur) : `getRealtimeHub().publish(canal, { seq, samples })`.
171
+ Côté navigateur, rien ne change : `socket.subscribe(canal)` puis `socket.on(canal, h)` — la charge
172
+ reçue porte un `Uint8Array`.
173
+
174
+ - `subscribe(canal)` prend **un seul argument** ; le handler se pose par `on(canal, handler)`.
175
+ - Une charge est un objet, un tableau, des octets, ou absente — jamais un scalaire
176
+ (`publish("c", 42)` lève une `TypeError` qui nomme le canal).
177
+ - Un `Buffer` de Node est un `Uint8Array` : il part en octets, jamais en `{type:"Buffer",data:[…]}`.
178
+ - Une `Date` revient en chaîne et un `bigint` est refusé — exactement comme en JSON.
179
+ - **Écris l'ordre des octets EXPLICITEMENT** (`setInt16(i, v, true)` : petit-boutiste) : la page
180
+ doit lire de même, quelle que soit la machine.
181
+
182
+ **Le client pousse des octets** — même règle dans l'autre sens : un `Uint8Array` dans les paramètres
183
+ (`socket.request(action, { bytes })` vers une action, `socket.emit(nom, { samples })` vers une
184
+ entrée `@RealtimeInbound`). Côté serveur, **valide la forme avant d'y toucher** : la démo binaire
185
+ reçoit le dessin de la page par l'action `binary:digest`, dont le schéma
186
+ `z.strictObject({ bytes: z.instanceof(Uint8Array) })` refuse tout ce qui n'est pas un `Uint8Array`
187
+ — un objet forgé ne passe pas pour des octets. Borne la longueur avant d'allouer. Un script Node
188
+ (passerelle de sonde) offre le codec comme le navigateur, à la création de sa socket.
189
+
190
+ ## 6. Lire des octets sans se tromper
191
+
192
+ - **Une série homogène se relit par `fromLittleEndianBytes(octets, Int16Array)`** (`nodefony/client`) :
193
+ elle vérifie la longueur, recopie si la vue est mal alignée, retourne les octets sur une machine
194
+ gros-boutiste. La boucle `DataView` ci-dessous ne sert qu'à un format MIXTE (champs de tailles
195
+ différentes dans les mêmes octets).
196
+ - **`DataView` : passe la tranche entière.** Un `Buffer` de Node — et une vue décodée — ne commence
197
+ pas à 0 dans son tampon : `new DataView(data.buffer)` lit des octets qui ne sont pas les tiens.
198
+
199
+ <!-- excerpt: demo/front/frontend/src/binary/binaryClient.ts.tpl -->
200
+
201
+ ```ts
202
+ export function readSamples(bytes: Uint8Array): Int16Array {
203
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
204
+ const samples = new Int16Array(bytes.byteLength / 2);
205
+ for (let i = 0; i < samples.length; i += 1) {
206
+ samples[i] = view.getInt16(i * 2, true);
207
+ }
208
+ return samples;
209
+ }
210
+ ```
211
+
212
+ - Écris ton décodeur sur `Uint8Array` : il sert tel quel dans le navigateur et côté serveur.
213
+ - Si tu appelles toi-même `decodeCbor()` sur un tampon que tu réutilises ensuite, **copie** les
214
+ octets décodés à garder (`slice()`) : ce sont des vues sur ce tampon.
215
+ - Ne fais jamais confiance à des octets reçus : borne les longueurs avant d'allouer.
216
+
217
+ ## 7. Canal ou flux brut ?
218
+
219
+ 🔴 **Le flux brut est HORS des gardes.** Une trame reçue par `onRealtimeBinary(data)` ne passe ni
220
+ par le verrou de frame ni par les politiques d'action et de canal : seule la connexion a franchi
221
+ l'origine, l'authentification et le pare-feu. Si tu l'emploies, **toute décision d'accès est à toi**
222
+ — lis l'identité par `RequestContext.getAuthenticatedUser()` et refuse explicitement.
223
+
224
+ 🔴 **Le flux brut n'existe pas sur une connexion CBOR** : toute trame binaire y est une frame du
225
+ codec. `onRealtimeBinary()` n'y est jamais appelée ; `sendBinaryToClient()` (serveur) et `socket.sendBinary()` (navigateur) rendent
226
+ `false`.
227
+
228
+ Il n'a plus de raison de performance : un canal CBOR décode 1 Mo en ~1 µs. **Préfère un canal +
229
+ `Uint8Array`** — même politique, mêmes gardes, même bus entre serveurs.
230
+
231
+ ## 8. Le piège du `$bytes` forgé
232
+
233
+ Quand le serveur a `codec: "cbor"` et qu'une connexion est restée en texte, un objet de la forme
234
+ **exacte** `{ $bytes: "<chaîne>" }` (seule clé, valeur chaîne) est réservé aux octets. L'encodeur
235
+ **refuse** d'en envoyer un qui ne vient pas d'octets : un client texte le relirait en octets, un
236
+ client CBOR en objet. La démo le montre — seul, il lève une `TypeError` à la ligne fautive ; avec
237
+ une seconde clé, il part (et c'est le schéma du serveur qui refuse ce qui n'est pas un
238
+ `Uint8Array`) :
239
+
240
+ <!-- excerpt: demo/front/frontend/src/binary/binaryClient.ts.tpl -->
241
+
242
+ ```ts
243
+ const forged = extraKey
244
+ ? { $bytes: "AQID", note: "deux clés" }
245
+ : { $bytes: "AQID" };
246
+ await socket.request("binary:digest", { bytes: forged });
247
+ ```
248
+
249
+ - Navigateur : `emit` / `publish` / `request` **lèvent** à la ligne fautive.
250
+ - Serveur : la publication n'atteint pas les abonnés texte, une ligne `ERROR` par publication. Un
251
+ tel objet **conservé** (état renvoyé à chaque abonné) est refusé à chaque envoi : renomme la clé.
252
+ - Avec `codec: "json"`, `$bytes` est une clé comme une autre : la règle ne s'applique pas.
253
+
254
+ ## 9. Plusieurs serveurs, bornes, et ce qui arrive SANS message
255
+
256
+ - **Plusieurs serveurs** : deux conditions, sinon une partie des écrans reçoit autre chose — le
257
+ canal est DIFFUSÉ (`@RealtimeBroadcast`, skill `nodefony-add-realtime-channel`), et `codec:
258
+ "cbor"` est réglé À L'IDENTIQUE sur chaque serveur (un serveur resté en JSON déforme en
259
+ silence les octets qu'il sert).
260
+ - **Bus** : les octets traversent `redis` et `cluster` **intacts**, scellés comme le texte si
261
+ `backplane.secret` est posé. Mesuré sur Redis, charge de 16 Ko : −25 % d'octets, +30 % de débit,
262
+ −11 % de latence médiane — pour +15 à 30 % de CPU par message. Le bus IPC de `nodefony cluster`
263
+ passe en sérialisation avancée tout seul (~1 µs par saut).
264
+ - **Taille d'une trame entrante** : bornée par `websocket.maxPayload` de `@nodefony/http` (1 Mio
265
+ par défaut) ; au-delà, la socket se ferme en `1009`. Un besoin plus gros est un fichier : route
266
+ HTTP. Ne relève la borne qu'en connaissance de cause (toute l'application en hérite).
267
+ - **Débit** : un flux audio (~50 trames/s) doit passer sous `maxFramesPerSecond` si tu l'actives.
268
+ - **Serveur resté en `codec: "json"`** : un `Uint8Array` part en `{"0":1,"1":2,…}` — aucune erreur.
269
+ Le piège n°1 : vérifier la clé `codec` avant tout autre diagnostic.
270
+ - **`codec` passé à un `shared()` qui n'est pas le premier** : l'instance existante revient,
271
+ réglages compris ; la connexion reste en texte (un avertissement hors production seulement).
272
+ - **`sendBinary()` sur un transport fermé** : rend `false`, compté dans `framesUnsent`, sans notice.
273
+ - **Ne déduis jamais le codec de l'en-tête `Sec-WebSocket-Protocol`** renvoyé au handshake : seule
274
+ `socket.codec` fait foi.
275
+ - Un message d'erreur ou une notice qui parle d'octets : sa cause et ton geste sont dans
276
+ [`references/messages.md`](references/messages.md).
277
+
278
+ ## 10. Prouver
279
+
280
+ - `socket.codec` vaut `"nodefony.jsonrpc.cbor.v1"` une fois l'accueil reçu ;
281
+ `"nodefony.jsonrpc.v1"` = texte.
282
+ - Compteurs côté navigateur : `socket.binaryFramesDiscarded`, `socket.malformedFramesDiscarded`
283
+ doivent rester à 0.
284
+ - Un test publie un `Uint8Array` sur un canal et vérifie que l'abonné reçoit un `Uint8Array`
285
+ **identique** (`instanceof Uint8Array` et mêmes octets) — sous CBOR ET sous texte.
286
+ - Une décision de représentation (tableau typé, bits tassés, réduction) se **mesure** sur TA donnée
287
+ : compare la taille de la trame encodée par `encodeCbor` (`nodefony/cbor`) et par `JSON.stringify`
288
+ — le spectre en `u32` a montré que l'intuition peut perdre.
289
+ - Pour lire le fil à l'œil dans l'inspecteur réseau : `NF_REALTIME_CODEC=json` remet le serveur en
290
+ texte. Exception de débogage : la taille des trames et la contre-pression changent, aucune mesure
291
+ prise ainsi ne vaut pour la production.
292
+
293
+ ## 11. Voisins
294
+
295
+ | Besoin | Skill |
296
+ | ----------------------------------------------------- | ------------------------------- |
297
+ | Le canal qui porte les octets : le créer, le protéger | `nodefony-add-realtime-channel` |
298
+ | Un fichier, une pièce jointe, un flux HTTP | `nodefony-add-crud` |
299
+ | Un service gRPC (protobuf, autre usage que la socket) | `nodefony-add-grpc-service` |
300
+ | Voir l'écran, lire les trames depuis la page | `nodefony-browser` |
301
+ | Une trame qui n'arrive plus, une socket qui se ferme | `nodefony-troubleshoot` |
@@ -0,0 +1,20 @@
1
+ # Chaque message, sa cause, ton geste — des octets qui ne passent pas
2
+
3
+ > Chargé depuis `SKILL.md` quand un message d'erreur ou une notice parle d'octets, de CBOR, de
4
+ > `$bytes` ou de trame binaire. Les textes entre guillemets sont recopiés du code (un test du
5
+ > framework refuse qu'ils en divergent) ; `<…>` marque une partie variable.
6
+
7
+ | Où | Message | Cause | Geste |
8
+ | -------------------------------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
9
+ | exception (navigateur) / journal `ERROR` (serveur) | `codec JSON : objet {"$bytes": …} non issu d'octets` | objet applicatif de la forme exacte du marqueur, vers une connexion texte | envoyer un vrai `Uint8Array`, ou renommer / ajouter une clé |
10
+ | journal `ERROR` serveur | `rpc <action> : réponse non encodable` | une `@RealtimeAction` a rendu une valeur que le codec refuse (`$bytes` forgé, cycle, `bigint`) ; le client reçoit `-32603` générique | corriger la valeur rendue |
11
+ | exception | `Impossible d'encoder un BigInt — JSON.stringify le refuse aussi` | `bigint` dans la charge, sous CBOR | convertir en chaîne ou en nombre |
12
+ | exception | `Impossible d'encoder une structure circulaire — JSON.stringify la refuse aussi` | cycle dans la charge | aplatir la donnée |
13
+ | notice navigateur (`-32700`) | `Trame binaire écartée : en JSON-RPC texte elle n'a pas de canal` | le serveur écrit du binaire brut sur une connexion texte sans `onBinary()` | lire par `onBinary()`, ou passer par un canal + CBOR |
14
+ | notice navigateur (`-32700`) | `Trame illisible écartée : elle ne se décode pas en <protocole>` | trame que le codec ne décode pas | côté serveur : chercher qui écrit hors du codec |
15
+ | notice navigateur | `Codec « <protocole> » annoncé sans avoir été offert : la connexion reste en JSON-RPC texte` | serveur qui annonce un codec que ce client n'a pas offert | mettre client et serveur à la même version |
16
+ | notice navigateur | `Flux binaire brut indisponible sous <protocole>` | `onBinary()` / `sendBinary()` sur une connexion CBOR | publier les octets sur un canal |
17
+ | journal `WARNING` serveur, une fois par connexion | `WS realtime : trame binaire reçue et écartée` | binaire brut reçu sur une connexion texte, sans `onRealtimeBinary()` | passer par un canal + CBOR, ou surcharger `onRealtimeBinary()` (hors gardes) |
18
+ | journal `WARNING` serveur, une fois par connexion | `WS realtime : sendBinaryToClient refusé` | `sendBinaryToClient()` sur une connexion CBOR | publier les octets sur un canal |
19
+ | journal `WARNING` serveur, une fois par nature | `WS realtime : trame binaire illisible pour <protocole> reçue et écartée` | trame CBOR malformée ou tronquée ; **aucune réponse** n'est renvoyée (pas d'oracle pour un fuzzer), connexion conservée | côté client : chercher qui écrit hors du codec |
20
+ | exception (`CborError`) | `CBOR refusé à l'octet <n> : <raison>` | `decodeCbor()` appelé par l'application sur des octets invalides ; `err.offset` dit où | valider la source ; ne jamais faire confiance à des octets reçus |
@@ -0,0 +1,59 @@
1
+ # Les mesures du binaire — de quoi décider, avec leurs conditions
2
+
3
+ > Chargé depuis `SKILL.md` §2 quand une décision de codec ou de représentation se discute. Toutes
4
+ > les valeurs viennent des bancs du framework (Node 26) ; une valeur sans ses conditions ne vaut
5
+ > rien — elles sont donc écrites à côté. Les pages installées qui portent une partie de ces
6
+ > chiffres : `nodefony/docs/cbor.md` (« Performance & mémoire »), `nodefony/docs/client.md`
7
+ > (« Choisir le codec de trame »). **Une mesure de ce tableau ne remplace pas la tienne** : ta
8
+ > donnée a sa forme, mesure-la (`SKILL.md` §10).
9
+
10
+ ## 1. Codec : JSON (octets en base64) contre CBOR — trames réelles, médiane de 7 séries
11
+
12
+ | Charge | Taille JSON → CBOR | Décodage JSON → CBOR | Lecture |
13
+ | --------------------------------------- | --------------------------------- | -------------------- | -------------------------------------- |
14
+ | 64 Ko d'octets | 87 452 o → 65 582 o (−25 %) | 541 µs → 0,9 µs | CBOR écrase JSON |
15
+ | 1 Mo d'octets | 1 398 174 o → 1 048 622 o (−25 %) | 9 488 µs → 1 µs | CBOR écrase JSON |
16
+ | 4 Ko d'octets, réception navigateur | −25 % d'octets | 26 454 ns → 792 ns | ≈ 33× plus rapide |
17
+ | objet de supervision (nombres, statuts) | 1 541 o → 1 172 o (−24 %) | 7,9 µs → 11,1 µs | CBOR plus court, mais plus lent à lire |
18
+ | objet de santé d'une socket | 1 526 o → 1 256 o (−18 %) | 9,5 µs → 18,2 µs | CBOR plus court, mais plus lent à lire |
19
+ | message de chat (salon) | 115 o → 89 o | — | gain de taille faible sur du texte |
20
+ | notification privée de chat | 183 o → 150 o | — | idem |
21
+
22
+ Le codec CBOR pèse environ 5,5 Ko compressés côté navigateur (sous-chemin `nodefony/cbor`, chargé
23
+ seulement par les pages qui l'importent). Un accélérateur natif de décodage CBOR a été essayé : il
24
+ n'apportait rien face à `JSON.parse` natif sur les objets, et CBOR pur suffit sur les octets.
25
+
26
+ ## 2. Représentation de la donnée — la forme compte plus que le codec
27
+
28
+ 🔴 Seul `Uint8Array` voyage en octets : un `Int16Array` ou un `Float32Array` posé tel quel est
29
+ refusé par le codec binaire. Toutes les lignes ci-dessous publient des OCTETS (écrits par `DataView`).
30
+
31
+ | Donnée | Comparaison | Lecture |
32
+ | ---------------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------- |
33
+ | 1 024 compteurs écrits en `u32` (octets) | 4 114 o (CBOR) contre 3 128 o (JSON) | **le binaire PERD** : petits entiers courts en texte, `u32` = 4 octets fixes |
34
+ | 1 000 tirs d'une mesure, bits tassés | 142 o contre 2 025 o (JSON) | tasser gagne d'un ordre de grandeur |
35
+ | comptes agrégés d'une mesure | 30 o | publier l'agrégat, pas la matière brute |
36
+ | fenêtre de capteur réduite (min, max, moyenne) | 176 o (CBOR) contre 307 o (JSON) | réduire côté serveur, publier à la cadence du serveur |
37
+
38
+ ## 3. Le bus entre serveurs
39
+
40
+ | Bus | Condition | CBOR contre JSON + base64 |
41
+ | ---------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
42
+ | Redis scellé | charge de 16 Ko, médianes | −25 % d'octets, +30 % de débit, −11 % de latence médiane, +15 à 30 % de CPU par message (ramasse-miettes : un tampon hors tas par message) |
43
+ | IPC de `nodefony cluster` (« advanced ») | maître + ouvriers | −23 % de débit à 64 o, ×2,2 à 64 Ko ; ~1 µs par saut — retenu partout, sans écart développement / production |
44
+
45
+ ## 4. Le coût du binaire pour qui ne s'en sert pas
46
+
47
+ - Le chemin binaire n'est armé QUE si un codec binaire est configuré : un parcours armé en
48
+ permanence coûtait +20 à +35 % sur le chemin texte, il a été écarté.
49
+ - Une application qui ne publie pas d'octets ne paie rien de plus (contrôle au premier niveau
50
+ seulement, sans codec).
51
+
52
+ ## 5. Bornes en vigueur
53
+
54
+ | Borne | Valeur | Effet au dépassement |
55
+ | ------------------------------------------------- | ---------------- | ------------------------------------------ |
56
+ | `websocket.maxPayload` (`@nodefony/http`) | 1 Mio par défaut | fermeture `1009` de la socket |
57
+ | `maxFramesPerSecond` (`@nodefony/http`) | `0` = désactivé | refus comptés, puis fermeture sur un solde |
58
+ | `backpressureCloseAfterDrops` (serveur et client) | 1 000 | fermeture (`1013` serveur, `4007` client) |
59
+ | `maxBufferedAmount` (navigateur) | `0` = désactivé | envois en trop jetés, comptés |