Il est 14h32 un vendredi après-midi cuando mon téléphone commence à exploser. Je viens de déployer un chatbot e-commerce pour une boutique de mode qui dépasse le million de visiteurs mensuels. Les ventes flash du Black Friday ont commencé, et en exactement 3 minutes, nous sommes passés de 50 connexions WebSocket simultanées à plus de 2 300. C'est à ce moment précis que j'ai compris l'importance critique d'une architecture WebSocket robuste pour les conversations IA en production.

Le Cas concret : Chatbot E-commerce avec Pic de 2 300 Connexions Simultanes

La problématique était claire : pendant les pics de traffic, les connexions timeout, les réponses arrivent avec 15 à 30 secondes de latence, et les utilisateurs abandonnent. J'ai dû重构 entire l'infrastructure WebSocket en intégrant HolySheep AI comme fournisseur, avec leur latence moyenne de 47ms qui a changé la donne.

Avec des tarifs de $0.42/1M tokens pour DeepSeek V3.2 contre $15 pour Claude Sonnet 4.5 sur d'autres providers, l'économie est considérable pour un volume de 50M tokens/mois. Voici comment j'ai résolu le problème.

Architecture WebSocket Optimisée pour les Conversational AI

Une connexion WebSocket pour l'IA conversationnelle doit gérer plusieurs défis simultanés : heartbeat pour détecter les déconnexions, retry automatique avec backoff exponentiel, queue de messages pour éviter les pertes, et gestion intelligente des timeouts.

1. Client WebSocket avec Reconnexion Automatique

import websockets
import asyncio
import json
import logging
from datetime import datetime, timedelta
from typing import Optional, Callable
import aiohttp

