איך יוצרים נוכחות באפליקציות באמצעות Cloud Functions

בהתאם לסוג האפליקציה שאתם מפתחים, יכול להיות שיהיה לכם שימושי לזהות אילו מהמשתמשים או המכשירים שלכם מחוברים לאינטרנט באופן פעיל – או במילים אחרות, לזהות 'נוכחות'.

לדוגמה, אם אתם מפתחים אפליקציה כמו רשת חברתית או פורסים צי של מכשירי IoT, אתם יכולים להשתמש במידע הזה כדי להציג רשימה של חברים שמחוברים לאינטרנט וזמינים לצ'אט, או למיין את מכשירי ה-IoT לפי 'המיקום האחרון שבו המכשיר תועד'.

‫Firestore לא תומך באופן מובנה בסטטוס נוכחות, אבל אפשר להשתמש במוצרי Firebase אחרים כדי ליצור מערכת סטטוס נוכחות.

פתרון: Cloud Functions עם מסד נתונים בזמן אמת

כדי לקשר את Firestore לתכונת הנוכחות המקורית של Firebase Realtime Database, צריך להשתמש ב-Cloud Functions.

משתמשים ב-מסד נתונים בזמן אמת כדי לדווח על סטטוס החיבור, ואז משתמשים ב-Cloud Functions כדי לשכפל את הנתונים האלה ל-Firestore.

שימוש בנוכחות ב-Realtime Database

קודם כל, כדאי להבין איך מערכת נוכחות מסורתית פועלת ב-מסד נתונים בזמן אמת.

אינטרנט

// Fetch the current user's ID from Firebase Authentication.
var uid = firebase.auth().currentUser.uid;

// Create a reference to this user's specific status node.
// This is where we will store data about being online/offline.
var userStatusDatabaseRef = firebase.database().ref('/status/' + uid);

// We'll create two constants which we will write to 
// the Realtime database when this device is offline
// or online.
var isOfflineForDatabase = {
    state: 'offline',
    last_changed: firebase.database.ServerValue.TIMESTAMP,
};

var isOnlineForDatabase = {
    state: 'online',
    last_changed: firebase.database.ServerValue.TIMESTAMP,
};

// Create a reference to the special '.info/connected' path in 
// Realtime Database. This path returns `true` when connected
// and `false` when disconnected.
firebase.database().ref('.info/connected').on('value', function(snapshot) {
    // If we're not currently connected, don't do anything.
    if (snapshot.val() == false) {
        return;
    };

    // If we are currently connected, then use the 'onDisconnect()' 
    // method to add a set which will only trigger once this 
    // client has disconnected by closing the app, 
    // losing internet, or any other means.
    userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function() {
        // The promise returned from .onDisconnect().set() will
        // resolve as soon as the server acknowledges the onDisconnect() 
        // request, NOT once we've actually disconnected:
        // https://firebase.google.com/docs/reference/js/firebase.database.OnDisconnect

        // We can now safely set ourselves as 'online' knowing that the
        // server will mark us as offline once we lose connection.
        userStatusDatabaseRef.set(isOnlineForDatabase);
    });
});

הדוגמה הזו היא מערכת שלמה של נוכחות ב-Realtime Database. היא מטפלת בכמה ניתוקים, קריסות וכו'.

התחברות ל-Firestore

כדי להטמיע פתרון דומה ב-Firestore, משתמשים באותו קוד של מסד נתונים בזמן אמת, ואז משתמשים ב-Cloud Functions כדי לשמור על סנכרון בין מסד נתונים בזמן אמת ל-Firestore.

אם עדיין לא עשיתם את זה, מוסיפים את מסד נתונים בזמן אמת לפרויקט וכוללים את פתרון הנוכחות שלמעלה.

בשלב הבא מסנכרנים את מצב הנוכחות עם Firestore באמצעות השיטות הבאות:

  1. באופן מקומי, למטמון Firestore של המכשיר במצב אופליין, כדי שהאפליקציה תדע שהיא במצב אופליין.
  2. באופן גלובלי, באמצעות Cloud Function, כך שכל המכשירים האחרים שיש להם גישה ל-Firestore יודעים שהמכשיר הספציפי הזה נמצא במצב אופליין.

אי אפשר להפעיל באפליקציית לקוח את הפונקציות שמומלצות במדריך הזה. צריך לפרוס אותן ב-Cloud Functions for Firebase, והן דורשות לוגיקה בצד השרת מ-SDK של Firebase לאדמינים. הנחיות מפורטות זמינות במאמרי העזרה בנושא Cloud Functions.

עדכון המטמון המקומי של Firestore

בואו נבדוק את השינויים שנדרשים כדי לפתור את הבעיה הראשונה – עדכון המטמון המקומי של Firestore.

אינטרנט

// ...
var userStatusFirestoreRef = firebase.firestore().doc('/status/' + uid);

