Indikator pemuatan (loading spinner) tidak memberi tahu Anda apa pun. Ketika sebuah tugas AI memakan waktu beritit-menit—atau kembali ke antrean untuk percobaan ketiga—Anda perlu melihat statusnya. Server-Sent Events memberikan visibilitas tersebut tanpa beban handshake WebSockets atau orkestrasi long polling. Server menjaga satu respons HTTP tetap terbuka dan mendorong pembaruan teks biasa (plain-text) saat terjadi perubahan. Klien membacanya saat data tiba.

Jika koneksi terputus, Anda mungkin tidak ingin mengulang dari awal. Stream SSE yang dibangun dengan baik akan mengingat posisi terakhir Anda. Hanya dengan Node.js 20 dan pustaka standarnya saja, Anda dapat menyusun ini. Tidak diperlukan paket eksternal.

Seperti apa format datanya

Pesan SSE adalah teks sederhana. Server menulis tiga hal: nama event opsional, bidang data yang wajib ada, dan bidang id yang menjadi titik simpan (save point) Anda. Setiap catatan diakhiri dengan dua karakter baris baru—baris kosong yang menandai batasnya.

Stream yang sehat mungkin terlihat seperti ini pada wire:

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

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

Klien EventSource pada browser membaca baris-baris ini secara otomatis. Ia akan memicu event untuk setiap blok dan menyimpan id terbaru secara internal. Jika koneksi TCP tidak stabil, klien akan menunggu, menyambung kembali, dan mengirimkan pengenal yang tersimpan kembali ke server sebagai header Last-Event-ID. Header itulah alasan utama mengapa pola ini berhasil. Tanpanya, Anda tidak memiliki cursor yang persisten.

Menghubungkan server di Node.js

Modul http bawaan Node.js dapat menangani ini secara langsung. Ketika permintaan masuk, atur header yang benar agar klien tahu bahwa ini adalah sebuah stream, bukan sebuah halaman:

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

Hilangkan proses buffering. Proxy dan framework terkadang menggabungkan respons dalam batch, yang dapat merusak sensasi real-time, jadi lakukan flush setelah setiap chunk.

Kirim ID terlebih dahulu, lalu tipe event, lalu data payload, kemudian baris kosong sebagai penutup. Urutan hanya penting dalam hal ID harus tiba sebelum baris kosong agar klien dapat menangkapnya. Jika Anda menggunakan response.write() asli, outputnya secara harfiah adalah:

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

Akhiran \n\n tersebut bukan sekadar hiasan. Parser SSE menganggapnya sebagai terminator catatan. Jika terlewat, klien akan menggantung menunggu lebih banyak data.

Cursor adalah segalanya

Koneksi HTTP yang baru tidak menjamin status yang baru. Saat klien menyambung kembali, header Last-Event-ID memberi tahu Anda pesan terakhir yang mereka terima. Tugas Anda adalah melanjutkan dari pesan berikutnya, bukan dari awal.

Ini berarti Anda harus memelihara log atau jurnal event yang terurut di sisi server. Array dalam memori (in-memory array) dapat digunakan untuk demo. Dalam produksi, Anda memerlukan sesuatu yang persisten—tambahkan ke log database, Redis stream, atau write-ahead journal—karena restart server tidak boleh menghapus riwayat dan memaksa setiap klien untuk mulai dari nol.

Indeks event Anda menggunakan integer yang meningkat secara monoton atau ULID. Saat koneksi ulang masuk, cari event di mana id > lastEventId, dan mainkan kembali secara berurutan. Masukkan sedikit penundaan buatan atau batch jika Anda memiliki ratusan pesan yang tertunda, tetapi kirimkan dari yang terlama agar klien dapat membangun kembali status secara kronologis.

Antisipasi duplikasi

