Crear una presencia en Cloud Firestore

Dependiendo del tipo de app que estés compilando, te podría resultar útil detectar cuáles de tus usuarios o dispositivos están en línea de manera activa, lo que también se conoce como la detección de "presencia".

Por ejemplo, si estás compilando una app como una red social o implementando una flota de dispositivos IoT, puedes usar esta información para mostrar una lista de amigos que están en línea y disponibles para chatear, o clasificar sus dispositivos IoT según la última vez que estuvieron conectados.

Cloud Firestore no admite la presencia de forma nativa, pero puedes aprovechar otros productos de Firebase para crear un sistema de presencia.

Solución: Cloud Functions con Realtime Database

Para conectar Cloud Firestore a la función de presencia nativa de Firebase Realtime Database, usa Cloud Functions.

Usa Realtime Database para informar estados de conexiones y, luego, usa Cloud Functions para duplicar esos datos en Cloud Firestore.

Uso de la presencia en Realtime Database

Primero, es importante entender cómo funciona un sistema de presencia tradicional en 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);
    });
});

Este ejemplo es un sistema completo de presencia de Realtime Database. Controla múltiples desconexiones, bloqueos y otros.

Conexión a Cloud Firestore

Para implementar una solución similar en Cloud Firestore, usa el mismo código de Realtime Database y, luego, usa Cloud Functions para mantener la sincronización entre Realtime Database y Cloud Firestore.

Si aún no lo hiciste, agrega Realtime Database a tu proyecto y, luego, incluye la solución de presencia que aparece más arriba.

A continuación, sincronizarás el estado de presencia con Cloud Firestore a través de los siguientes métodos:

  1. De manera local, en la caché de Cloud Firestore del dispositivo sin conexión para que la app sepa que está sin conexión.
  2. De manera global, con una función de Cloud Functions para que todos los demás dispositivos que acceden a Cloud Firestore sepan que este dispositivo está sin conexión.

Actualización de la caché local de Cloud Firestore

Revisemos los cambios necesarios para resolver el primer problema: actualizar la caché local de Cloud 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);
    });
});

Con estos cambios ahora nos hemos asegurado de que el estado local de Cloud Firestore refleje siempre el estado en línea o sin conexión del dispositivo. Esto significa que puedes escuchar el documento /status/{uid} y usar los datos para cambiar tu IU a fin de que refleje el estado de la conexión.

Web

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

Actualización global de Cloud Firestore

Si bien nuestra aplicación informa la presencia en línea a sí misma de manera correcta, este estado aún no será preciso en otras apps de Cloud Firestore, ya que nuestra escritura de estado "sin conexión" es local y no se sincronizará cuando se restablezca una conexión. Para contrarrestar esto, usaremos una función de Cloud Functions que observa la ruta de acceso status/{uid} en Realtime Database. Cuando cambia el valor de Realtime Database, el valor se sincronizará con Cloud Firestore para que los estados de todos los usuarios sean correctos.

Node.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();

// Since this code will be running in the Cloud Functions enviornment
// 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* Cloud Firestore.
exports.onUserStatusChanged = functions.database.ref('/status/{uid}').onUpdate(
    (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.
      return data.after.ref.once('value').then((statusSnapshot) => {
        const status = statusSnapshot.val();
        console.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);
      });
    });

Una vez que implementes esta función, tendrás un sistema de presencia completo que se ejecutará con Cloud Firestore. A continuación, se muestra un ejemplo de supervisión de los usuarios que se conectan o se desconectan con una consulta 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);
                // ...
            }
        });
    });

Limitaciones

La solución anterior es una forma escalable de crear un sistema de presencia en Cloud Firestore, pero ten en cuenta que es probable que active varios cambios cuando escucha los cambios en tiempo real en Cloud Firestore. Si los cambios activan más eventos de los que deseas, desactiva los eventos de Cloud Firestore de forma manual.

Esta implementación mide la conectividad a Realtime Database, no a Cloud Firestore, por lo que es posible que no sea correcta en todo momento.

Enviar comentarios sobre...

Si necesitas ayuda, visita nuestra página de asistencia.