class HolySheepWebSocketClient:
    """
    Client WebSocket optimisé pour les conversations IA avec HolySheep AI.
    Gère automatiquement les reconnexions, heartbeats et timeouts.
    """
    
    def __init__(
        self,
        api_key: str,
        base_url: str = "https://api.holysheep.ai/v1",
        max_retries: int = 5,
        base_delay: float = 1.0,
        max_delay: float = 60.0,
        heartbeat_interval: int = 30,
        message_timeout: int = 120
    ):
        self.api_key = api_key
        self.base_url = base_url.rstrip('/')
        self.ws_url = f"{self.base_url}/ws/chat"
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.max_delay = max_delay
        self.heartbeat_interval = heartbeat_interval
        self.message_timeout = message_timeout
        
        self._websocket = None
        self._connected = False
        self._session_id = None
        self._last_pong = datetime.now()
        self._message_queue = asyncio.Queue()
        self._response_handlers: dict[str, Callable] = {}
        
        self.logger = logging.getLogger(__name__)
    
    async def connect(self, conversation_id: str) -> str:
        """Établit une connexion WebSocket avec gestion des retries."""
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "X-Conversation-ID": conversation_id,
            "X-Client-Version": "2.1.0",
            "X-SDK": "holy-sheep-python/1.0"
        }
        
        for attempt in range(self.max_retries):
            try:
                self._websocket = await websockets.connect(
                    self.ws_url,
                    extra_headers=headers,
                    ping_interval=self.heartbeat_interval,
                    ping_timeout=10,
                    close_timeout=5
                )
                self._connected = True
                
                # Recevoir le session_id du serveur
                init_msg = await asyncio.wait_for(
                    self._websocket.recv(),
                    timeout=self.message_timeout
                )
                init_data = json.loads(init_msg)
                self._session_id = init_data.get("session_id")
                
                self.logger.info(
                    f"Connecté avec succès. Session: {self._session_id}"
                )
                return self._session_id
                
            except asyncio.TimeoutError:
                self.logger.warning(
                    f"Tentative {attempt + 1}/{self.max_retries} - Timeout"
                )
            except Exception as e:
                self.logger.warning(
                    f"Tentative {attempt + 1}/{self.max_retries} échouée: {e}"
                )
            
            # Backoff exponentiel avec jitter
            if attempt < self.max_retries - 1:
                delay = min(
                    self.base_delay * (2 ** attempt),
                    self.max_delay
                )
                # Ajout d'un jitter aléatoire (0.5 à 1.5 du delay)
                delay *= (0.5 + asyncio.get_event_loop().time() % 1)
                await asyncio.sleep(delay)
        
        raise ConnectionError(
            f"Impossible de se connecter après {self.max_retries} tentatives"
        )
    
    async def send_message(
        self,
        content: str,
        model: str = "deepseek-v3.2",
        temperature: float = 0.7,
        max_tokens: int = 2048
    ) -> dict:
        """Envoie un message et attend la réponse avec timeout."""
        if not self._connected:
            raise ConnectionError("Non connecté au serveur WebSocket")
        
        message = {
            "type": "chat.completion",
            "id": f"msg_{datetime.now().timestamp()}",
            "model": model,
            "messages": [
                {"role": "user", "content": content}
            ],
            "temperature": temperature,
            "max_tokens": max_tokens,
            "stream": False
        }
        
        await self._websocket.send(json.dumps(message))
        
        try:
            response = await asyncio.wait_for(
                self._websocket.recv(),
                timeout=self.message_timeout
            )
            return json.loads(response)
        except asyncio.TimeoutError:
            self.logger.error("Timeout lors de la réception de la réponse")
            raise
    
    async def listen(self, callback: Callable[[dict], None]):
        """Écoute les messages entrants en continu."""
        if not self._connected:
            raise ConnectionError("Non connecté au serveur WebSocket")
        
        try:
            async for message in self._websocket:
                try:
                    data = json.loads(message)
                    
                    # Gérer les messages de contrôle
                    msg_type = data.get("type", "")
                    
                    if msg_type == "pong":
                        self._last_pong = datetime.now()
                        self.logger.debug("Pong reçu - connexion active")
                    
                    elif msg_type == "error":
                        self.logger.error(f"Erreur serveur: {data.get('message')}")
                    
                    elif msg_type == "ping":
                        await self._websocket.send(json.dumps({"type": "pong"}))
                    
                    else:
                        await callback(data)
                        
                except json.JSONDecodeError:
                    self.logger.warning(f"Message invalide: {message}")
                    
        except websockets.exceptions.ConnectionClosed:
            self.logger.warning("Connexion fermée par le serveur")
            self._connected = False
            await self._handle_reconnection()
    
    async def _handle_reconnection(self):
        """Gère la reconnexion automatique avec preservation du contexte."""
        self.logger.info("Tentative de reconnexion...")
        
        for attempt in range(self.max_retries):
            try:
                # Tenter de récupérer la session
                await self.connect(self._session_id)
                
                # Renvoyer les messages en attente
                while not self._message_queue.empty():
                    msg = await self._message_queue.get()
                    await self._websocket.send(json.dumps(msg))
                
                self.logger.info("Reconnexion réussie")
                return
                
            except Exception as e:
                self.logger.warning(f"Reconnexion échouée: {e}")
                await asyncio.sleep(self.base_delay * (2 ** attempt))
        
        raise ConnectionError("Impossible de se reconnecter")

Utilisation basique

async def main(): client = HolySheepWebSocketClient( api_key="YOUR_HOLYSHEEP_API_KEY", max_retries=5, heartbeat_interval=30, message_timeout=120 ) try: session_id = await client.connect("conv_e-commerce_12345") print(f"Session établie: {session_id}") # Exemple de conversation response = await client.send_message( content="Bonjour, je cherche une robe noire pour un mariage", model="deepseek-v3.2", temperature=0.7 ) print(f"Réponse IA: {response['choices'][0]['message']['content']}") except Exception as e: print(f"Erreur: {e}") finally: await client.close()

Lancer avec: asyncio.run(main())

2. Serveur Proxy avec Gestion des Timeouts et Load Balancing

import WebSocket, { WebSocketServer, RawData } from 'ws';
import http from 'http';
import { v4 as uuidv4 } from 'uuid';

interface HolySheepConfig {
  apiKey: string;
  baseUrl: string;
  maxConnections: number;
  idleTimeout: number;
  messageTimeout: number;
  rateLimitPerMinute: number;
}

interface ConnectionContext {
  id: string;
  ws: WebSocket;
  sessionId?: string;
  createdAt: Date;
  lastActivity: Date;
  messageCount: number;
  conversationHistory: Array<{role: string; content: string}>;
  isAuthenticated: boolean;
  retries: number;
}

