Crea una presencia en Cloud Firestore

Dependiendo del tipo de app que quieras crear, te podría resultar útil detectar qué 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 creando 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 tus 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.

Usa 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, fallas y otros.

Conéctate 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 Cloud Function para que todos los demás dispositivos que acceden a Cloud Firestore sepan que este dispositivo específico está sin conexión.

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

Estos cambios permiten asegurarnos 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 realizar cambios en la IU a fin de reflejar el estado de conexión.

Web

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

Actualiza Cloud Firestore de manera global

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 solucionar este problema, usaremos una Cloud Function que supervisa la ruta de acceso status/{uid} de 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 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);
    });

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 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

El uso de Realtime Database para agregar presencia a tu app de Cloud Firestore es escalable y eficaz, pero presenta algunas limitaciones, como las siguientes:

  • Debouncing: cuando se escuchan cambios en tiempo real en Cloud Firestore, es probable que esta solución active varios de ellos. Si estos cambios activan más eventos de los que deseas, desactiva los eventos de Cloud Firestore de forma manual.
  • Conectividad: esta implementación mide la conectividad a Realtime Database, no a Cloud Firestore. Si el estado de la conexión a cada base de datos no es el mismo, es posible que esta solución informe un estado de presencia incorrecto.
  • Android: en Android, Realtime Database se desconecta del backend después de 60 segundos de inactividad. Inactividad se refiere a que no hay objetos de escucha abiertos ni operaciones pendientes. Para mantener abierta la conexión, te recomendamos que agregues un objeto de escucha de eventos de valores a una ruta distinta de .info/connected. Por ejemplo, podrías usar FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced() al inicio de cada sesión. Para obtener más información, consulta Detecta el estado de conexión.