Presence & Online Status — Realtime

Presence & Online Status Menampilkan siapa yang online — green dot di Slack, "last seen" di WhatsApp. Terlihat sederhana tapi surprisingly complex di skala…

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() })
  })
})

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.

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"

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari