Un indicateur de chargement ne vous dit rien. Lorsqu'une tâche d'IA s'étend sur plusieurs minutes — ou retourne dans la file d'attente pour une troisième tentative — vous avez besoin de voir l'état d'avancement. Les Server-Sent Events (SSE) vous offrent cette visibilité sans la surcharge de la poignée de main (handshake) des WebSockets ou la chorégraphie du long polling. Le serveur maintient une seule réponse HTTP ouverte et pousse des mises à jour en texte brut au fur et à mesure des changements. Le client les lit à leur arrivée.

Si la connexion est interrompue, vous ne voudrez probablement pas tout recommencer. Un flux SSE bien conçu se souvient de l'endroit où vous en étiez. Avec Node.js 20 et la bibliothèque standard uniquement, vous pouvez mettre cela en place. Aucun package externe n'est requis.

À quoi ressemble le format de transmission

Un message SSE est du texte simple. Le serveur écrit trois éléments : un nom d'événement optionnel, un champ data obligatoire, et un champ id qui devient votre point de sauvegarde. Chaque enregistrement se termine par deux caractères de saut de ligne — une ligne vide qui marque la limite.

Un flux sain pourrait ressembler à ceci sur le réseau :

id: 14
event: status
data: {"phase":"testing","progress":43}

id: 15
event: status
data: {"phase":"retrying","attempt":2}

Le client EventSource du navigateur lit ces lignes automatiquement. Il lève un événement pour chaque bloc et stocke le dernier id en interne. Si la connexion TCP flanche, le client attend, se reconnecte et renvoie l'identifiant stocké au serveur via l'en-tête Last-Event-ID. Cet en-tête est la raison même pour laquelle ce modèle fonctionne. Sans lui, vous n'avez pas de curseur durable.

Implémentation du serveur dans Node.js

Le module http intégré de Node peut gérer cela directement. Lorsqu'une requête arrive, définissez les en-têtes corrects pour que le client sache qu'il s'agit d'un flux (stream) et non d'une page :

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Supprimez la mise en mémoire tampon (buffering). Les proxys et les frameworks regroupent parfois les réponses par lots, ce qui nuit à l'aspect temps réel ; il faut donc vider le tampon (flush) après chaque fragment (chunk).

Envoyez d'abord l'ID, puis le type d'événement, puis les données de la charge utile (payload), puis la ligne vide de terminaison. L'ordre n'importe que dans le sens où l'ID doit arriver avant la ligne vide pour que le client puisse le capturer. Si vous utilisez le response.write() natif, la sortie est littéralement :

response.write(`id: ${cursor}\n`);
response.write(`event: ${eventName}\n`);
response.write(`data: ${JSON.stringify(payload)}\n\n`);

Ce \n\n final n'est pas décoratif. Les parseurs SSE le traitent comme le terminateur d'enregistrement. Si vous l'oubliez, le client restera en attente de données supplémentaires.

Le curseur est l'élément essentiel

Une nouvelle connexion HTTP ne garantit pas un nouvel état. Lorsqu'un client se reconnecte, l'en-tête Last-Event-ID vous indique le dernier message qu'il a reçu. Votre travail consiste à reprendre à partir du suivant, et non depuis le début.

Cela signifie maintenir un journal ou un log d'événements ordonné côté serveur. Un tableau en mémoire fonctionne pour une démo. En production, vous voudrez quelque chose de durable — ajoutez les événements à un journal de base de données, un flux Redis ou un journal d'écriture (write-ahead journal) — car un redémarrage du serveur ne doit pas effacer l'historique et forcer chaque client à repartir de zéro.

Indexez vos événements par un entier monotone croissant ou un ULID. Lorsqu'une reconnexion survient, interrogez les événements où id > lastEventId et rejouez-les dans l'ordre. Insérez un léger délai artificiel ou regroupez les messages si vous avez des centaines de messages en retard, mais envoyez-les du plus ancien au plus récent afin que le client puisse reconstruire l'état chronologiquement.

