Bila Laravel Reverb Sampai, Tapi Event Tak Pernah Trigger
Published:
Updated:
Aku baru-baru ini kena satu masalah yang agak annoying dengan Laravel Reverb.
Bukan sebab WebSocket tak connect.
Bukan sebab Reverb down.
Bukan sebab event tak dihantar.
Malah dalam Network tab, aku boleh nampak data WebSocket memang sampai.
Tapi UI tak react langsung.
Dalam kes ni, ada satu button Next yang bergantung kepada status workflow.
Bila workflow dah berubah, backend akan broadcast satu event. Frontend dengar event tersebut dan enable button Next.
Lebih kurang macam ini:
Backend
↓
Laravel Event
↓
Reverb
↓
WebSocket
↓
Laravel Echo
↓
Update UI
↓
Enable Next
Masalahnya, satu bahagian dalam chain ini nampak macam berfungsi, tapi sebenarnya tidak.
Event memang sampai
Ini yang buat debugging jadi sedikit mengelirukan.
Aku buka DevTools.
WebSocket connection ada.
Private channel pun subscribe.
Kemudian bila backend trigger event, nampak message masuk dalam Network tab.
Jadi pada pandangan pertama:
"Okay, Reverb dah jalan."
Tapi button masih disabled.
Frontend tak buat apa-apa.
Tak ada error yang jelas.
Tak ada exception.
Callback Echo pun macam tak pernah berlaku.
Ini bagi aku satu lesson yang penting:
WebSocket message sampai tidak bermaksud application event berjaya diterima oleh frontend.
Ada beberapa layer yang berbeza.
Event dispatched
↓
Reverb broadcast
↓
WebSocket frame received
↓
Echo match event name
↓
Echo callback
↓
UI state changes
Network tab mungkin hanya membuktikan satu benda:
WebSocket frame received
Ia tak membuktikan callback Echo telah execute.
broadcastAs() sebenarnya memainkan peranan
Event dia lebih kurang:
class WorkflowStatusUpdated implements ShouldBroadcast
{
public function broadcastAs(): string
{
return 'workflow.status.updated';
}
}
Kemudian aku listen daripada frontend.
Yang aku terlepas pandang ialah bila kita menggunakan custom broadcast name melalui broadcastAs(), cara Echo resolve nama event juga berubah.
Jadi listener perlu menggunakan . di depan:
Echo.private(`workflow.${workflowId}`)
.listen('.workflow.status.updated', (event) => {
// update UI
})
Bukan:
.listen('workflow.status.updated', ...)
Dan bukan berdasarkan nama class:
.listen('WorkflowStatusUpdated', ...)
Ini benda kecil.
Satu character sahaja.
.
Tapi kesannya:
WebSocket ✅
Reverb ✅
Message ✅
Echo listener ❌
UI update ❌
Dan sebab takde error yang obvious, kita boleh buang masa mencari masalah di tempat yang salah.
Lagi menarik bila ada polling
Sebelum aku betul-betul debug root cause, aku sebenarnya dah ada fallback.
Aku buat polling setiap 3 saat.
Logic dia lebih kurang:
┌── Reverb event ──→ update UI
Backend ─────┤
└── Poll 3 sec ───→ update UI
Dari sudut production, idea ini nampak reasonable.
Kalau WebSocket bermasalah, polling boleh recover state.
Tapi masa development, dia hasilkan masalah lain.
Aku test workflow.
Tiba-tiba button Next enable.
Soalan pertama aku:
"Reverb dah trigger ke?"
Atau sebenarnya:
"Polling yang detect perubahan?"
Aku tak tahu.
Aku terpaksa letak console.log() di kedua-dua tempat.
.listen('.workflow.status.updated', () => {
console.log('REVERB EVENT FIRED')
// ...
})
Dan:
setInterval(() => {
console.log('POLLING')
// ...
}, 3000)
Barulah boleh nampak siapa sebenarnya yang mengubah state.
Pada masa itu aku sedar sesuatu.
Fallback yang sepatutnya membantu recovery sebenarnya sedang menyembunyikan bug yang kita cuba debug.
Jadi aku buang polling
Untuk sementara waktu, kita remove polling.
Aku nak satu source sahaja yang bertanggungjawab untuk perubahan tu.
Backend
↓
Reverb
↓
Echo
↓
UI
Kalau event berjaya, button enable.
Kalau event gagal, button kekal disabled.
Dan itu sebenarnya lebih bagus untuk debugging.
Sekarang aku boleh test dan terus nampak:
"Event memang tak trigger."
Kemudian aku pergi satu layer ke bawah.
WebSocket frame sampai.
Jadi bukan masalah connection.
Bukan masalah Reverb broadcast.
Kemudian aku tengok listener.
Dan akhirnya jumpa isu broadcastAs() tadi.
Benda yang nampak macam masalah Reverb sebenarnya berada pada event matching di Echo.
Realtime bukan semestinya source of truth
Lepas kena masalah ini, aku rasa ada satu architecture principle yang lebih penting.
Kalau button Next bergantung kepada business state, aku tak nak WebSocket jadi satu-satunya source of truth.
Reverb patut memberitahu:
"Something changed."
Kemudian frontend boleh fetch state sebenar.
Contohnya:
Reverb event
↓
Something changed
↓
GET latest workflow status
↓
Update UI
Jadi event tidak semestinya membawa seluruh logic untuk menentukan UI state.
Server tetap menjadi authority.
Ini juga membantu kalau event terlepas.
Contohnya user buka page selepas event sudah berlaku.
Kalau UI hanya bergantung kepada event:
Event berlaku
↓
User buka page
↓
Event sudah lepas
↓
UI tak tahu
Tapi kalau initial page load tetap fetch current state:
Page load
↓
GET current state
↓
Subscribe Reverb
↓
Future changes arrive instantly
Lebih predictable.
Production pula ada masalah lain
Dalam project lain, aku kena pula dengan isu Reverb masa deploy ke production.
Local semuanya okay.
Production tak.
Kali ini masalahnya lebih kepada configuration.
Yang membuatkan benda ini confusing ialah kita sebenarnya bercakap tentang dua dunia yang berbeza.
Ada server-side Reverb:
Reverb server
localhost:8080
Dan ada browser:
Browser
↓
wss://reverb.example.com
Browser tak semestinya patut connect menggunakan host yang sama dengan host yang digunakan oleh Reverb process secara internal.
Aku akhirnya asingkan konsep tersebut kepada:
REVERB_SERVER_HOST
REVERB_SERVER_PORT
untuk Reverb process itu sendiri,
dan public host untuk frontend/Echo:
REVERB_PUBLIC_HOST
atau nilai yang digunakan oleh Vite/Echo dalam setup project tersebut.
Mentaliti yang lebih mudah bagi aku ialah:
INTERNET
│
▼
reverb.example.com
│
Reverse Proxy
│
▼
Reverb Server
0.0.0.0:8080
Jangan terus anggap:
public host == internal server host
Sebab dua benda itu mewakili dua connection boundary yang berbeza.
Dan jangan panik bila nampak "Pusher"
Satu lagi benda yang boleh buat kita confused masa debug.
Reverb menggunakan Pusher protocol.
Jadi bila DevTools atau configuration nampak benda macam:
pusher
app_id
key
channel
jangan terus fikir:
"Kenapa dia pergi Pusher? Aku setup Reverb."
Reverb memang menggunakan Pusher-compatible protocol.
Architecture dia lebih kurang:
Laravel Echo
↓
Pusher JS protocol
↓
Laravel Reverb
Jadi kewujudan terminology Pusher tidak semestinya bermaksud request kita sedang menggunakan Pusher sebagai service.
Ini penting bila troubleshooting sebab kalau tidak, kita boleh mula debug benda yang sebenarnya bukan masalah.
Apa yang aku belajar daripada benda ini
Aku rasa masalah Reverb yang paling annoying bukan semestinya complexity WebSocket itu sendiri.
Kadang-kadang ia adalah layering.
Bila semua layer nampak hampir sama, kita mudah assume:
Message arrived
=
Event handled
Padahal tidak.
Aku sekarang lebih suka debug realtime system dengan checkpoint yang jelas:
1. Event dispatched?
2. Reverb broadcast?
3. Channel subscribed?
4. WebSocket frame received?
5. Event name matched?
6. Echo callback executed?
7. UI state changed?
Setiap soalan mengecilkan search space.
Dan satu lagi:
Jangan letak fallback terlalu awal ketika sedang debug.
Polling, retry, optimistic UI, cached state dan fallback mechanism memang berguna untuk production.
Tapi masa mencari bug, benda-benda ini boleh membuat system nampak "okay" walaupun sebenarnya broken.
Kadang-kadang cara paling cepat untuk cari bug ialah dengan sengaja buang semua benda yang cuba menyelamatkan system.
Biarkan dia fail.
Bila dia fail dengan jelas, barulah senang nampak di mana sebenarnya ia putus.
Untuk Reverb pula, aku rasa mentaliti paling berguna ialah:
Transport ≠ Event routing ≠ UI state
WebSocket boleh healty.
Reverb boleh healthy.
Message boleh sampai.
Tapi Echo masih boleh tak trigger.
Dan kalau UI bergantung kepada event tersebut untuk membenarkan user teruskan workflow, satu character seperti . pun boleh menjadi beza antara:
workflow berjalan
dan
user stuck di button Next.
