loading...

แสดงสถานะ user online ด้วย Firebase (2)

ilumin profile image Teerasak Vichadee ・1 min read

ต่อจากบทความที่แล้ว

ผมทิ้งท้ายไว้ว่า tutorial ของ Firebase มีจุดอ่อนที่

  1. กรณีที่ reload page จะทำให้สถานะหยุด online แป๊ปนึง เพราะ Firebase จะโค้ดจะสร้าง session ใหม่
  2. กรณีที่ user เปิดหลาย tab และปิดไปสัก tab นึง จะทำให้สถานะกลายเป็น offline ทั้งๆที่เปิดบาง tab ค้างไว้
  3. กรณีที่ login หลาย device หรือหลาย browser ก็จะได้คล้ายๆกับข้อ 2.

สำหรับผม ผมสนใจในกรณีที่ 2, 3 ที่สุด เนื่องจากไม่ควรจะเกิดขึ้นเลย มันคงจะดูแปลกๆถ้า user เปิดใช้งาน app แต่จู่ๆก็เห็นสถานะของตัวเอง offline เพราะว่าดันเปิดหลาย tab แล้วปิดไปสักตัวหนึ่ง

ดังนั้นที่ผมแก้ไขเพิ่มเติมจาก tutorial คือ ใช้ Cloud Function เป็นตัวลบ presence แทน ดังนี้

  • ไม่ลบ presence ทิ้งตอน disconnect
  • ใช้ Cloud Function ดัก event RTDB.update ของ sessions ว่ามีการ update เพื่อเพิ่ม endedAt หรือไม่
    • ถ้าไม่มีก็ข้าม ไม่ทำอะไร
    • ถ้ามี แสดงว่าเกิด disconnect บน browser ก็ให้เช็คว่ามี sessions ที่มี uid และไม่มี endedAt เหลืออยู่ไหม ถ้าไม่มีเลยก็ลบ presence ทันที

โค้ดตัวอย่างหลังจากปรับแล้วจะเป็นแบบนี้

// ดึง uid มาใช้งาน
const uid = firebase.auth().currentUser.uid
// สร้าง RTDB object เอาไว้ใช้งาน
const RTDB = firebase.database().ref()

// node พิเศษ เอามาดูได้ว่า uid ปัจจุบัน connect กับ Firebase อยู่หรือปล่าว
const amOnline = RTDB.child('.info/connected')
// ให้ RTDB ชี้ไปที่ /presence/{uid}
const presenceRef = RTDB.child('presence').child(uid)
// ให้ RTDB ชี้ไปที่ /sessions
const sessionsRef = RTDB.child('sessions')

amOnline.on('value', snapShot => {
  // ถ้ายังไม่ได้ connect กับ Firebase ก็ยังไม่ต้องทำอะไร
  if (!snapShot.val()) return

  // connect ได้แล้วสร้าง presence ได้เลย
  presenceRef.set(true)
  // เราจะไม่ทำ disconnect ในฝั่ง browser แล้ว แต่จะใช้ Cloud Function ลบให้แทน
  // presenceRef.onDisconnect().remove()

  // สร้าง session ขึ้นมาใหม่ โดยเก็บ uid และ startedAt
  const session = sessionsRef.push({
    startedAt: firebase.database.ServerValue.TIMESTAMP,
    uid: uid,
  })

  // ในกรณีที่ disconnect ให้เพิ่ม endedAt ใน session ที่เพิ่งสร้างมาเมื่อกี้
  session.child('endedAt')
    .onDisconnect()
    .set(firebase.database.ServerValue.TIMESTAMP)
})

ส่วนโค้ดใน Cloud Function จะเป็นแบบนี้

// ทำ setup env ต่างๆของ Firebase Function
const admin = require('firebase-admin')
const functions = require('firebase-functions')
const DEFAULT_REGION = 'asia-northeast1'
const functionRunner = functions.region(DEFAULT_REGION)

// สร้าง function สำหรับ event listener RTDB
exports.SessionDeleted = functionRunner.database
  // ตรงนี้บอกว่า เราจะ listen เฉพาะที่ /sessions/... เท่านั้นนะ
  .ref('/sessions/{sessionKey}')
  .onUpdate((snapshot, context) => {
    // ดึง data ที่ update มาใช้งาน
    const data = snapshot.data()
    // ไม่มี endedAt แสดงว่าไม่ได้ disconnect ข้าม
    if (!data.endedAt) return

    // ดึง uid มาใช้งาน
    const uid = data.uid

    // สร้าง object ของ RTDB โดยเอา presence และ sessions มาใช้งาน 
    const ref = database.ref()
    const presenceRef = ref.child('presence').child(uid)
    const sessionsRef = ref.child('sessions')

    // ตรงนี้เป็นการดึงข้อมูลใน RTDB sessions มาทำงาน
    sessionsRef.once('value', data => {
      // ดึงข้อมูล sessions มาใช้งาน กรณีที่ไม่มี ให้เอา empty array มาแทน
      const allSessions = data.val() || []
      // ทำ filter หา sessions ที่ยัง active อยู่ คือ มี uid และไม่มี endedAt
      const remainingSessions = Object.keys(allSessions).filter(
        sessionKey => allSessions[sessionKey].uid === uid 
          && !allSessions[sessionKey].endedAt,
      )

      // ถ้าไม่เจอ ก็ลบ presence ทิ้งเลย
      if (remainingSessions.length <= 0) {
        presenceRef.remove()
      }
    })
  })

สำหรับกรณีที่ 1 ที่ให้ priority ต่ำ เพราะสามารถทำ work around โดยใช้ debounce เพื่อขยายช่วงเวลาที่จะเปลี่ยนสถานะได้ โดยไม่ต้องไปแก้ไข logic ในการตรวจสอบ presence เลย

แล้วพบกันใหม่ สวัสดีครับ

Discussion

markdown guide