ต่อจากบทความที่แล้ว
ผมทิ้งท้ายไว้ว่า tutorial ของ Firebase มีจุดอ่อนที่
- กรณีที่ reload page จะทำให้สถานะหยุด online แป๊ปนึง เพราะ Firebase จะโค้ดจะสร้าง session ใหม่
- กรณีที่ user เปิดหลาย tab และปิดไปสัก tab นึง จะทำให้สถานะกลายเป็น offline ทั้งๆที่เปิดบาง tab ค้างไว้
- กรณีที่ 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 เลย
แล้วพบกันใหม่ สวัสดีครับ
Top comments (0)