Créer une présence

Selon le type d'application que vous créez, il peut être utile de détecter les utilisateurs ou appareils actifs en ligne, c'est-à-dire la détection de "présence".

Par exemple, si vous créez une application comme un réseau social ou déployez une flotte d'appareils IoT, vous pouvez utiliser cette information pour afficher une liste d'amis en ligne disponibles pour discuter ou trier vos appareils IoT par "dernière activité".

Firestore n'est pas compatible avec la présence en natif, mais vous pouvez exploiter d'autres produits Firebase pour créer un système de présence.

Solution : Cloud Functions avec Realtime Database

Pour associer Firestore à la fonctionnalité de présence native de Firebase Realtime Database, utilisez Cloud Functions.

Utilisez Realtime Database pour signaler l'état de la connexion, puis utilisez Cloud Functions pour mettre en miroir ces données dans Firestore.

Utilisation de la présence dans Realtime Database

Commencez par examiner le fonctionnement d'un système de présence traditionnel dans Realtime Database.

Web

// 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);
    });
});

Cet exemple est un système complet de présence Realtime Database. Il gère plusieurs déconnexions, plantages, etc.

Se connecter à Firestore

Pour mettre en œuvre une solution similaire dans Firestore, utilisez le même code Realtime Database, puis utilisez Cloud Functions pour synchroniser Realtime Database et Firestore.

Si ce n'est pas déjà fait, ajoutez Realtime Database à votre projet et incluez la solution de présence ci-dessus.

Vous allez ensuite synchroniser l'état de présence avec Cloud Firestore à l'aide des méthodes suivantes :

  1. Localement, vers le cache Firestore de l'appareil hors connexion, afin que l'application sache qu'il est hors connexion.
  2. Globalement, en utilisant une fonction Cloud permettant à tous les autres appareils qui accèdent à Firestore de savoir que cet appareil est hors connexion.

Mise à jour du cache local Firestore

Examinons les modifications nécessaires pour résoudre le premier problème : mettre à jour le cache local Firestore.

Web

// ...
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);
    });
});

Grâce à ces modifications, nous avons veillé à ce que l'état local Firestore reflète toujours l'état en ligne ou hors connexion de l'appareil. Cela signifie que vous pouvez écouter le document /status/{uid} et utiliser les données pour modifier votre interface utilisateur afin de refléter l'état de la connexion.

Web

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

Mise à jour Firestore globale

Bien que notre application signale correctement la présence en ligne, cet état ne sera pas encore exact dans les autres applications Firestore, car l'écriture de l'état "Hors connexion" est seulement locale, et elle ne sera pas synchronisée tant qu'une connexion ne sera pas restaurée. Pour contourner ce problème, nous allons utiliser une fonction Cloud qui surveille le chemin status/{uid} dans Realtime Database. Lorsque la valeur Realtime Database change, la valeur est synchronisée avec Firestore de sorte que l'état de tous les utilisateurs soit correct.

Node.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.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 = admin.firestore();

// 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);
    });

Une fois cette fonction déployée, vous disposerez d'un système de présence complet avec Firestore. Vous trouverez ci-dessous un exemple de surveillance des utilisateurs qui se connectent ou se déconnectent via une requête where().

Web

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);
                // ...
            }
        });
    });

Limites

L'utilisation de Realtime Database pour ajouter une présence à votre application Firestore est évolutive et efficace, mais présente certaines limites :

  • Fonctionnalité anti-rebond : lorsque vous écoutez des modifications en temps réel dans Firestore, cette solution est susceptible de déclencher plusieurs modifications. Si ces modifications déclenchent plus d'événements que vous le souhaitez, annulez manuellement les événements Firestore.
  • Connectivité : cette implémentation mesure la connectivité à Realtime Database et non à Firestore. Si l'état de la connexion à chaque base de données n'est pas le même, cette solution peut signaler un état de présence incorrect.
  • Android : sur Android, Realtime Database se déconnecte du backend après 60 secondes d'inactivité. L'inactivité signifie qu'il n'y a pas d'écouteurs ouverts ni d'opérations en attente. Pour que la connexion reste ouverte, nous vous recommandons d'ajouter un écouteur d'événements de valeur à un chemin d'accès autre que .info/connected. Par exemple, vous pouvez stipuler FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced() au début de chaque session. Pour en savoir plus, consultez la page Détecter l'état de la connexion.