Principes d'architecture client
Cette page décrit comment un client doit être structuré, pas le format des messages échangés (voir Structure des données et les pages précédentes pour ça).
Principe : un datastore unique détient la vérité
Le client maintient une seule structure de données représentant l’état courant (modules → propriétés → éléments). Toute l’UI lit cette structure ; rien d’autre ne la modifie que les messages reçus du serveur.
Lecture : Datastore ────────────► UI
Écriture : UI ─── SV/SA ───► Serveur ─── update ───► Datastore- Pas de duplication : si un composant a besoin d’une valeur, il la lit dans le datastore, il ne la stocke pas de son côté.
- Pas d’écriture directe : l’UI ne modifie jamais le datastore elle-même, même pour un affichage optimiste.
Une action utilisateur (changer une valeur, cliquer un bouton) déclenche
l’envoi d’un message au serveur (SV/SA — voir
Client → Serveur). Le datastore
local ne change qu’en retour, quand le serveur répond avec l’update
confirmé. Le client ne présume jamais du résultat.
Dump complet vs mises à jour partielles
À la connexion, le client envoie DU ; le serveur répond avec un dump
complet (d) contenant tous les modules avec leurs métadonnées
complètes (label, type, min/max, LOV, etc.).
Les messages qui arrivent ensuite (ee, ea) ne contiennent que la
valeur, pas les métadonnées. Le client doit :
- Conserver les métadonnées du dernier dump complet.
- Sur une mise à jour partielle, ne patcher que la valeur, sans jamais supposer que les métadonnées ont changé.
Si les métadonnées elles-mêmes changent (min/max/format d’un élément),
le serveur le signale explicitement via un message distinct, ev
(voir Serveur → Client) — ce
n’est jamais silencieux. Le client n’a donc qu’à réagir au type de
message reçu, sans avoir à deviner ce qui a changé.
Quand une propriété référence une LOV globale et que celle-ci change, le
serveur envoie la mise à jour sans le champ value. Le client ne doit
jamais préserver l’ancienne valeur dans ce cas précis — il faut
appliquer ce que le serveur envoie tel quel, y compris l’absence de
valeur. Le serveur fait foi ; ne jamais tenter de “combler” un champ
manquant avec l’ancien état local.
Heartbeat
Le heartbeat n’est pas imposé par le protocole. C’est un mécanisme que le client met en place de son côté, seulement si sa technologie l’exige, pour garder la connexion WebSocket active (certains navigateurs, proxys ou plateformes ferment une connexion jugée inactive). Un client qui n’en a pas besoin peut s’en passer entièrement.
Le mécanisme existe côté protocole pour qui en a besoin : envoyer XX
toutes les 30 secondes, le serveur répond xx (sans payload utile).
→ {"XX": {}}
← {"xx": {}}Reconnexion
À la moindre coupure, le client doit vider entièrement son datastore — aucun état partiel ne peut être considéré fiable après une déconnexion.
Ce que le client fait ensuite (retenter automatiquement N fois, proposer à l’utilisateur de relancer la connexion, abandonner…) ne regarde que lui : ce n’est pas spécifié par le protocole, chaque client est libre de sa propre stratégie.
Mais une fois (re)connecté, renvoyer DU pour obtenir un nouveau dump
complet n’est quasiment pas optionnel : reconstruire un état exploitable
à partir des seules mises à jour partielles qui arriveraient ensuite
(ee/ea/ev) est impossible — elles ne donnent qu’une vue
fragmentaire, jamais un état complet. Côté serveur, rien n’est à
préserver entre deux connexions d’un même client : c’est une
contrainte purement côté client.
Exemple minimal : premier échange
Sans framework, juste le tout premier échange (connexion → DU →
écoute) — extrait de test-ws-advanced.js, le script utilisé pour le
health-check horaire du serveur :
const ws = new WebSocket("ws://127.0.0.1:9624");
ws.on("open", () => {
ws.send(JSON.stringify({ "DU": { "language": "en" } }));
});
ws.on("message", (data) => {
console.log("received:", data.toString());
});Exemple : lecture et écriture dans OstErix
Extraits réels illustrant la séparation lecture/écriture de tête de cette page, une fois qu’un vrai client (ici OstErix, en Angular) est construit autour de ce principe.
Lecture — datastore.service.ts, un simple accès à l’état interne :
getProperty(moduleName: string, propertyName: string): Observable<any> {
return new Observable(subscriber => {
const subscription = this.changed$.subscribe(() => {
const module = this.modules[moduleName];
const prop = (module as any)?.properties?.[propertyName] || null;
subscriber.next(prop);
});
subscriber.next(this.modules[moduleName]?.properties?.[propertyName] || null);
return () => subscription.unsubscribe();
});
}Écriture — websocket.service.ts, aucune mutation locale, juste
l’envoi du message :
setPropertyOneElement(module: string, property: string, elements: { [key: string]: any }): void {
this.send({
SV: { m: { [module]: { p: { [property]: { e: elements } } } } }
});
}