Crea presencia en apps con Cloud Functions

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 compilas una app como una red social o implementas una flota de dispositivos IoT, puedes usar esta información a fin de 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.

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

Solución: Cloud Functions con Realtime Database

Para conectar 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 a fin de duplicar esos datos en 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 varias desconexiones, fallas y otros.

Conéctate a Firestore

Para implementar una solución similar en Firestore, usa el mismo código de Realtime Database y, luego, usa Cloud Functions a fin de mantener la sincronización entre Realtime Database y 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, deberás sincronizar el estado de presencia con Firestore a través de los siguientes métodos:

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

Actualiza la caché local de Firestore

Revisemos los cambios necesarios para resolver el primer problema: actualizar la caché local de 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, nos aseguramos de que el estado local de Firestore siempre reflejará 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 Firestore a nivel mundial

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 Firestore, ya que nuestra escritura de estado “sin conexión” solo es local y no se sincronizará cuando se restablezca una conexión. Para solucionar este problema, usaremos una función de Cloud Functions, que supervisa la ruta status/{uid} de Realtime Database. Cuando cambie el valor de Realtime Database, el valor se sincronizará con Firestore para que los estados de todos los usuarios sean correctos.

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

Una vez que implementes esta función, tendrás un sistema de presencia completo que se ejecutará con Firestore. A continuación, se muestra un ejemplo de supervisión de los usuarios que se conectan o desconectan mediante 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 la app de Firestore es escalable y eficaz, pero presenta algunas limitaciones:

  • Debouncing: Cuando se detectan cambios en tiempo real en 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 Firestore de forma manual.
  • Conectividad: Esta implementación mide la conectividad a Realtime Database, no a 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.