class HolySheepProxyServer {
  private wss: WebSocketServer;
  private connections: Map = new Map();
  private config: HolySheepConfig;
  private cleanupInterval: NodeJS.Timeout;
  private rateLimitMap: Map = new Map();
  
  constructor(config: HolySheepConfig) {
    this.config = config;
    this.wss = new WebSocketServer({ 
      port: 8080,
      maxPayload: 10 * 1024 * 1024 // 10MB max
    });
    
    this.setupServer();
    this.startCleanupJob();
  }
  
  private setupServer(): void {
    this.wss.on('connection', (ws: WebSocket, req: http.IncomingMessage) => {
      const clientId = uuidv4();
      const clientIP = req.headers['x-forwarded-for']?.toString() || req.socket.remoteAddress || 'unknown';
      
      // Rate limiting par IP
      if (this.isRateLimited(clientIP)) {
        ws.close(1008, 'Rate limit exceeded');
        return;
      }
      
      // Limite de connexions
      if (this.connections.size >= this.config.maxConnections) {
        ws.close(1013, 'Server at capacity');
        return;
      }
      
      const context: ConnectionContext = {
        id: clientId,
        ws,
        createdAt: new Date(),
        lastActivity: new Date(),
        messageCount: 0,
        conversationHistory: [],
        isAuthenticated: false,
        retries: 0
      };
      
      this.connections.set(clientId, context);
      console.log([${clientId}] Nouvelle connexion depuis ${clientIP});
      
      this.handleMessages(clientId);
      this.setupHeartbeat(clientId);
    });
  }
  
  private isRateLimited(clientIP: string): boolean {
    const now = Date.now();
    const limit = this.rateLimitMap.get(clientIP);
    
    if (!limit || now > limit.resetTime) {
      this.rateLimitMap.set(clientIP, {
        count: 1,
        resetTime: now + 60000
      });
      return false;
    }
    
    if (limit.count >= this.config.rateLimitPerMinute) {
      return true;
    }
    
    limit.count++;
    return false;
  }
  
  private async handleMessages(clientId: string): Promise {
    const context = this.connections.get(clientId);
    if (!context) return;
    
    context.ws.on('message', async (data: RawData) => {
      try {
        const message = JSON.parse(data.toString());
        context.lastActivity = new Date();
        context.messageCount++;
        
        // Valider et authentifier
        if (!context.isAuthenticated) {
          await this.authenticate(context, message);
          return;
        }
        
        // Router selon le type de message
        switch (message.type) {
          case 'chat.completion':
            await this.handleChatCompletion(context, message);
            break;
            
          case 'chat.completion.stream':
            await this.handleStreamingCompletion(context, message);
            break;
            
          case 'ping':
            context.ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
            break;
            
          case 'reset_conversation':
            context.conversationHistory = [];
            context.ws.send(JSON.stringify({ 
              type: 'conversation_reset', 
              success: true 
            }));
            break;
            
          default:
            context.ws.send(JSON.stringify({
              type: 'error',
              code: 'UNKNOWN_MESSAGE_TYPE',
              message: Type de message inconnu: ${message.type}
            }));
        }
        
      } catch (error) {
        console.error([${clientId}] Erreur de parsing:, error);
        context.ws.send(JSON.stringify({
          type: 'error',
          code: 'INVALID_MESSAGE',
          message: 'Format de message invalide'
        }));
      }
    });
    
    context.ws.on('close', (code: number, reason: Buffer) => {
      console.log([${clientId}] Connexion fermée: ${code} - ${reason.toString()});
      this.connections.delete(clientId);
    });
    
    context.ws.on('error', (error: Error) => {
      console.error([${clientId}] Erreur WebSocket:, error.message);
    });
  }
  
  private async authenticate(
    context: ConnectionContext, 
    message: any
  ): Promise {
    // Valider la clé API
    if (!message.api_key || message.api_key !== this.config.apiKey) {
      context.ws.close(1008, 'Invalid API key');
      this.connections.delete(context.id);
      return;
    }
    
    context.isAuthenticated = true;
    context.sessionId = uuidv4();
    
    context.ws.send(JSON.stringify({
      type: 'connected',
      session_id: context.sessionId,
      server_time: Date.now(),
      models: ['deepseek-v3.2', 'gpt-4.1', 'claude-sonnet-4.5', 'gemini-2.5-flash'],
      context_window: 128000
    }));
    
    console.log([${context.id}] Authentifié. Session: ${context.sessionId});
  }
  
