Pub/Sub (publish/subscribe) adalah cara mengirim kabar lewat perantara. Pengirim (publisher) mengirim pesan ke sebuah topik di perantara yang disebut broker, dan penerima (subscriber) berlangganan topik itu di broker. Publisher dan subscriber gak saling kenal: yang mereka sama-sama tahu cuma broker dan nama topiknya. Bayangin siaran radio. Stasiun memancarkan di satu frekuensi, dan siapa pun yang menyetel frekuensi itu ikut mendengar. Stasiunnya gak tahu siapa pendengarnya, dan pendengarnya gak perlu tahu letak studionya. Tapi kalau radiomu mati saat berita dibacakan, berita itu lewat begitu saja.
🧪 Tebakan cepat: gudang Kurir Kancil mem-publish 3 pesan "paket transit". Layanan notifikasi baru subscribe sesudah pesan kedua, karena servernya baru selesai restart. Berapa SMS yang terkirim? Tebak dulu, jawabannya ada di bagian "Pesan hilang kalau belum ada yang subscribe".
Masalahnya: gudang harus kenal semua penerima
Di lesson Observer, tracker dan listener-nya hidup di satu program. Sekarang gudang punya layanan sendiri, terpisah dari layanan notifikasi dan laporan. Versi pertamanya memanggil mereka satu per satu:
// gudang.js versi pertama
function paketMasukGudang(resi, kota) {
notifikasi.kirimSms(resi, kota) // gudang harus kenal notifikasi
laporan.tambah(kota) // ...dan laporan, dan tiap layanan baru
}
Layanan tagihan mau ikut tahu? Kode gudang diubah lagi. Layanan notifikasi sedang mati? Panggilan pertama gagal, dan pemindaian paket di gudang ikut berhenti.
Broker di tengah: publish dan subscribe
const broker = {
listeners: {}, // { "paket.status": [fn, fn] }
subscribe(topic, fn) {
(this.listeners[topic] ??= []).push(fn)
return () => {
this.listeners[topic] = this.listeners[topic].filter((f) => f !== fn)
}
},
publish(topic, message) {
const fns = this.listeners[topic] ?? []
fns.forEach((fn) => fn(message))
return fns.length // berapa subscriber yang menerima
},
}
// gudang.js: cuma kenal broker dan nama topik
function paketMasukGudang(resi, kota) {
return broker.publish("paket.status", { resi, status: "transit", kota })
}
// notifikasi.js: cuma kenal broker dan nama topik
broker.subscribe("paket.status", (m) => console.log("SMS: " + m.resi + " transit di " + m.kota))
console.log(paketMasukGudang("KCL-0003", "Medan"))
// SMS: KCL-0003 transit di Medan
// 1
a ??= bartinya "isiadenganbkalauamasihnullatauundefined", lalu hasilnya dipakai. Jadi topik yang belum punya daftar dapat array baru, danpushmenambahkan function ke situ.a ?? bartinya "pakaia, kecuali kalauanullatauundefined, maka pakaib".this.listeners[topic] ?? []membuat topik tanpa subscriber tetap aman: daftarnya kosong, bukan error.publishmemanggil tiap subscriber topik itu dengan pesannya, lalu mengembalikan jumlah yang menerima.subscribemengembalikan function pelepas. Simpan, lalu panggil saat layanannya berhenti:const stop = broker.subscribe(...), nantistop().- Gudang dan notifikasi gak saling meng-import. Layanan laporan ikut cukup dengan satu
broker.subscribe("paket.status", ...)di kodenya sendiri, danpublishberikutnya mengembalikan 2. Kode gudang gak disentuh.
Banyak library menamai method-nya on dan emit. Bentuknya sama: emit membaca bus.listeners[event] ?? [], lalu memanggil tiap function di dalamnya dengan datanya.
Bedanya dengan Observer di lesson 3
Observer (tracker) |
Pub/Sub | |
|---|---|---|
| yang memegang daftar penerima | subject itu sendiri | broker |
| yang harus dikenal penerima | object tracker |
broker dan nama topik |
| tempat jalannya | satu program | bisa beda program, beda server |
Di dalam satu program, broker cuma object biasa, mirip EventEmitter. Browser bahkan punya bus bawaan: const bus = new EventTarget(), lalu bus.addEventListener("paket.status", fn) dan bus.dispatchEvent(new CustomEvent("paket.status", { detail: pesan })). Bedanya baru terasa saat publisher dan subscriber ada di program berbeda: broker-nya jadi server sendiri, seperti Redis Pub/Sub, RabbitMQ, atau Kafka. Antar-tab browser ada BroadcastChannel.
Pesan hilang kalau belum ada yang subscribe
Broker pub/sub cuma meneruskan pesan ke subscriber yang ada saat itu. Dia gak menyimpan apa pun.
| pesan | subscriber saat itu | publish mengembalikan |
SMS |
|---|---|---|---|
| KCL-0001 Bandung | belum ada | 0 | gak ada |
| KCL-0002 Surabaya | belum ada | 0 | gak ada |
| notifikasi subscribe | |||
| KCL-0003 Medan | notifikasi | 1 | SMS: KCL-0003 transit di Medan |
Cuma 1 SMS, untuk pesan ketiga. Itu jawaban tebakannya. Dua pesan pertama hilang tanpa error. Redis sama: PUBLISH mengembalikan jumlah penerima, (integer) 0 kalau gak ada, dan pesannya hilang. Tab yang dibuka belakangan juga gak menerima pesan BroadcastChannel yang dikirim sebelumnya. Nama topik yang salah ketik, misalnya "paket.stauts", berakhir sama: 0 penerima, tanpa error.
Antrian menyimpan pesan sampai diambil
Kalau pesan gak boleh hilang, pakai antrian (queue): tiap layanan punya antrian sendiri yang dibuat sejak awal, dan pesan menunggu di situ sampai diambil.
const antrian = { notifikasi: [] } // dibuat sejak awal
function publishKeAntrian(message) {
antrian.notifikasi.push(message) // disimpan sampai diambil
}
publishKeAntrian({ resi: "KCL-0001", kota: "Bandung" })
publishKeAntrian({ resi: "KCL-0002", kota: "Surabaya" })
// notifikasi menyala, lalu mengambil yang menunggu, dari yang paling lama
while (antrian.notifikasi.length > 0) {
const m = antrian.notifikasi.shift()
console.log("SMS: " + m.resi + " transit di " + m.kota)
}
Dua pesan yang tadi hilang sekarang terkirim, terlambat tapi lengkap. Broker sungguhan menyimpannya di disk: antrian RabbitMQ, Redis Streams, Amazon SQS (menyimpan pesan 4 hari kalau gak diatur, paling lama 14 hari), atau Kafka (7 hari secara bawaan). Harganya: ada penyimpanan yang harus dirawat, dan pesan bisa datang terlambat.
Kapan gak perlu pub/sub
- Pengirim butuh jawaban. "Kode pos ini valid?" harus kembali dengan ya atau tidak. Pub/sub cuma mengabarkan, gak menunggu jawaban. Panggil langsung.
- Satu program, satu penerima yang tetap. Panggilan langsung lebih mudah ditelusuri daripada mencari siapa yang subscribe ke topik apa.
- Pesannya gak boleh hilang. Pub/sub murni melewatkan pesan saat subscriber mati. Pakai antrian.
⚠️ Jebakan yang sering ketemu
- Topik salah ketik.
"paket.stauts"gak punya subscriber, jadipublishmengembalikan 0 tanpa error. Simpan nama topik di satu konstanta. - Subscribe terlambat. Pesan yang di-publish sebelum subscriber siap hilang.
- Lupa melepas subscriber. Sama seperti listener di Observer: menumpuk, dan pesan diproses dobel.
- Subscriber yang error di broker satu program.
forEachberhenti di situ, subscriber sesudahnya gak menerima, dan error-nya sampai kepublishdi gudang.
TL;DR: Pub/Sub mengirim pesan lewat broker: publisher memanggil publish(topic, message), subscriber mendaftar dengan subscribe(topic, fn), dan keduanya cuma kenal broker dan nama topik, bukan satu sama lain. Itu bedanya dengan Observer, yang subject-nya memegang daftar penerimanya sendiri. Broker menyimpan listeners[topic], membuat daftar dengan ??=, membaca dengan ?? [], dan publish mengembalikan jumlah penerima. Penerima baru gak mengubah kode publisher. Pub/sub murni gak menyimpan pesan: subscribe sesudah pesan kedua berarti cuma 1 SMS dari 3, dan topik salah ketik berarti 0. Kalau pesan gak boleh hilang, pakai antrian. Kalau butuh jawaban, panggil langsung.