בניית נוכחות ב-Cloud Firestore

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

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

ב-Cloud Firestore אין תמיכה מובנית בנוכחות, אבל אפשר להשתמש במוצרים אחרים של Firebase כדי ליצור מערכת נוכחות.

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

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

משתמשים ב-Realtime Database כדי לדווח על סטטוס החיבור, ואז משתמשים ב-Cloud Functions כדי לשקף את הנתונים האלה ב-Cloud 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);
    });
});

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

מתבצע חיבור אל Cloud Firestore

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

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

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

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

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

מתבצע עדכון של המטמון המקומי של Cloud Firestore

בואו נראה מה צריך לשנות כדי לפתור את הבעיה הראשונה – עדכון המטמון המקומי של Cloud 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);
    });
});

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

אינטרנט

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

עדכון של Cloud Firestore בכל המקומות

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

Node.js

const functions = require('firebase-functions/v1');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
initializeApp();

// Since this code will be running in the Cloud Functions environment
// we call initialize Firestore without any arguments because it
// detects authentication from the environment.
const firestore = getFirestore();

// Create a new function which is triggered on changes to /status/{uid}
// Note: This is a Realtime Database trigger, *not* Firestore.
exports.onUserStatusChanged = functions.database.ref('/status/{uid}').onUpdate(
    async (change, context) => {
      // Get the data written to Realtime Database
      const eventStatus = change.after.val();

      // Then use other event data to create a reference to the
      // corresponding Firestore document.
      const userStatusFirestoreRef = firestore.doc(`status/${context.params.uid}`);

      // It is likely that the Realtime Database change that triggered
      // this event has already been overwritten by a fast change in
      // online / offline status, so we'll re-read the current data
      // and compare the timestamps.
      const statusSnapshot = await change.after.ref.once('value');
      const status = statusSnapshot.val();
      functions.logger.log(status, eventStatus);
      // If the current timestamp for this data is newer than
      // the data that triggered this event, we exit this function.
      if (status.last_changed > eventStatus.last_changed) {
        return null;
      }

      // Otherwise, we convert the last_changed field to a Date
      eventStatus.last_changed = new Date(eventStatus.last_changed);

      // ... and write it to Firestore.
      return userStatusFirestoreRef.set(eventStatus);
    });

אחרי פריסת הפונקציה, תהיה לכם מערכת נוכחות מלאה שפועלת עם Cloud 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);
                // ...
            }
        });
    });

מגבלות

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

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