Class WSClient - Client WebSocket Navigateur
Class WSClient - Client WebSocket Navigateur
📖 Introduction
WSClient est un client WebSocket universel côté navigateur, conçu pour fournir une connexion robuste et durable avec des serveurs WebSocket hétérogènes. Il abstrait les mécanismes bas niveau du WebSocket natif afin de proposer une API plus fiable, orientée production, tout en restant compatible avec les implémentations standards.
La classe fournit notamment :
- une API événementielle inspirée du DOM via
DOMStyleEmitter, - un système de heartbeat client pour les connexions longues durées,
- un support ping/pong standard ou personnalisé,
- une gestion automatique des coupures silencieuses,
- une abstraction claire des événements WebSocket.
WSClient est conçu pour fonctionner en tandem avec UniverselWebSocketServer, tout en restant pleinement interopérable avec d’autres serveurs WebSocket.
Le code complet de la classe est disponible en fin d’article.
Objectifs
- Garantir la stabilité des connexions WebSocket longues durées
- Détecter rapidement les coupures silencieuses
- Supporter différents protocoles ping/pong
- Simplifier l’usage côté navigateur
- Fournir une API événementielle claire et cohérente
Architecture
- Environnement : Navigateur
- Transport : WebSocket natif
- Pattern : Client + Événementiel
- Heartbeat : actif ou optionnel
- Interopérabilité : ping/pong texte ou custom
📄 Proprietes
Socket
socket : WebSocket = null;
Instance WebSocket représentant la connexion au serveur.
HeartbeatInterval
heartbeatInterval : numbers = null ;
Identifiant de l’intervalle utilisé pour le système de heartbeat.
Options
options : WSClient_options = null;
Structure de configuration du client.
type WSClient_options = {
url? : string ,
customPingPong? : {
pingDetector : (pingData : any) => boolen,
pongDetector : (pongData : any) => boolen,
sendPing? : (ws : WebSocket) => string | object ,
sendPong? : (ws : WebSocket) => string | object ,
},
heartbeat ?: boolen,
heartbeatTimeout? : number
}
⚙️ Methodes
Constructor
constructor( options : WSClient_option) {}
Initialise le client, valide les options et déclenche automatiquement la connexion WebSocket.
constructor( options : WSClient_option) {
super();
if (typeof options != "object") throw new Error("Options invalide");
if (typeof options.url !== "string") throw new Error("URL invalide");
if (options.heartbeatTimeout == undefined) options.heartbeatTimeout = 15000
this.options = options
this.connect();
}
Connect
connect() {}
Établit la connexion WebSocket et initialise les systèmes de heartbeat, de ping/pong et de gestion des événements.
connect() {
const ws = new WebSocket(this.options.url);
this.socket = ws
ws.onopen = (event) => {
this.lastPong = Date.now();
this.dispatchEvent("open", { ws, event });
if (this.options.heartbeat == true) {
this.startHeartbeat();
}
}
ws.onmessage = (event) => {
const msg = event.data?.toString?.().trim?.();
// Custome Ping/Pong, pour d'autre systeme
if (typeof this.options.customPingPong == "object" ) {
if (typeof this.options.customPingPong.pingDetector != "function") throw new Error("pingDetector invalide");
if (typeof this.options.customPingPong.pongDetector != "function") throw new Error("pongDetector invalide");
if (this.options.customPingPong.pingDetector(msg)) {
this.dispatchEvent("ping", { ws, event } );
if (typeof this.options.customPingPong.sendPong == "function") { this.options.customPingPong.sendPong(ws) }
this.lastPong = Date.now(); // si j'ai un ping du serveur. c'est que la connection est en vie
return
}
if (this.options.customPingPong.pongDetector(msg)) {
this.dispatchEvent("pong", { ws, event } );
this.lastPong = Date.now();
return
}
}
// Default Ping/Pong
else {
if (msg === "ping") {
ws.send("pong");
this.lastPong = Date.now(); // si j'ai un ping du serveur. c'est que la connection est en vie
this.dispatchEvent("ping", { ws, event } );
return;
}
if (msg === "pong") {
this.lastPong = Date.now();
this.dispatchEvent("pong", { ws, event } );
return;
}
}
this.dispatchEvent("message", { ws, event, data: msg } );
}
ws.onerror = (event) => {
this.dispatchEvent(new CustomEvent("error", { detail: { ws, event } }));
};
ws.onclose = (event) => {
this.stopHeartbeat();
this.dispatchEvent(new CustomEvent("close", { detail: { ws, event } }));
};
}
Reload
reload() {}
Reconnecte le client WebSocket.
reload() { this.connect() }
Send
send(data : any ){}
Envoie une donnée au serveur WebSocket.
send(data) {
if (!this.socket) return console.warn("🚫 Socket non initialisée");
if (this.socket.readyState !== WebSocket.OPEN) return console.warn("🚫 Socket non ouverte");
this.socket.send(typeof data === "string" ? data : JSON.stringify(data));
}
StopHeartbeat
stopHeartbeat() {}
Désactive le système de heartbeat.
stopHeartbeat() {
this.heartbeat = false;
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
StartHeartbeat
startHeartbeat() {}
Active le système de heartbeat et surveille la réactivité du serveur.
startHeartbeat() {
this.heartbeat = true;
this.lastPong = Date.now();
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = setInterval(() => {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return;
// Custome Ping/Pong, pour d'autre systeme
if (typeof this.options.customPingPong == "object" ) {
if (typeof this.options.customPingPong.sendPing != "function") throw new Error("sendPing invalide");
this.options.customPingPong.sendPing(ws)
}
// Default Ping/Pong
else {
this.socket.send("ping");
}
// si le dernier pong est trop vieux, on ferme la connexion
if (Date.now() - this.lastPong > this.options.heartbeatTimeout) {
console.warn("⚠️ Pas de réponse du serveur, fermeture...");
this.socket.close();
}
}, this.options.heartbeatTimeout / 2);
}
🔔 Evénement
| Événement | Description |
|---|---|
open | Connexion établie |
message | Message applicatif |
ping | Ping reçu |
pong | Pong reçu |
error | Erreur WebSocket |
close | Connexion fermée |
🚀 Exemple d’utilisation
const client = new WSClient({
url: 'ws://localhost:8080',
heartbeat: true,
});
client.addEventListener('open', () => console.log('Connecté'));
client.addEventListener('message', (e) => console.log(e.detail.data));
client.addEventListener('close', () => console.log('Connexion fermée'));
🎯 Conclusion
WSClient est un client WebSocket robuste, extensible et orienté production, spécialement conçu pour le navigateur.
Grâce à son système de heartbeat, son support flexible du ping/pong et son API événementielle claire, il permet de gérer efficacement les connexions WebSocket longue durée, même dans des environnements réseau instables.
Associé à UniverselWebSocketServer, il constitue une solution WebSocket complète et interopérable, adaptée aux besoins des applications temps réel modernes.
📜 Code
class WSClient extends DOMStyleEmitter {
socket = null;
heartbeatInterval
options
constructor( options ) {
super();
if (typeof options != "object") throw new Error("Options invalide");
if (typeof options.url !== "string") throw new Error("URL invalide");
if (options.heartbeatTimeout == undefined) options.heartbeatTimeout = 15000
this.options = options
this.connect();
}
connect() {
const ws = new WebSocket(this.options.url);
this.socket = ws
ws.onopen = (event) => {
this.lastPong = Date.now();
this.dispatchEvent("open", { ws, event });
if (this.options.heartbeat == true) {
this.startHeartbeat();
}
}
ws.onmessage = (event) => {
const msg = event.data?.toString?.().trim?.();
// Custome Ping/Pong, pour d'autre systeme
if (typeof this.options.customPingPong == "object" ) {
if (typeof this.options.customPingPong.pingDetector != "function") throw new Error("pingDetector invalide");
if (typeof this.options.customPingPong.pongDetector != "function") throw new Error("pongDetector invalide");
if (this.options.customPingPong.pingDetector(msg)) {
this.dispatchEvent("ping", { ws, event } );
if (typeof this.options.customPingPong.sendPong == "function") { this.options.customPingPong.sendPong(ws) }
this.lastPong = Date.now(); // si j'ai un ping du serveur. c'est que la connection est en vie
return
}
if (this.options.customPingPong.pongDetector(msg)) {
this.dispatchEvent("pong", { ws, event } );
this.lastPong = Date.now();
return
}
}
// Default Ping/Pong
else {
if (msg === "ping") {
ws.send("pong");
this.lastPong = Date.now(); // si j'ai un ping du serveur. c'est que la connection est en vie
this.dispatchEvent("ping", { ws, event } );
return;
}
if (msg === "pong") {
this.lastPong = Date.now();
this.dispatchEvent("pong", { ws, event } );
return;
}
}
this.dispatchEvent("message", { ws, event, data: msg } );
}
ws.onerror = (event) => {
this.dispatchEvent(new CustomEvent("error", { detail: { ws, event } }));
};
ws.onclose = (event) => {
this.stopHeartbeat();
this.dispatchEvent(new CustomEvent("close", { detail: { ws, event } }));
};
}
reload() { this.connect() }
send(data) {
if (!this.socket) return console.warn("🚫 Socket non initialisée");
if (this.socket.readyState !== WebSocket.OPEN) return console.warn("🚫 Socket non ouverte");
this.socket.send(typeof data === "string" ? data : JSON.stringify(data));
}
stopHeartbeat() {
this.heartbeat = false;
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
startHeartbeat() {
this.heartbeat = true;
this.lastPong = Date.now();
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
this.heartbeatInterval = setInterval(() => {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) return;
// Custome Ping/Pong, pour d'autre systeme
if (typeof this.options.customPingPong == "object" ) {
if (typeof this.options.customPingPong.sendPing != "function") throw new Error("sendPing invalide");
this.options.customPingPong.sendPing(ws)
}
// Default Ping/Pong
else {
this.socket.send("ping");
}
// si le dernier pong est trop vieux, on ferme la connexion
if (Date.now() - this.lastPong > this.options.heartbeatTimeout) {
console.warn("⚠️ Pas de réponse du serveur, fermeture...");
this.socket.close();
}
}, this.options.heartbeatTimeout / 2);
}
}