Jaringan tidak selalu dapat diandalkan. Server mungkin mengirimkan sebuah event, kehilangan konfirmasi TCP, dan mengirimkannya lagi setelah timeout. Rancanglah sistem untuk pengiriman setidaknya satu kali (at-least-once delivery) sejak awal.

Di sisi klien, deduplikasi sangatlah ringan. Gunakan Map dengan kunci ID event. Saat event baru tiba, periksa map tersebut. Jika ID sudah ada, buang duplikatnya secara diam-diam. Karena server Anda menetapkan ID yang deterministik, hal ini membuat duplikasi tidak berbahaya. Map tersebut tidak perlu tumbuh selamanya. Setelah Anda mengonfirmasi bahwa sebuah event telah diproses dengan aman, hapus ID yang lebih lama. Jendela geser (sliding window) berisi beberapa ratus entri biasanya sudah cukup untuk klien browser.

Saat cursor kedaluwarsa

Pada akhirnya, klien akan menyambung kembali setelah berjam-jam atau berhari-hari. Jika buffer riwayat Anda hanya mencakup seribu event terakhir dan klien tertinggal dua ribu event, memutar kembali celah yang ada menjadi tidak mungkin.

Jangan melakukan streaming pada riwayat yang parsial. Hal itu akan membuat klien berada dalam status yang tidak konsisten. Sebaliknya, deteksi cursor yang kedaluwarsa dan kirimkan snapshot lengkap sebagai event berikutnya. Snapshot tersebut harus membawa cursor baru yang menambatkan klien ke status saat ini. Dari sana, perubahan langsung (live deltas) dapat dilanjutkan seperti biasa. Dokumentasikan batasan ini dengan jelas dalam protokol Anda sehingga kode klien tahu kapan harus mengatur ulang model lokalnya alih-alih sekadar menambahkannya.

Lindungi stream Anda

Endpoint SSE yang terbuka adalah target yang menarik. Siapa pun dapat menahan koneksi, dan permintaan replay dapat memperbesar beban baca pada penyimpanan Anda.

Amankan endpoint dengan otorisasi yang tepat. Karena EventSource pada browser tidak mendukung header kustom, kirimkan token dalam query string atau gunakan cookie dengan kebijakan SameSite yang ketat. Validasi token sebelum Anda mengalokasikan sumber daya stream.

Tetapkan batas riwayat dan kuota per pengguna. Batasi jumlah event yang disimpan per tugas, dan batasi jumlah koneksi konkuren per klien. Catat diskoneksi dan replay agar Anda dapat mendeteksi klien nakal yang membombardir endpoint cursor Anda.

Pola ini dapat diterapkan di mana saja

Pendekatan ini tidak terbatas pada HTTP. Aturan yang sama berlaku saat Anda beralih ke WebSocket, message queues, atau antarmuka agent-to-agent. Transport-nya berubah—Anda mungkin menggunakan binary frames atau topic subscriptions—tetapi masalah dasarnya tetap sama. Anda membutuhkan cursor, durable log, semantik at-least-once, deduplikasi klien, dan fallback ke full snapshots saat cursor sudah kedaluwarsa. Selesaikan state convergence satu kali, dan Anda dapat mengirimkannya melalui TCP, WebSocket, atau broker seperti RabbitMQ tanpa merancang ulang logika intinya.

Buatlah tetap sederhana

Server-Sent Events bekerja karena mereka berjalan di atas HTTP biasa. Proxy memahaminya. Load balancer dapat melakukan health-check. Debugging semudah curl. Namun kesederhanaan itu akan hilang jika Anda mengabaikan edge cases. Bangun cursor-nya. Antisipasi replay. Lakukan deduplikasi di sisi klien. Buat snapshot saat riwayat habis. Lakukan itu, dan tugas AI Anda yang berjalan lama akan melaporkan progresnya secara jujur, bahkan melalui Wi-Fi yang tidak stabil, restart server, dan sesekali mode tidur browser semalaman.

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

Bergabunglah dalam diskusi: GyaanSetu AI Community