Nodefony 10.0.0-beta.6
Framework Node.js fullstack — HTTP & WebSocket, même contexte

Codec CBOR (nodefony/cbor)

stablenodefony (cœur)mis à jour 2026-10-29

CBOR est un JSON binaire : les mêmes objets, tableaux, nombres et textes, en moins d’octets, et des octets bruts (une image, un son) sans les convertir en texte. Nodefony embarque son propre codec, isomorphe (navigateur et serveur, même code), sans dépendance, qui rend exactement ce que JSON rendrait — seuls les octets font exception. Ancré sur src/nodefony/src/realtime/codec/.

📍 Documentation › @nodefony/core › Codec CBOR

🧠 Le modèle mental — JSON, écrit autrement#

Pense à un formulaire qu’on peut remplir de deux façons : en toutes lettres (JSON, lisible par un humain) ou en abrégé (CBOR, lisible par une machine, plus court). Le formulaire est le même ; seule l’écriture change.

C’est la promesse du codec, et elle est stricte : pour toute valeur v,

decodeCbor(encodeCbor(v))   ≡   JSON.parse(JSON.stringify(v))

Une Date devient sa chaîne ISO, NaN devient null, une Map devient {} — comme en JSON. Une seule exception : un Uint8Array voyage en octets et revient en Uint8Array, là où JSON ne sait pas le porter.

📖 Lexique#

Qu’est-ce qu’un codec binaire — et pourquoi il compte en temps réel#

Une socket temps réel échange des milliers de petits messages. En JSON, chaque message est du texte : des guillemets, des noms de clés répétés, des nombres écrits chiffre par chiffre. Et des octets (une photo, un échantillon de capteur) doivent d’abord être convertis en base64 — un tiers plus lourds, et un calcul à chaque message.

CBOR retire ce surcoût : un message ordinaire pèse 30 à 45 % de moins, et des octets passent tels quels. En échange, encoder un petit message prend plus de calcul qu’en JSON, que le moteur JavaScript exécute en code natif. D’où la règle : JSON reste le format par défaut ; CBOR est un choix, utile quand les messages portent du binaire ou quand la bande passante compte.

La vision Nodefony#

Trois engagements, décidés dans l’ADR-0022 :

  1. JSON est la référence. Des clients JSON et CBOR peuvent écouter le même canal ; le relais entre serveurs parle JSON ; un type ne peut pas dépendre du fil. Tous reçoivent donc la même chose — ce que JSON rendrait.
  2. Rien en plus de la norme, rien en moins. Le codec suit RFC 8949 à la lettre là où elle s’applique, et restreint explicitement ce qu’il accepte au modèle JSON plus les octets.
  3. Isomorphe et autonome. Aucune dépendance, aucun eval (la politique de sécurité du contenu par défaut l’interdit), aucun Buffer : le même fichier sert le navigateur et Node.

🚀 Démarrage rapide#

Encoder et décoder#

import { encodeCbor, decodeCbor, CborError } from "nodefony/cbor";

const bytes = encodeCbor({
  jsonrpc: "2.0",
  method: "capteur:mesures",
  params: { seq: 42, at: new Date(0), data: new Uint8Array([1, 2, 255]) },
});

const message = decodeCbor(bytes);
// → { jsonrpc: "2.0", method: "capteur:mesures",
//     params: { seq: 42, at: "1970-01-01T00:00:00.000Z", data: Uint8Array [1, 2, 255] } }

try {
  decodeCbor(new Uint8Array([0xc1, 0x00])); // une étiquette : hors du profil
} catch (error) {
  if (error instanceof CborError) console.log(error.message, error.position);
}

Ce qu’on observe#

Le même import, côté navigateur#

nodefony/cbor est un sous-chemin à part : nodefony/client ne l’inclut pas, et seule l’application qui l’importe le télécharge (environ 5,5 Ko compressés, mesurés par le budget du build). Le code est identique côté serveur et côté navigateur.

Ce que fait l’encodeur — l’algorithme de JSON.stringify#

