Uno spinner di caricamento non dice nulla. Quando un task di IA si protrae per minuti — o torna in coda per un terzo tentativo — hai bisogno di vedere lo stato. I Server-Sent Events ti offrono questa visibilità senza l'overhead dell'handshake dei WebSocket o la coreografia del long polling. Il server mantiene aperta una singola risposta HTTP e invia aggiornamenti in testo semplice man mano che le cose cambiano. Il client li legge non appena arrivano.

Se la connessione cade, probabilmente non vorrai ricominciare da capo. Un flusso SSE ben costruito ricorda dove eri arrivato. Con Node.js 20 e la sola libreria standard, puoi implementarlo. Non sono richiesti pacchetti esterni.

Come appare il formato di trasmissione

Un messaggio SSE è semplice testo. Il server scrive tre cose: un nome dell'evento opzionale, un campo data obbligatorio e un campo id che diventa il tuo punto di salvataggio. Ogni record termina con due caratteri di nuova riga — una riga vuota che segna il confine.

Un flusso sano potrebbe apparire così sul filo:

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

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

Il client EventSource del browser legge queste righe automaticamente. Genera un evento per ogni blocco e memorizza internamente l'ultimo id. Se la connessione TCP salta, il client attende, si riconnette e invia l'identificatore memorizzato al server come header Last-Event-ID. Quell'header è il motivo per cui questo pattern funziona. Senza di esso, non hai un cursore persistente.

Implementare il server in Node.js

Il modulo http integrato di Node può gestire tutto direttamente. Quando arriva una richiesta, imposta gli header corretti in modo che il client sappia che si tratta di uno stream e non di una pagina:

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

Elimina il buffering. Proxy e framework a volte raggruppano le risposte, il che compromette la sensazione di real-time, quindi esegui il flush dopo ogni chunk.

Invia prima l'ID, poi il tipo di evento, poi i dati del payload e infine la riga vuota di chiusura. L'ordine conta solo nel senso che l'ID deve arrivare prima della riga vuota affinché il client possa catturarlo. Se stai usando il metodo nativo response.write(), l'output è letteralmente:

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

Quel \n\n finale non è decorativo. I parser SSE lo trattano come il terminatore del record. Se lo ometti, il client rimarrà in attesa di ulteriori dati.

Il cursore è tutto

Una nuova connessione HTTP non garantisce uno stato nuovo. Quando un client si riconnette, l'header Last-Event-ID ti dice l'ultimo messaggio che ha ricevuto. Il tuo compito è riprendere dal successivo, non dall'inizio.

Ciò significa mantenere un log o un journal ordinato di eventi lato server. Un array in memoria funziona per una demo. In produzione avrai bisogno di qualcosa di persistente — aggiungi i dati a un log del database, a uno stream Redis o a un write-ahead journal — perché il riavvio di un server non dovrebbe cancellare la cronologia e costringere ogni client a ricominciare da zero.

Indizza i tuoi eventi tramite un intero monotonicamente crescente o un ULID. Quando arriva una riconnessione, interroga gli eventi in cui id > lastEventId e riproducili in ordine. Inserisci un piccolo ritardo artificiale o raggruppa i messaggi se ne hai centinaia in coda, ma inviali dal più vecchio al più recente in modo che il client possa ricostruire lo stato cronologicamente.

Aspettati dei duplicati

Le reti non sono affidabili. Un server potrebbe inviare un evento, perdere l'acknowledgment TCP e inviarlo di nuovo dopo un timeout. Progetta fin dall'inizio per una consegna "at-least-once".

Sul client, la deduplicazione è economica. Mantieni una Map indicizzata per ID dell'evento. Quando arriva un nuovo evento, controlla la mappa. Se l'ID esiste, scarta il duplicato silenziosamente. Poiché il tuo server assegna ID deterministici, questo rende i duplicati innocui. La mappa non deve crescere all'infinito. Una volta confermato che un evento è stato elaborato correttamente, elimina gli ID più vecchi. Una finestra scorrevole di qualche centinaio di voci è solitamente sufficiente per i client browser.

Quando il cursore scade

Alla fine, un client si riconnetterà dopo ore o giorni. Se il tuo buffer di cronologia copre solo gli ultimi mille eventi e il client è indietro di duemila, riprodurre i vuoti è impossibile.

Non trasmettere una cronologia parziale. Questo lascerebbe il client in uno stato inconsistente. Invece, rileva un cursore scaduto e invia uno snapshot completo come evento successivo. Lo snapshot dovrebbe contenere un nuovo cursore che ancora il client allo stato attuale. Da lì, i delta in tempo reale riprendono normalmente. Documenta chiaramente questo confine nel tuo protocollo, in modo che il codice del client sappia quando resettare il proprio modello locale invece di aggiungere dati.

Proteggi lo stream

Gli endpoint SSE aperti sono bersagli attraenti. Chiunque può mantenere una connessione aperta e le richieste replicate possono amplificare il carico di lettura sul tuo storage.

Gate the endpoint with proper authorization. Because the browser EventSource does not support custom headers, pass the token in the query string or use cookies with strict SameSite policies. Validate the token before you allocate stream resources.

Set history limits and per-user quotas. Cap the number of stored events per task, and cap the number of concurrent connections per client. Log disconnects and replays so you can spot a rogue client hammering your cursor endpoint.

The pattern travels

This approach is not trapped inside HTTP. The same rules apply when you move to WebSockets, message queues, or agent-to-agent interfaces. The transport changes—you might use binary frames or topic subscriptions—but the underlying problem stays identical. You need a cursor, a durable log, at-least-once semantics, client deduplication, and a fallback to full snapshots when the cursor goes stale. Solve state convergence once, and you can ship it over TCP, WebSocket, or a broker like RabbitMQ without redesigning the core logic.

Keep it simple

Server-Sent Events work because they ride on ordinary HTTP. Proxies understand them. Load balancers can health-check them. Debugging is as easy as curl. But that simplicity disappears if you ignore the edge cases. Build the cursor. Expect replays. Deduplicate on the client. snapshot when history runs out. Do that, and your long-running AI tasks will report their progress honestly, even through spotty Wi-Fi, server restarts, and the occasional overnight browser sleep.

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

Join the discussion: GyaanSetu AI Community