Presence adalah fitur yang menampilkan siapa yang sedang tersambung (titik hijau "online") dan kapan seseorang terakhir terlihat. Bayangin buku absen di pos satpam: orang masuk dicatat, keluar dicoret. Masalahnya, satu orang bisa masuk lewat dua pintu sekaligus, dan ada yang pergi tanpa pamit. Satpam baru tahu orang itu sudah pergi kalau ia dipanggil dan gak menjawab. Di Laundry Bu Rini, layar staf menampilkan apakah Bu Rini dan Mas Eko sedang online, dan sejak kapan mereka offline.
🧪 Tebakan cepat: Baterai tablet Mas Eko habis dan tablet mati mendadak. Server Socket.IO dengan pengaturan bawaan, berapa lama sampai tahu tablet itu sudah gak tersambung? Jawabannya ada di bagian "Heartbeat: tahu siapa yang pergi tanpa pamit".
Online artinya punya koneksi
Server menghitung koneksi per orang di namespace /staff (socket.data.user diisi middleware dari token):
const koneksi = new Map() // user -> Set berisi socket.id
staf.on('connection', (socket) => {
const user = socket.data.user // 'rini' atau 'eko'
if (!koneksi.has(user)) koneksi.set(user, new Set())
koneksi.get(user).add(socket.id)
if (koneksi.get(user).size === 1) staf.emit('presence:online', { user }) // koneksi pertama saja
socket.on('disconnect', () => {
koneksi.get(user).delete(socket.id)
if (koneksi.get(user).size > 0) return // masih ada tab lain
koneksi.delete(user)
staf.emit('presence:offline', { user, lastSeen: new Date().toISOString() })
})
})
- Offline dideteksi dengan mendengarkan event
disconnectpada socket, lalu memperbarui status dan mengabari yang lain. - Kabar
presence:onlinecuma dikirim saat koneksi pertama, danpresence:offlinecuma saat koneksi terakhir putus. - Cara lain menghitung:
socket.join('user:rini'), lalustaf.adapter.rooms.get('user:rini')?.size. Room itu dibuang saat kosong.
Satu orang, banyak koneksi
| Kejadian | Koneksi Bu Rini | Kabar ke staf lain |
|---|---|---|
| buka papan di tab 1 | 1 | presence:online |
| buka chat di tab 2 | 2 | (gak ada) |
| buka papan di HP | 3 | (gak ada) |
| tutup tab 2 | 2 | (gak ada) |
| tutup tab 1, HP dikunci | 0 | presence:offline |
Kalau server langsung mengirim offline di setiap disconnect, menutup satu tab saja sudah membuat Bu Rini terlihat offline, padahal ia masih di depan papan.
Heartbeat: tahu siapa yang pergi tanpa pamit
Saat tab ditutup, browser menutup koneksinya, dan server langsung dapat disconnect dengan alasan transport close (dicoba). Tapi tablet yang mati mendadak, masuk lift, atau kehilangan Wi-Fi gak sempat menutup apa pun. Server baru tahu lewat heartbeat: server Socket.IO mengirim ping setiap pingInterval (bawaan 25.000 ms), lalu menunggu pong selama pingTimeout (bawaan 20.000 ms).
Dicoba dengan socket.io 4.8.4 dan pengaturan bawaan: client yang diam 1 detik setelah pong terakhir diputus dengan alasan ping timeout 44 detik kemudian, tepat 45 detik setelah pong terakhir. Jadi tergantung kapan tablet mati, server tahu 20 sampai 45 detik kemudian. Itu jawaban tebakannya. Client juga memutus sendiri kalau 45 detik gak ada ping dari server.
- Mau lebih cepat?
new Server({ pingInterval: 10000, pingTimeout: 5000 })mendeteksi dalam 5 sampai 15 detik, tapi setiap perangkat (pengaturan ini berlaku untuk semua namespace) bangun lebih sering untuk menjawab ping, dan baterai HP lebih boros. - Jangan tambah
setIntervalheartbeat buatanmu sendiri untuk online dan offline. Socket.IO sudah punya, dan browser memperlambat timer di tab yang tersembunyi, jadi heartbeat buatan sendiri bisa telat. - Dengan
wsbiasa, kamu yang membuatnya:ws.ping()tiap 25 detik, tandai hidup saat eventpongdatang, danws.terminate()yang gak menjawab sampai putaran berikutnya. Browser menjawab ping otomatis.
Sinyal naik turun: masa tenggang
Tablet Mas Eko pindah dari Wi-Fi ke data seluler. Koneksi lamanya ditutup, server menerima disconnect, lalu client Socket.IO menyambung lagi sekitar 1 detik kemudian. Tanpa jeda, staf lain melihat Mas Eko offline lalu online lagi sedetik kemudian. Di sinyal lemah, titiknya berkedip terus. Obatnya masa tenggang (grace period): saat koneksi terakhir putus, tunggu 10 detik dulu sebelum mengabari offline.
const tunda = new Map() // user -> timer kabar offline
function koneksiPertama(user) {
if (tunda.has(user)) {
clearTimeout(tunda.get(user)) // kembali dalam masa tenggang: diam saja
tunda.delete(user)
} else {
staf.emit('presence:online', { user })
}
}
function koneksiTerakhirPutus(user) {
const lastSeen = new Date().toISOString()
tunda.set(user, setTimeout(() => {
tunda.delete(user)
staf.emit('presence:offline', { user, lastSeen })
}, 10000))
}
Kalau server belum tahu koneksi lama putus (ping timeout belum lewat) saat koneksi baru datang, hitungannya naik ke 2 lalu turun ke 1, dan status gak berubah sama sekali. Penghitung koneksi sudah menangani urutan itu.
Kenapa 10 detik? Client Socket.IO mencoba lagi setelah 1 detik, lalu 2, lalu 4 (paling lama 5), masing-masing diacak ±50%. Tiga percobaan pertama selesai paling lambat dalam 1,5 + 3 + 5 = 9,5 detik, jadi 10 detik menampung tiga kesempatan bahkan di keacakan terburuk. Lebih lama dari itu, orang yang benar-benar pergi terlihat online terlalu lama: tablet yang mati bisa baru offline 45 + 10 = 55 detik kemudian.
"Terakhir dilihat"
- Simpan
lastSeendi database saat koneksi terakhir putus, bukan saat timer masa tenggang selesai. - Untuk
ping timeout, kontak terakhir yang sebenarnya bisa 45 detik lebih awal. Itu batas ketelitian "terakhir dilihat". - Tampilkan seperlunya: "terakhir dilihat 09.42" cukup untuk staf. Beri pilihan menyembunyikannya kalau fitur ini untuk pelanggan.
⚠️ Jebakan yang sering ketemu
- Offline di
disconnectpertama. Orang dengan dua tab terlihat offline padahal masih aktif. Hitung koneksinya. - Mengira
disconnectlangsung datang saat HP mati. Baru 20 sampai 45 detik kemudian, lewat ping timeout. - Tanpa masa tenggang. Status berkedip setiap sinyal naik turun.
- Menyimpan presence di memori satu server. Server restart,
Mapkosong, dan dengan dua server, masing-masing cuma tahu separuh. Pelajaran Pub/Sub dengan Redis membahasnya.
TL;DR: Online artinya punya minimal satu koneksi: hitung koneksi per orang (Map dari user ke Set socket.id, atau room user:rini), kirim presence:online di koneksi pertama dan presence:offline saat koneksi terakhir putus, dengan mendengarkan event disconnect. Tab yang ditutup langsung terdeteksi (transport close), tapi perangkat yang mati baru ketahuan lewat heartbeat Socket.IO: ping tiap 25 detik, tunggu pong 20 detik, jadi 20 sampai 45 detik. Pakai masa tenggang 10 detik (cukup untuk tiga percobaan reconnect pertama, paling lambat 9,5 detik) supaya status gak berkedip, dan simpan "terakhir dilihat" saat koneksi terakhir putus.