encodeCbor() (src/nodefony/src/realtime/codec/encode.ts#encodeCbor) suit l’algorithme SerializeJSONProperty d’ECMA-262, valeur par valeur :

Valeur JavaScript Ce qui est encodé
objet avec une méthode toJSON (dont Date) le résultat de toJSON()
Number, String, Boolean en enveloppe la primitive
NaN, Infinity, -Infinity null
-0 0
undefined, fonction, symbole dans un tableau null
undefined, fonction, symbole dans un objet la clé est omise
Map, Set, objet sans propriété énumérable {}
bigint, structure circulaire refus (CborError), comme JSON
Uint8Array (et Buffer sous Node) chaîne d’octets — la seule exception à JSON
racine undefined, fonction ou symbole refus : un codec doit rendre une trame
caractère UTF-16 isolé (substitut sans sa paire) U+FFFD, comme un TextEncoder ou une trame texte

L’ordre des clés est celui de Object.keys, jamais trié. Les nombres prennent la forme la plus courte qui les conserve : entier sur 1 à 9 octets, flottant sur 16, 32 ou 64 bits.

Un Buffer Node est traité avant sa méthode toJSON, qui rendrait sinon { type: "Buffer", data: [...] } — précisément la déformation qu’un octet ne doit pas subir.

Ce que fait le décodeur — le profil JSON + octets#

decodeCbor() (src/nodefony/src/realtime/codec/decode.ts#decodeCbor) lit tout CBOR bien formé qui reste dans le modèle, et refuse le reste :

Reçu Résultat
entiers, flottants 16/32/64 bits, textes les valeurs JavaScript correspondantes
chaîne d’octets un Uint8Array, vue sans copie sur l’entrée
tableaux et objets, définis ou indéfinis acceptés (§3.2 : un encodeur tiers peut écrire en flux)
NaN, ±Infinity null
undefined null dans un tableau, clé omise dans un objet
entier au-delà de Number.MAX_SAFE_INTEGER le Number le plus proche, comme JSON.parse
clé __proto__ propriété propre, comme JSON.parse — jamais le prototype
étiquette (type majeur 6) refus
clé non textuelle, clé en double refus
texte UTF-8 invalide refus (§5.3.1)
octets en trop après la valeur refus

Les options#

ICborDecodeOptions (src/nodefony/src/realtime/codec/decode.ts#ICborDecodeOptions) porte une seule option aujourd’hui, maxDepth : la profondeur d’imbrication maximale, 64 par défaut, un entier ≥ 1. Les options passent par un objet, pour en ajouter sans rupture.

La vue sans copie — ce qu’elle t’engage#

Un Uint8Array décodé regarde le tampon que tu as passé à decodeCbor. Si tu réutilises ce tampon (un tampon de réception recyclé), copie d’abord les octets que tu gardes : new Uint8Array(message.params.data). C’est ce qui rend le décodage du binaire quasi gratuit : une image de 1 Mo se décode en environ 1 µs.

🔐 Sécurité — un message hostile ne passe pas#

Un décodeur reçoit des octets d’inconnus. Le codec applique les considérations de sécurité de la RFC 8949 (§10) :

Chaque refus lève une CborError (src/nodefony/src/realtime/codec/errors.ts#CborError), qui étend TypeError — la classe que lèvent déjà JSON.stringify et un TextDecoder strict — et porte la position de l’octet en cause.

⚡ Performance & mémoire#

Mesuré sur une machine de développement (Node 26, médiane de sept mesures, deux passages) :

Message JSON (octets) CBOR (octets) Encodage CBOR Décodage CBOR
court (3 champs) 91 68 ~0,9 µs ~0,8 µs
50 objets de 6 champs 4 754 3 250 ~35 µs ~40 µs
500 nombres à virgule 2 620 1 399 ~16 µs ~17 µs
binaire 64 Ko 87 445 (base64) 65 587 ~35 µs ~1 µs

À retenir : JSON natif reste plus rapide sur un petit message ordinaire ; CBOR gagne en octets partout, et sur le binaire de très loin (aucune conversion en base64, décodage sans copie).

Côté mémoire :

Le banc qui produit ces chiffres se rejoue : node .claude/skills/nodefony-load-test/scripts/micro/micro-cbor-codec.mjs.

Sur la socket — CBOR_FRAME_CODEC#

Le codec de frame de la socket temps réel (realtime/codec/frameCodec.ts#CBOR_FRAME_CODEC, sous-protocole nodefony.jsonrpc.cbor.v1) s’importe du même sous-chemin et se passe à la socket du navigateur :

import { NodefonySocket } from "nodefony/client";
import { CBOR_FRAME_CODEC } from "nodefony/cbor";

const socket = NodefonySocket.shared({
  url: "/realtime",
  codec: CBOR_FRAME_CODEC,
});
console.log(socket.codec); // « nodefony.jsonrpc.v1 » tant que l'accueil n'a rien annoncé

Le client l’offre au handshake, le serveur choisit et l’annonce dans realtime:welcome ; une connexion restée en texte porte les octets sous la convention {"$bytes": …} (IFrameCodec.textCodec), annoncée par le champ bytes de l’accueil : un client sans codec les relit aussi en Uint8Array. Le déroulé, les cas limites et la mesure texte contre CBOR sont dans Client isomorphe — Choisir le codec de trame.

Le reste du chemin vit dans le module temps réel :

📜 Normes appliquées#

Norme Ce qu’on en applique
RFC 8949 §3 encodage des types majeurs 0 à 5 et 7 ; longueurs indéfinies lues (§3.2)
RFC 8949 §4.1 sérialisation préférée : la forme la plus courte, flottants compris
RFC 8949 §5.3.1, §5.6 UTF-8 valide exigé ; clés en double refusées
RFC 8949 §10 bornes contre un message hostile
RFC 8949, appendices A et F tous les exemples applicables servent de tests, octet pour octet
ECMA-262 SerializeJSONProperty l’algorithme que suit l’encodeur
RFC 6455 §1.9 CBOR_PROTOCOL (src/nodefony/src/realtime/codec/index.ts#CBOR_PROTOCOL) est le nom du sous-protocole WebSocket

Ce qui n’est pas visé : l’encodage déterministe de la §4.2.1, qui trie les clés — il changerait leur ordre, que JSON conserve.

⚠️ Pièges (symptôme → cause → correction)#

🧪 Tests & couverture#

Familles présentes :

🔗 Pour aller plus loin#