// Firestore uses a different server timestamp value, so we'll 
// create two more constants for Firestore state.
var isOfflineForFirestore = {
    state: 'offline',
    last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};

var isOnlineForFirestore = {
    state: 'online',
    last_changed: firebase.firestore.FieldValue.serverTimestamp(),
};

firebase.database().ref('.info/connected').on('value', function(snapshot) {
    if (snapshot.val() == false) {
        // Instead of simply returning, we'll also set Firestore's state
        // to 'offline'. This ensures that our Firestore cache is aware
        // of the switch to 'offline.'
        userStatusFirestoreRef.set(isOfflineForFirestore);
        return;
    };

    userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function() {
        userStatusDatabaseRef.set(isOnlineForDatabase);

        // We'll also add Firestore set here for when we come online.
        userStatusFirestoreRef.set(isOnlineForFirestore);
    });
});

בעקבות השינויים האלה, אנחנו מוודאים עכשיו שהסטטוס של Firestore במכשיר תמיד ישקף את הסטטוס של המכשיר (מחובר או לא מחובר לאינטרנט). כלומר, אפשר להאזין למסמך /status/{uid} ולהשתמש בנתונים כדי לשנות את ממשק המשתמש כך שישקף את סטטוס החיבור.

אינטרנט

userStatusFirestoreRef.onSnapshot(function(doc) {
    var isOnline = doc.data().state == 'online';
    // ... use isOnline
});

עדכון Firestore באופן גלובלי

למרות שהאפליקציה שלנו מדווחת על הנוכחות באינטרנט בצורה נכונה, הסטטוס הזה לא יהיה מדויק באפליקציות אחרות של Firestore, כי הכתיבה של הסטטוס 'אופליין' היא מקומית בלבד ולא תסונכרן כשהחיבור ישוחזר. כדי לפתור את הבעיה הזו, נשתמש ב-Cloud Function שעוקבת אחרי הנתיב status/{uid} ב-Realtime Database. כשערך מסוים ב-מסד נתונים בזמן אמת משתנה, הערך מסונכרן עם Firestore כדי שכל הסטטוסים של המשתמשים יהיו נכונים.

Node.js

firebase.firestore().collection('status')
    .where('state', '==', 'online')
    .onSnapshot(function(snapshot) {
        snapshot.docChanges().forEach(function(change) {
            if (change.type === 'added') {
                var msg = 'User ' + change.doc.id + ' is online.';
                console.log(msg);
                // ...
            }
            if (change.type === 'removed') {
                var msg = 'User ' + change.doc.id + ' is offline.';
                console.log(msg);
                // ...
            }
        });
    });

אחרי שתפרסו את הפונקציה הזו, תהיה לכם מערכת נוכחות מלאה שתפעל עם Firestore. למטה מוצגת דוגמה למעקב אחרי משתמשים שמתחברים לאינטרנט או מתנתקים ממנו באמצעות שאילתת where().

אינטרנט

firebase.firestore().collection('status')
    .where('state', '==', 'online')
    .onSnapshot(function(snapshot) {
        snapshot.docChanges().forEach(function(change) {
            if (change.type === 'added') {
                var msg = 'User ' + change.doc.id + ' is online.';
                console.log(msg);
                // ...
            }
            if (change.type === 'removed') {
                var msg = 'User ' + change.doc.id + ' is offline.';
                console.log(msg);
                // ...
            }
        });
    });

מגבלות

שימוש ב-Realtime Database כדי להוסיף נוכחות לאפליקציית Firestore הוא יעיל וניתן להרחבה, אבל יש לו כמה מגבלות:

  • ביטול כפילויות (Debouncing) – כשמאזינים לשינויים בזמן אמת ב-Firestore, סביר להניח שהפתרון הזה יפעיל כמה שינויים. אם השינויים האלה מפעילים יותר אירועים ממה שרוצים, צריך לבצע ידנית את ביטול הכפילויות של אירועי Firestore.
  • קישוריות – ההטמעה הזו מודדת את הקישוריות ל-Realtime Database, ולא ל-Firestore. אם סטטוס החיבור לכל מסד נתונים לא זהה, יכול להיות שהפתרון הזה ידווח על מצב נוכחות שגוי.
  • Android – ב-Android, מסד הנתונים בזמן אמת מתנתק מהבק-אנד אחרי 60 שניות של חוסר פעילות. חוסר פעילות פירושו שאין רכיבי listener פתוחים או פעולות בהמתנה. כדי שהחיבור יישאר פתוח, מומלץ להוסיף רכיב event listener של ערך לנתיב מלבד .info/connected. לדוגמה, אפשר להשתמש ב-FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced() בתחילת כל סשן. למידע נוסף, אפשר לעיין במאמר בנושא זיהוי מצב החיבור.