  private async handleChatCompletion(
    context: ConnectionContext,
    message: any
  ): Promise {
    const timeout = this.config.messageTimeout;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout * 1000);
    
    try {
      // Construire la requête vers HolySheep
      const requestBody = {
        model: message.model || 'deepseek-v3.2',
        messages: [
          ...context.conversationHistory,
          { role: 'user', content: message.content }
        ],
        temperature: message.temperature ?? 0.7,
        max_tokens: message.max_tokens ?? 2048
      };
      
      const response = await fetch(
        ${this.config.baseUrl}/chat/completions,
        {
          method: 'POST',
          headers: {
            'Authorization': Bearer ${this.config.apiKey},
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(requestBody),
          signal: controller.signal
        }
      );
      
      clearTimeout(timeoutId);
      
      if (!response.ok) {
        throw new Error(HolySheep API error: ${response.status});
      }
      
      const data = await response.json();
      
      // Mettre à jour l'historique
      context.conversationHistory.push({ role: 'user', content: message.content });
      context.conversationHistory.push({
        role: 'assistant',
        content: data.choices[0].message.content
      });
      
      // Limiter l'historique à 20 messages
      if (context.conversationHistory.length > 20) {
        context.conversationHistory = context.conversationHistory.slice(-20);
      }
      
      context.ws.send(JSON.stringify({
        type: 'chat.completion',
        id: data.id,
        model: data.model,
        choices: data.choices,
        usage: data.usage,
        latency_ms: Date.now() - context.lastActivity.getTime()
      }));
      
    } catch (error: any) {
      clearTimeout(timeoutId);
      
      if (error.name === 'AbortError') {
        context.ws.send(JSON.stringify({
          type: 'error',
          code: 'TIMEOUT',
          message: Timeout après ${timeout}s
        }));
      } else {
        context.ws.send(JSON.stringify({
          type: 'error',
          code: 'API_ERROR',
          message: error.message
        }));
      }
    }
  }
  
  private async handleStreamingCompletion(
    context: ConnectionContext,
    message: any
  ): Promise {
    const requestBody = {
      model: message.model || 'deepseek-v3.2',
      messages: [...context.conversationHistory, { role: 'user', content: message.content }],
      temperature: message.temperature ?? 0.7,
      max_tokens: message.max_tokens ?? 2048,
      stream: true
    };
    
    try {
      const response = await fetch(
        ${this.config.baseUrl}/chat/completions,
        {
          method: 'POST',
          headers: {
            'Authorization': Bearer ${this.config.apiKey},
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(requestBody)
        }
      );
      
      if (!response.ok || !response.body) {
        throw new Error(Stream error: ${response.status});
      }
      
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let fullContent = '';
      
      // Envoyer les chunks au client
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        const chunk = decoder.decode(value);
        const lines = chunk.split('\n').filter(line => line.trim());
        
        for (const line of lines) {
          if (line.startsWith('data: ')) {
            const data = line.slice(6);
            if (data === '[DONE]') continue;
            
            try {
              const parsed = JSON.parse(data);
              if (parsed.choices?.[0]?.delta?.content) {
                fullContent += parsed.choices[0].delta.content;
                
                context.ws.send(JSON.stringify({
                  type: 'stream_chunk',
                  delta: parsed.choices[0].delta.content,
                  finish_reason: parsed.choices[0].finish_reason
                }));
              }
            } catch (e) {
              // Ignorer les lignes JSON invalides
            }
          }
        }
      }
      
      // Mise à jour de l'historique
      context.conversationHistory.push({ role: 'user', content: message.content });
      context.conversationHistory.push({ role: 'assistant', content: fullContent });
      
      context.ws.send(JSON.stringify({
        type: 'stream_complete',
        full_content: fullContent
      }));
      
    } catch (error: any) {
      context.ws.send(JSON.stringify({
        type: 'error',
        code: 'STREAM_ERROR',
        message: error.message
      }));
    }
  }
  
