Crear presencia en aplicaciones con Cloud Functions

En función del tipo de aplicación que estés creando, puede que te resulte útil detectar qué usuarios o dispositivos están online, lo que también se conoce como detectar la "presencia".

Por ejemplo, si estás creando una aplicación como una red social o desplegando una flota de dispositivos de IoT, puedes usar esta información para mostrar una lista de amigos que están online y disponibles para chatear, u ordenar tus dispositivos de IoT por "última vez que se ha visto".

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

Solución: Cloud Functions con Realtime Database

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

Usa Realtime Database para informar del estado de la conexión y, a continuación, usa Cloud Functions para replicar esos datos en Firestore.

Usar la presencia en Realtime Database

Primero, veamos 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 de presencia completo de Realtime Database. Gestiona varias desconexiones, fallos, etc.

Conectarse a Firestore

Para implementar una solución similar en Firestore, usa el mismo código de Realtime Database y, a continuación, usa Cloud Functions para mantener sincronizados Realtime Database y Firestore.

Si aún no lo has hecho, añade Realtime Database a tu proyecto e incluye la solución de presencia anterior.

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

  1. De forma local, en la caché de Firestore del dispositivo sin conexión para que la aplicación sepa que no tiene conexión.
  2. De forma global, se usa una función de Cloud para que todos los demás dispositivos que accedan a Firestore sepan que este dispositivo concreto no tiene conexión.

Las funciones recomendadas en este tutorial no se pueden ejecutar en una aplicación cliente. Deben implementarse en Cloud Functions para Firebase y requieren lógica del lado del servidor del SDK de administrador de Firebase. Para obtener instrucciones detalladas, consulta la documentación de Cloud Functions.

Actualizar la caché local de Firestore

Vamos a ver los cambios necesarios para solucionar 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 hemos asegurado de que el estado local de Firestore siempre refleje el estado online u offline del dispositivo. Esto significa que puedes escuchar el documento /status/{uid} y usar los datos para cambiar tu interfaz de usuario de forma que refleje el estado de la conexión.

Web

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

Actualizar Firestore de forma global

Aunque nuestra aplicación informa correctamente de la presencia online a sí misma, este estado aún no será preciso en otras aplicaciones de Firestore porque nuestra escritura de estado "sin conexión" es solo local y no se sincronizará cuando se restaure una conexión. Para evitarlo, usaremos una función de Cloud Functions que monitorice la ruta status/{uid} en Realtime Database. Cuando el valor de Realtime Database cambie, se sincronizará con Firestore para que el estado de todos los usuarios sea correcto.

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 hayas implementado esta función, tendrás un sistema de presencia completo en funcionamiento con Firestore. A continuación, se muestra un ejemplo de monitorizació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

Usar Realtime Database para añadir la función de presencia a tu aplicación de Firestore es una solución eficaz y escalable, pero tiene algunas limitaciones:

  • Rebote: al escuchar los cambios en tiempo real en Firestore, es probable que esta solución active varios cambios. Si estos cambios activan más eventos de los que quiere, aplique manualmente un rebote a los eventos de Firestore.
  • Conectividad: esta implementación mide la conectividad con Realtime Database, no con Firestore. Si el estado de la conexión a cada base de datos no es el mismo, esta solución podría informar de un estado de presencia incorrecto.
  • Android en Android, Realtime Database se desconecta del backend tras 60 segundos de inactividad. La inactividad significa que no hay listeners abiertos ni operaciones pendientes. Para mantener la conexión abierta, te recomendamos que añadas un receptor de eventos de valor a una ruta que no sea .info/connected. Por ejemplo, puedes hacerlo FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced() al principio de cada sesión. Para obtener más información, consulta Detecting Connection State (Detectar el estado de la conexión).