Prévoyez les doublons

Les réseaux ne sont pas fiables. Un serveur peut envoyer un événement, perdre l'accusé de réception TCP et le renvoyer après un délai d'attente (timeout). Concevez votre système pour une livraison de type « au moins une fois » (at-least-once delivery) dès le départ.

Côté client, la déduplication est peu coûteuse. Conservez une Map indexée par l'ID de l'événement. Lorsqu'un nouvel événement arrive, vérifiez la map. Si l'ID existe, ignorez silencieusement le doublon. Comme votre serveur attribue des ID déterministes, cela rend les doublons inoffensifs. La map n'a pas besoin de croître indéfiniment. Une fois que vous avez confirmé qu'un événement est traité en toute sécurité, supprimez les anciens ID. Une fenêtre glissante de quelques centaines d'entrées suffit généralement pour les clients navigateurs.

Lorsque le curseur expire

Tôt ou tard, un client se reconnectera après des heures ou des jours. Si votre tampon d'historique ne couvre que les mille derniers événements et que le client a deux mille événements de retard, rejouer les écarts est impossible.

Ne diffusez pas un historique partiel. Cela laisse le client dans un état incohérent. Au lieu de cela, détectez un curseur expiré et envoyez un instantané (snapshot) complet comme prochain événement. L'instantané doit porter un nouveau curseur qui ancre le client à l'état actuel. À partir de là, les deltas en direct reprennent normalement. Documentez clairement cette limite dans votre protocole afin que le code client sache quand réinitialiser son modèle local plutôt que de simplement ajouter des données.

Protégez le flux

Les points de terminaison (endpoints) SSE ouverts sont des cibles attrayantes. N'importe qui peut maintenir une connexion, et les requêtes rejouées peuvent amplifier la charge de lecture sur votre stockage.

Sécurisez l'endpoint avec une autorisation appropriée. Comme l'objet EventSource du navigateur ne prend pas en charge les en-têtes personnalisés, passez le jeton dans la chaîne de requête ou utilisez des cookies avec des politiques SameSite strictes. Validez le jeton avant d'allouer les ressources du flux.

Définissez des limites d'historique et des quotas par utilisateur. Limitez le nombre d'événements stockés par tâche et le nombre de connexions simultanées par client. Journalisez les déconnexions et les rejeux afin de pouvoir repérer un client malveillant qui bombarde votre endpoint de curseur.

Ce modèle est universel

Cette approche ne se limite pas au HTTP. Les mêmes règles s'appliquent lorsque vous passez aux WebSockets, aux files d'attente de messages ou aux interfaces agent-à-agent. Le transport change — vous pourriez utiliser des trames binaires ou des abonnements à des sujets — mais le problème de fond reste identique. Vous avez besoin d'un curseur, d'un journal durable, d'une sémantique « au moins une fois », d'une déduplication côté client et d'un repli vers des instantanés complets lorsque le curseur devient obsolète. Résolvez une fois pour toutes la convergence d'état, et vous pourrez le déployer via TCP, WebSocket ou un broker comme RabbitMQ sans avoir à repenser la logique de base.

Restez simple

Les Server-Sent Events fonctionnent parce qu'ils reposent sur le HTTP ordinaire. Les proxys les comprennent. Les équilibreurs de charge peuvent effectuer des tests de santé dessus. Le débogage est aussi simple qu'un curl. Mais cette simplicité disparaît si vous ignorez les cas limites. Construisez le curseur. Anticipez les rejeux. Dédupliquez côté client. Prenez un instantané lorsque l'historique est épuisé. Faites cela, et vos tâches d'IA de longue durée rapporteront leur progression honnêtement, même avec un Wi-Fi instable, des redémarrages de serveur ou la mise en veille occasionnelle du navigateur pendant la nuit.

Source : Build a Reconnecting SSE Task Stream with Node.js

Rejoignez la discussion : GyaanSetu AI Community