  private setupHeartbeat(clientId: string): void {
    const context = this.connections.get(clientId);
    if (!context) return;
    
    const pingInterval = setInterval(() => {
      if (context.ws.readyState === WebSocket.OPEN) {
        // Vérifier l'inactivité
        const idleTime = Date.now() - context.lastActivity.getTime();
        if (idleTime > this.config.idleTimeout * 1000) {
          console.log([${clientId}] Timeout d'inactivité);
          context.ws.close(1008, 'Idle timeout');
          clearInterval(pingInterval);
          this.connections.delete(clientId);
          return;
        }
        
        context.ws.ping();
      }
    }, this.config.heartbeat_interval || 30000);
    
    context.ws.on('pong', () => {
      context.lastActivity = new Date();
    });
  }
  
  private startCleanupJob(): void {
    // Nettoyer les connexions inactives toutes les 60 secondes
    this.cleanupInterval = setInterval(() => {
      const now = Date.now();
      const idleLimit = this.config.idleTimeout * 1000;
      
      for (const [id, context] of this.connections.entries()) {
        const idleTime = now - context.lastActivity.getTime();
        
        if (idleTime > idleLimit) {
          console.log([${id}] Nettoyage - inactif depuis ${Math.round(idleTime/1000)}s);
          context.ws.close(1008, 'Idle cleanup');
          this.connections.delete(id);
        }
      }
      
      // Stats
      console.log([Stats] Connexions actives: ${this.connections.size});
    }, 60000);
  }
  
  public getStats(): object {
    return {
      totalConnections: this.connections.size,
      maxConnections: this.config.maxConnections,
      uptime: process.uptime(),
      memoryUsage: process.memoryUsage()
    };
  }
  
  public shutdown(): void {
    clearInterval(this.cleanupInterval);
    for (const [id, context] of this.connections.entries()) {
      context.ws.close(1001, 'Server shutdown');
    }
    this.wss.close();
  }
}

// Lancer le serveur
const server = new HolySheepProxyServer({
  apiKey: 'YOUR_HOLYSHEEP_API_KEY',
  baseUrl: 'https://api.holysheep.ai/v1',
  maxConnections: 5000,
  idleTimeout: 300,      // 5 minutes d'inactivité max
  messageTimeout: 120,    // 2 minutes pour une réponse
  rateLimitPerMinute: 60
});

console.log('Serveur WebSocket HolySheep démarré sur le port 8080');
console.log('Stats:', server.getStats());

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('Arrêt en cours...');
  server.shutdown();
  process.exit(0);
});

3. Implémentation Frontend avec React et Gestion Avancée

import React, { useEffect, useRef, useState, useCallback } from 'react';

interface Message {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp: number;
  status: 'sending' | 'sent' | 'error';
  error?: string;
}

interface UseAIWebSocketOptions {
  apiKey: string;
  baseUrl?: string;
  conversationId?: string;
  model?: string;
  temperature?: number;
  maxRetries?: number;
  heartbeatInterval?: number;
  reconnectOnClose?: boolean;
  onConnectionChange?: (connected: boolean) => void;
  onError?: (error: Error) => void;
}

interface UseAIWebSocketReturn {
  messages: Message[];
  isConnected: boolean;
  isConnecting: boolean;
  latency: number;
  sendMessage: (content: string) => Promise;
  resetConversation: () => void;
  reconnect: () => Promise;
}

