Le modèle en 10 minutes
Cinq notions, lues dans l'ordre : le message, le catalogue, le broker, l'objet standard, le canal. Tout le reste du site les décline.
1. Le message
Un message est une enveloppe à clé unique : le nom du message, et son payload.
{ message_demande: { ref: "3f2c8a9e-…", texte: "bonjour" } }
Le payload est de la donnée JSON pure : string, number, boolean, object, array. Jamais une Date, une instance de classe, une fonction ou un handle. Un message doit se comporter à l'identique qu'il soit livré localement (copie) ou à travers le réseau (texte JSON).
Chaque champ est déclaré au catalogue. Aucun champ implicite : un champ en trop fait rejeter le message, comme un champ manquant.
2. Le catalogue
Le catalogue décrit chaque message par un contrat : des versions, des révisions, des champs typés, et des exemples qui prouvent le tout. C'est de la pure donnée, injectée au broker à la construction. Voici le contrat réel de message_demande dans le chat :
Vocabulaire : type, required, pattern, enum, default, fields pour un sous-objet, items pour une liste, et oneOf au niveau d'une révision pour « exactement un champ parmi N ». Aucune règle conditionnelle par valeur : ce vocabulaire est fermé, et c'est voulu. Le tableau complet.
Une révision ajoute des champs de façon compatible. Une version casse la forme et vient avec un upcaster déclaratif. Un contrat validé ne se modifie jamais en place.
3. Le broker
Un par environnement : un dans le navigateur, un sur le serveur. Il fait trois choses, et il les fait strictement.
register(objet, emits, receives): l'objet déclare ce qu'il émet et ce qu'il reçoit. Une seule déclaration inconnue du catalogue rejette l'objet entier, sans exception : l'application continue sans lui.publish(objet, message): validation immédiate contre le contrat, sur la pile de l'émetteur. Payload invalide : jeté, violations listées en console. Validé : mis en file.- Livraison FIFO à épuisement : chaque abonné reçoit sa copie privée dans
handleMessage. Unpublishfait depuis unhandleMessageest mis en file et livré après le message courant, jamais imbriqué.
Le broker ne connaît aucun composant par son nom. Il connaît des émetteurs et des abonnés par message.
4. L'objet standard
Un composant est une classe à la signature unique, client et serveur : (broker, instanceName, deps). Le squelette tient en une page :
class StandardObject {
instanceName; // différencie une instance d'une autre
broker; // l'aiguilleur auprès duquel l'objet s'inscrit et par lequel il émet
deps; // tout le reste : supports DOM, pools, sockets, réglages
supports; // les nœuds HTML du rendu, normalisés en carte
support; // raccourci vers supports.main
verbose = false;
emits = [{ name: "some_message", version: 1 }];
receives = [{ name: "other_message", version: 1 }]; // un abonné ne déclare JAMAIS de révision
constructor(broker, instanceName, deps = {}) {
this.broker = broker;
this.instanceName = instanceName;
this.deps = deps;
this.supports = deps.support?.nodeType ? { main: deps.support } : (deps.support ?? {});
this.support = this.supports.main;
this.render(); // objets client seulement
this.broker.register(this, this.emits, this.receives);
}
handleMessage(message) {
// selon les clés présentes : rafraîchir l'affichage et/ou publier
// this.broker.publish(this, { some_message: { … } });
}
destroy() { // symétrique du constructeur
this.broker.unregister(this, this.emits, this.receives);
for (const node of Object.values(this.supports)) node.replaceChildren();
}
}
export default StandardObject;
Tout ce qui n'est ni le broker ni le nom d'instance arrive par deps, à la construction : un support DOM, un pool de base de données déjà ouvert, un socket. Jamais de process.env, jamais d'import de configuration. Les cinq règles de deps.
5. Le canal
Client et serveur ont chacun leur broker. Un canal est un objet standard comme les autres, abonné des deux côtés : ce qu'un client émet, la moitié client du canal le reçoit et l'envoie ; la moitié serveur le publie auprès du broker serveur, qui le valide à nouveau. Rien ne parle jamais directement au canal.
Le trajet d'un message_demande, étape par étape
ecran_fil publie
L'utilisateur envoie « bonjour ». Le composant tire un
ref(UUID) et publie{ message_demande: { ref, texte } }auprès de son broker.Le broker client valide
Contre
message_demandev1 r0 :refdoit ressembler à un UUID,textefaire entre 1 et 1000 caractères. Validé : mis en file, puis livré aux abonnés. Ici le seul abonné est le canal.Le canal client envoie la trame
Il reçoit le message dans son
handleMessagecomme n'importe quel abonné, et l'écrit sur le socket en JSON.Le canal serveur publie
Il pose
id(identifiant de session) etsession(l'objet partagé) sur l'enveloppe, consulteallowMessage, puis publie. Le broker serveur valide à nouveau le payload : un client modifié ne peut rien injecter hors contrat.gestion_messages traite et répond
Il vérifie que la session est identifiée, enregistre, puis publie
message_posteciblé sur toutes les sessions connectées, par la clé d'enveloppeid.Retour en message_poste
Le canal serveur traduit l'
iden sockets et envoie la trame, sans les clés d'enveloppe. Le canal client la publie, le broker client la valide,ecran_fill'affiche.
À l'intérieur d'un composant, on programme normalement : POO classique, une bibliothèque ESM, un rendu à la main. fwtozza ne régit que les frontières entre composants. Une liste et sa zone de saisie, une table et son formulaire, vivent dans le même composant si elles partagent le même état et se déploient ensemble.
On découpe par fonction et/ou déploiement, jamais par géométrie d'écran. Un agrégat de une à trois tables liées fait un composant serveur. Une zone fonctionnelle fait un composant client. Une capacité fait un message avec oneOf (gerer_clients.read, .create, .update, .delete) plutôt qu'une nuée de contrats-verbes. Avant de rendre une conception, comptez vos composants et vos messages : trop nombreux, vous avez sur-découpé. La méthode complète.