@nodefony/devkit 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/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +3 -3
- package/dist/nodefony/controllers/DevkitController.js +1 -1
- package/dist/nodefony/controllers/McpController.js +2 -2
- package/dist/nodefony/controllers/ProfileSessionController.js +1 -1
- package/dist/nodefony/mcp/profileRoute.js +28 -9
- package/dist/nodefony/service/DevkitService.js +6 -6
- package/docs/index.md +1 -1
- package/package.json +13 -13
- package/skills/nodefony-add-crud/SKILL.md +38 -23
- package/skills/nodefony-add-grpc-service/SKILL.md +179 -0
- package/skills/nodefony-add-grpc-service/scripts/call.mjs +218 -0
- package/skills/nodefony-add-realtime-binary/SKILL.md +301 -0
- package/skills/nodefony-add-realtime-binary/references/messages.md +20 -0
- package/skills/nodefony-add-realtime-binary/references/mesures.md +59 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +452 -105
- package/skills/nodefony-add-realtime-channel/references/contrat.md +22 -0
- package/skills/nodefony-add-realtime-channel/references/production.md +140 -0
- package/skills/nodefony-add-realtime-presence/SKILL.md +328 -0
- package/skills/nodefony-authentication/SKILL.md +2 -2
- package/skills/nodefony-browser/SKILL.md +37 -2
- package/skills/nodefony-browser/references/sondes.md +19 -0
- package/skills/nodefony-browser/scripts/audit.mjs +17 -2
- package/skills/nodefony-browser/scripts/inspect.mjs +108 -87
- package/skills/nodefony-browser/scripts/lib/actions.mjs +164 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +67 -11
- package/skills/nodefony-browser/scripts/lib/probes.mjs +108 -3
- package/skills/nodefony-browser/scripts/watch.mjs +27 -11
- package/skills/nodefony-dev/SKILL.md +31 -15
- package/skills/nodefony-dev/references/rapport-de-build.md +19 -0
- package/skills/nodefony-devops/SKILL.md +28 -21
- package/skills/nodefony-devops/references/dimensionnement.md +1 -1
- package/skills/nodefony-devops/references/frontal.md +5 -2
- package/skills/nodefony-devops/references/image.md +23 -15
- package/skills/nodefony-migrate-schema/SKILL.md +110 -20
- package/skills/nodefony-migrate-schema/references/verdicts.md +23 -20
- package/skills/nodefony-protect-route/SKILL.md +27 -32
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Appelle une méthode gRPC d'une application Nodefony, en protocole Connect
|
|
4
|
+
* (JSON sur HTTP) — la preuve qu'un service répond, sans `curl` ni guillemets.
|
|
5
|
+
*
|
|
6
|
+
* `curl -d '{"name":"Ada"}'` ne passe ni dans cmd.exe ni dans Windows PowerShell,
|
|
7
|
+
* et `grpcurl` n'est pas installé partout. Ce script n'emploie que `fetch` de
|
|
8
|
+
* Node : il marche à l'identique sur les trois plateformes, et il rend le CODE
|
|
9
|
+
* gRPC du refus (`unauthenticated`, `permission_denied`…), pas seulement un
|
|
10
|
+
* statut HTTP.
|
|
11
|
+
*
|
|
12
|
+
* `@usage` node node_modules/@nodefony/devkit/skills/nodefony-add-grpc-service/scripts/call.mjs greet.v1.GreetService/Greet --field name=Ada
|
|
13
|
+
* `@usage` node node_modules/@nodefony/devkit/skills/nodefony-add-grpc-service/scripts/call.mjs greet.v1.GreetService/Greet --file body.json
|
|
14
|
+
* `@option` --field - un champ du message, `nom=valeur` (répétable ; la valeur est une chaîne)
|
|
15
|
+
* `@option` --file - le message entier, lu dans un fichier JSON
|
|
16
|
+
* `@option` --header - un en-tête, `nom=valeur` (répétable), par exemple `authorization=Bearer …`
|
|
17
|
+
* `@option` --url - l'adresse de l'application (défaut http://127.0.0.1:5151)
|
|
18
|
+
* `@option` --json - la réponse brute, sérialisée, pour rechaîner
|
|
19
|
+
* `@requires` l'application démarrée
|
|
20
|
+
* `@output` le statut HTTP, puis le message reçu ou le code gRPC du refus
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
/** L'adresse d'une application de développement, sans TLS. */
|
|
27
|
+
const DEFAULT_URL = "http://127.0.0.1:5151";
|
|
28
|
+
|
|
29
|
+
/** Une méthode gRPC : `paquet.Service/Méthode`, sans barre de tête. */
|
|
30
|
+
const METHOD_RE = /^[A-Za-z_][\w.]*\.[A-Za-z_]\w*\/[A-Za-z_]\w*$/u;
|
|
31
|
+
|
|
32
|
+
const USAGE = `
|
|
33
|
+
Appelle une méthode gRPC d'une application Nodefony, en Connect (JSON sur HTTP).
|
|
34
|
+
|
|
35
|
+
call.mjs <paquet.Service/Méthode> [--field nom=valeur]... le message, champ par champ
|
|
36
|
+
call.mjs <paquet.Service/Méthode> --file message.json le message, depuis un fichier
|
|
37
|
+
call.mjs ... --header authorization=Bearer <jeton> un en-tête (répétable)
|
|
38
|
+
call.mjs ... --url http://127.0.0.1:5151 l'application visée
|
|
39
|
+
call.mjs ... --json la réponse brute
|
|
40
|
+
|
|
41
|
+
Codes de sortie : 0 réponse · 1 refus ou erreur gRPC · 64 mauvais usage · 69 application injoignable
|
|
42
|
+
`.trim();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Coupe `nom=valeur` au PREMIER `=` : une valeur peut en contenir (jeton base64).
|
|
46
|
+
*
|
|
47
|
+
* @param {string} pair - la paire saisie.
|
|
48
|
+
* @returns {[string, string] | null} le nom et la valeur, ou `null` si la forme est fausse.
|
|
49
|
+
*/
|
|
50
|
+
const splitPair = (pair) => {
|
|
51
|
+
const at = pair.indexOf("=");
|
|
52
|
+
if (at <= 0) return null;
|
|
53
|
+
return [pair.slice(0, at), pair.slice(at + 1)];
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Lit la ligne de commande.
|
|
58
|
+
*
|
|
59
|
+
* @param {string[]} argv - les arguments, sans `node` ni le script.
|
|
60
|
+
* @returns {{ method: string, url: string, body: Record<string, unknown>, headers: Record<string, string>, json: boolean } | { error: string }}
|
|
61
|
+
* la demande, ou l'erreur d'usage à afficher.
|
|
62
|
+
*/
|
|
63
|
+
export function parseArgs(argv) {
|
|
64
|
+
let method = null;
|
|
65
|
+
let url = DEFAULT_URL;
|
|
66
|
+
let json = false;
|
|
67
|
+
let file = null;
|
|
68
|
+
const fields = Object.create(null);
|
|
69
|
+
const headers = Object.create(null);
|
|
70
|
+
for (let i = 0; i < argv.length; i++) {
|
|
71
|
+
const arg = argv[i];
|
|
72
|
+
if (arg === "--json") {
|
|
73
|
+
json = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
arg === "--field" ||
|
|
78
|
+
arg === "--header" ||
|
|
79
|
+
arg === "--url" ||
|
|
80
|
+
arg === "--file"
|
|
81
|
+
) {
|
|
82
|
+
const value = argv[++i];
|
|
83
|
+
if (value === undefined) return { error: `${arg} attend une valeur` };
|
|
84
|
+
if (arg === "--url") url = value;
|
|
85
|
+
else if (arg === "--file") file = value;
|
|
86
|
+
else {
|
|
87
|
+
const pair = splitPair(value);
|
|
88
|
+
if (pair === null)
|
|
89
|
+
return { error: `${arg} attend nom=valeur, reçu « ${value} »` };
|
|
90
|
+
(arg === "--field" ? fields : headers)[pair[0]] = pair[1];
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg.startsWith("-")) return { error: `option inconnue : ${arg}` };
|
|
95
|
+
if (method !== null)
|
|
96
|
+
return { error: `une seule méthode par appel (reçu « ${arg} »)` };
|
|
97
|
+
method = arg.replace(/^\/+/u, "");
|
|
98
|
+
}
|
|
99
|
+
if (method === null) return { error: "la méthode à appeler manque" };
|
|
100
|
+
if (!METHOD_RE.test(method)) {
|
|
101
|
+
return {
|
|
102
|
+
error: `méthode « ${method} » : la forme est paquet.Service/Méthode, comme greet.v1.GreetService/Greet`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
let body = fields;
|
|
106
|
+
if (file !== null) {
|
|
107
|
+
if (Object.keys(fields).length !== 0)
|
|
108
|
+
return { error: "--file et --field s'excluent" };
|
|
109
|
+
try {
|
|
110
|
+
body = JSON.parse(readFileSync(file, "utf8"));
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return {
|
|
113
|
+
error: `--file ${file} : ${e instanceof Error ? e.message : String(e)}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { method, url: url.replace(/\/+$/u, ""), body, headers, json };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Envoie l'appel Connect unaire et lit la réponse.
|
|
122
|
+
*
|
|
123
|
+
* @param {{ method: string, url: string, body: unknown, headers: Record<string, string> }} request - la demande.
|
|
124
|
+
* @returns {Promise<{ status: number, payload: unknown }>} le statut HTTP et le corps décodé (texte brut s'il n'est pas JSON).
|
|
125
|
+
*/
|
|
126
|
+
export async function callConnect(request) {
|
|
127
|
+
const response = await fetch(`${request.url}/${request.method}`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
...request.headers,
|
|
131
|
+
"content-type": "application/json",
|
|
132
|
+
"connect-protocol-version": "1",
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify(request.body),
|
|
135
|
+
});
|
|
136
|
+
const text = await response.text();
|
|
137
|
+
let payload = text;
|
|
138
|
+
try {
|
|
139
|
+
payload = text === "" ? {} : JSON.parse(text);
|
|
140
|
+
} catch {
|
|
141
|
+
// corps non JSON (une page d'erreur HTML) : rendu tel quel
|
|
142
|
+
}
|
|
143
|
+
return { status: response.status, payload };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Le code gRPC d'un refus Connect (`{"code":"unauthenticated"}`), ou `null`.
|
|
148
|
+
*
|
|
149
|
+
* @param {unknown} payload - le corps décodé.
|
|
150
|
+
* @returns {string | null} le code textuel.
|
|
151
|
+
*/
|
|
152
|
+
const connectCode = (payload) =>
|
|
153
|
+
payload !== null &&
|
|
154
|
+
typeof payload === "object" &&
|
|
155
|
+
"code" in payload &&
|
|
156
|
+
typeof payload.code === "string"
|
|
157
|
+
? payload.code
|
|
158
|
+
: null;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Exécute la commande.
|
|
162
|
+
*
|
|
163
|
+
* @param {string[]} argv - les arguments.
|
|
164
|
+
* @returns {Promise<number>} le code de sortie.
|
|
165
|
+
*/
|
|
166
|
+
export async function main(argv) {
|
|
167
|
+
const request = parseArgs(argv);
|
|
168
|
+
if ("error" in request) {
|
|
169
|
+
process.stderr.write(`${request.error}\n\n${USAGE}\n`);
|
|
170
|
+
return 64;
|
|
171
|
+
}
|
|
172
|
+
let result;
|
|
173
|
+
try {
|
|
174
|
+
result = await callConnect(request);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
const cause =
|
|
177
|
+
e instanceof Error && e.cause instanceof Error
|
|
178
|
+
? e.cause.message
|
|
179
|
+
: String(e);
|
|
180
|
+
process.stderr.write(
|
|
181
|
+
`L'application ne répond pas sur ${request.url} (${cause}).\n` +
|
|
182
|
+
"Démarre-la : npx nodefony development — puis relance l'appel.\n",
|
|
183
|
+
);
|
|
184
|
+
return 69;
|
|
185
|
+
}
|
|
186
|
+
const { status, payload } = result;
|
|
187
|
+
if (request.json) {
|
|
188
|
+
process.stdout.write(`${JSON.stringify({ status, payload })}\n`);
|
|
189
|
+
} else if (status === 200) {
|
|
190
|
+
process.stdout.write(
|
|
191
|
+
`HTTP 200 · ${request.method}\n${JSON.stringify(payload, null, 2)}\n`,
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
const code = connectCode(payload);
|
|
195
|
+
process.stdout.write(
|
|
196
|
+
code === null
|
|
197
|
+
? `HTTP ${status} · ${request.method} — pas une réponse gRPC :\n${typeof payload === "string" ? payload : JSON.stringify(payload, null, 2)}\n`
|
|
198
|
+
: `HTTP ${status} · ${request.method} — refus gRPC « ${code} »${
|
|
199
|
+
typeof payload === "object" &&
|
|
200
|
+
payload !== null &&
|
|
201
|
+
"message" in payload
|
|
202
|
+
? ` : ${String(payload.message)}`
|
|
203
|
+
: ""
|
|
204
|
+
}\n`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return status === 200 ? 0 : 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Exécuté directement (et non importé par son test) : rendre le code. Chemins
|
|
211
|
+
// RÉELS des deux côtés — lancé par un lien (`/var/…` sous macOS), la
|
|
212
|
+
// comparaison brute échouait et le script ne faisait rien.
|
|
213
|
+
if (
|
|
214
|
+
process.argv[1] !== undefined &&
|
|
215
|
+
realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
216
|
+
) {
|
|
217
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
218
|
+
}
|
|
@@ -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 |
|