export function useAIWebSocket(options: UseAIWebSocketOptions): UseAIWebSocketReturn {
  const {
    apiKey,
    baseUrl = 'wss://api.holysheep.ai/v1/ws/chat',
    conversationId,
    model = 'deepseek-v3.2',
    temperature = 0.7,
    maxRetries = 5,
    heartbeatInterval = 30000,
    reconnectOnClose = true,
    onConnectionChange,
    onError
  } = options;
  
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);
  const [isConnecting, setIsConnecting] = useState(false);
  const [latency, setLatency] = useState(0);
  
  const wsRef = useRef(null);
  const reconnectAttempts = useRef(0);
  const heartbeatTimer = useRef(null);
  const lastPingTime = useRef(0);
  const pendingResponses = useRef void>>(new Map());
  
  // Calculer le délai avec backoff exponentiel
  const getReconnectDelay = useCallback((attempt: number): number => {
    const baseDelay = 1000;
    const maxDelay = 30000;
    const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
    // Ajouter du jitter (10-20%)
    return delay * (0.9 + Math.random() * 0.2);
  }, []);
  
  // Établir la connexion WebSocket
  const connect = useCallback(async (): Promise => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      return;
    }
    
    setIsConnecting(true);
    
    const convId = conversationId || conv_${Date.now()};
    const wsUrl = ${baseUrl}?conversation_id=${encodeURIComponent(convId)};
    
    try {
      const ws = new WebSocket(wsUrl);
      
      // Headers pour l'authentification (si supporté)
      // Note: WebSocket standard ne supporte pas les headers personnalisés
      // Utilisez un token dans l'URL ou un protocole de握手机制
      
      ws.onopen = () => {
        console.log('[WS] Connexion établie');
        setIsConnected(true);
        setIsConnecting(false);
        reconnectAttempts.current = 0;
        onConnectionChange?.(true);
        
        // Envoyer le message d'authentification
        ws.send(JSON.stringify({
          type: 'auth',
          api_key: apiKey
        }));
        
        // Démarrer le heartbeat
        startHeartbeat(ws);
      };
      
      ws.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data);
          handleMessage(data);
        } catch (e) {
          console.error('[WS] Erreur de parsing:', e);
        }
      };
      
      ws.onerror = (event) => {
        console.error('[WS] Erreur:', event);
        onError?.(new Error('WebSocket error'));
      };
      
      ws.onclose = (event) => {
        console.log([WS] Connexion fermée: ${event.code} - ${event.reason});
        setIsConnected(false);
        setIsConnecting(false);
        onConnectionChange?.(false);
        
        stopHeartbeat();
        
        // Reconnexion automatique si enabled
        if (reconnectOnClose && event.code !== 1000) {
          scheduleReconnect();
        }
      };
      
      wsRef.current = ws;
      
    } catch (error) {
      console.error('[WS] Erreur de connexion:', error);
      setIsConnecting(false);
      scheduleReconnect();
    }
  }, [apiKey, baseUrl, conversationId, reconnectOnClose, onConnectionChange, onError]);
  
  // Gérer les messages entrants
  const handleMessage = useCallback((data: any) => {
    switch (data.type) {
      case 'pong':
        const pongLatency = Date.now() - lastPingTime.current;
        setLatency(pongLatency);
        console.log([WS] Latence mesurée: ${pongLatency}ms);
        break;
        
      case 'connected':
        console.log('[WS] Authentifié:', data);
        break;
        
      case 'chat.completion':
        // Résoudre la promesse en attente
        const resolver = pendingResponses.current.get(data.id);
        if (resolver) {
          resolver(data);
          pendingResponses.current.delete(data.id);
        }
        
        // Ajouter la réponse aux messages
        setMessages(prev => [
          ...prev.map(msg => 
            msg.id === data.request_id 
              ? { ...msg, status: 'sent', content: data.choices[0].message.content }
              : msg
          )
        ]);
        break;
        
      case 'stream_chunk':
        // Mise à jour incrémentale pour le streaming
        setMessages(prev => {
          const lastMsg = prev[prev.length - 1];
          if (lastMsg && lastMsg.status === 'sending') {
            return [
              ...prev.slice(0, -1),
              { ...lastMsg, content: lastMsg.content + data.delta }
            ];
          }
          return prev;
        });
        break;
        
      case 'stream_complete':
        setMessages(prev =>
          prev.map(msg =>
            msg.status === 'sending'
              ? { ...msg, status: 'sent' }
              : msg
          )
        );
        break;
        
      case 'error':
        console.error('[WS] Erreur serveur:', data);
        setMessages(prev =>
          prev.map(msg =>
            msg.status === 'sending'
              ? { ...msg, status: 'error', error: data.message }
              : msg
          )
        );
        onError?.(new Error(data.message));
        break;
        
      default:
        console.log('[WS] Message inconnu:', data);
    }
  }, [onError]);
  
  // Heartbeat pour maintenir la connexion
  const startHeartbeat = useCallback((ws: WebSocket) => {
    stopHeartbeat();
    
    heartbeatTimer.current = setInterval(() => {
      if (ws.readyState === WebSocket.OPEN) {
        lastPingTime.current = Date.now();
        ws.send(JSON.stringify({ type: 'ping' }));
      }
    }, heartbeatInterval);
  }, [heartbeatInterval]);
  
  const stopHeartbeat = useCallback(() => {
    if (heartbeatTimer.current) {
      clearInterval(heartbeatTimer.current);
      heartbeatTimer.current = null;
    }
  }, []);
  
  // Programmer une reconnexion
  const scheduleReconnect = useCallback(() => {
    if (reconnectAttempts.current >= maxRetries) {
      console.error('[WS] Nombre max de tentatives atteint');
      onError?.(new Error('Connexion impossible après plusieurs tentatives'));
      return;
    }
    
    const delay = getReconnectDelay(reconnectAttempts.current);
    console.log([WS] Reconnexion dans ${Math.round(delay/1000)}s...);
    
    reconnectAttempts.current++;
    
    setTimeout(() => {
      connect();
    }, delay);
  }, [maxRetries, getReconnectDelay, connect, onError]);
  
  // Envoyer un message
  const sendMessage = useCallback(async (content: string): Promise => {
    const ws = wsRef.current;
    
    if (!ws || ws.readyState !== WebSocket.OPEN) {
      throw new Error('Non connecté au serveur');
    }
    
    const messageId = msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)};
    
    // Ajouter le message à la liste avec état "sending"
    setMessages(prev => [...prev, {
      id: messageId,
      role: 'user',
      content,
      timestamp: Date.now(),
      status: 'sending'
    }]);
    
    const message = {
      type: 'chat.completion',
      id: messageId,
      model,
      content,
      temperature,
      stream: false
    };
    
    ws.send(JSON.stringify(message));
    
    // Créer une promesse pour attendre la réponse
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        pendingResponses.current.delete(messageId);
        reject(new Error('Timeout de réponse'));
      }, 120000); // 2 minutes
      
      pendingResponses.current.set(messageId, (data) => {
        clearTimeout(timeout);
        resolve();
      });
    });
  }, [model, temperature]);
  
  // Réinitialiser la conversation
  const resetConversation = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ type: 'reset_conversation' }));
    }
    setMessages([]);
  }, []);
  
  // Reconnexion manuelle
  const reconnect = useCallback(async (): Promise => {
    reconnectAttempts.current = 0;
    if (wsRef.current) {
      wsRef.current.close(1000, 'Manual reconnect');
    }
    await connect();
  }, [connect]);
  
  // Connexion initiale et cleanup
  useEffect(() => {
    connect();
    
    return () => {
      stopHeartbeat();
      if (wsRef.current) {
        wsRef.current.close(1000, 'Component unmount');
      }
    };
  }, [connect, stopHeartbeat]);
  
  return {
    messages,
    isConnected,
    isConnecting,
    latency,
    sendMessage,
    resetConversation,
    reconnect
  };
}

// Composant React d'exemple
export function AIChatComponent() {
  const [input, setInput] = useState('');
  const messagesEndRef = useRef(null);
  
  const {
    messages,
    isConnected,
    isConnecting,
    latency,
    sendMessage,
    resetConversation
  } = useAIWebSocket({
    apiKey: 'YOUR_HOLYSHEEP_API_KEY',
    conversationId: 'user_session_123',
    model: 'deepseek-v3.2',
    maxRetries: 5,
    heartbeatInterval: 30000
  });
  
  // Auto-scroll vers le dernier message
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!input.trim() || !isConnected) return;
    
    try {
      await sendMessage(input);
      setInput('');
    } catch (error) {
      console.error('Erreur:', error);
    }
  };
  
  return (
    <div className="chat-container">
      <div className="chat-header">
        <span className={status ${isConnected ? 'connected' : 'disconnected'}}>
          {isConnecting ? 'Connexion...' : isConnected ? Connecté (${latency}ms) : 'Déconnecté'}
        </span>
        <button onClick={resetConversation}>Nouvelle conversation</button>
      </div>
      
      <div className="chat-messages">
        {messages.map(msg => (
          <div key={msg.id} className={message ${msg.role}}>
            <p>{msg.content}</p>
            {msg.status === 'sending' && <span className="typing">En train d'écrire...</span>}
            {msg.status === 'error' && <span className="error">{msg.error}</span>}
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>
      
      <form onSubmit={handleSubmit} className="chat-input">
        <input
          type="text"
          value={input}
          onChange={e =>