Como criar presença

Dependendo do tipo de app que você está desenvolvendo, pode ser útil detectar quais dos seus usuários ou dispositivos estão on-line ativamente. Essa ação também é conhecida como detectar "presença".

Por exemplo, se você estiver criando um app como uma rede social ou implementando uma frota de dispositivos de Internet das Coisas (IoT, na sigla em inglês), poderá usar essas informações para exibir uma lista de amigos que estão on-line e disponíveis para conversar ou classificar seus dispositivos de IoT por "visto pela última vez".

O Firestore não tem compatibilidade nativa com o recurso de presença, mas você pode aproveitar outros produtos do Firebase para criar um sistema de presença.

Solução: Cloud Functions com Realtime Database

Para conectar o Firestore ao recurso de presença nativa do Firebase Realtime Database, use o Cloud Functions.

Use o Realtime Database para reportar o status da conexão e, em seguida, use o Cloud Functions para espelhar esses dados no Firestore.

Como usar o recurso de presença no Realtime Database

Primeiro, pense em como um sistema de presença tradicional funciona no 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);
    });
});

Esse exemplo é um sistema completo de presença no Realtime Database. Ele lida com várias desconexões, falhas e assim por diante.

Como se conectar ao Firestore

Para implementar uma solução similar no Firestore, use o mesmo código do Realtime Database e, em seguida, use o Cloud Functions para sincronizar o Realtime Database e o Firestore.

Adicione o Realtime Database ao seu projeto e inclua a solução de presença acima se ainda não tiver feito isso.

Em seguida, você sincronizará o estado de presença com o Firestore por meio dos seguintes métodos:

  1. Localmente, para o cache do Firestore do dispositivo off-line, para que o saiba que está off-line.
  2. Globalmente, use uma Função do Cloud para que todos os outros dispositivos que acessam o Firestore saibam que esse dispositivo específico está off-line.

Como atualizar o cache local do Firestore

Vamos dar uma olhada nas mudanças necessárias para solucionar o primeiro problema, que é atualizar o cache local do 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);
    });
});

Com essas mudanças, garantimos que o estado local do Firestore sempre refletirá o status on-line/off-line do dispositivo. Isso significa que você pode detectar o documento /status/{uid} e usar os dados para alterar a IU a fim de refletir o status da conexão.

Web

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

Como atualizar o Firestore globalmente

Nosso aplicativo já consegue reportar a presença on-line para si mesmo corretamente, mas esse status ainda não é preciso em outros apps do Firestore. Isso acontece porque nossa gravação de status "off-line" é apenas local e não será sincronizada quando uma conexão for restaurada. Para resolver isso, usamos uma Função do Cloud que monitora o caminho status/{uid} no Realtime Database. Quando o valor do Realtime Database mudar, ele será sincronizado com o Firestore para que os status dos usuários fiquem corretos.

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* Cloud 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();
      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);
    });

Depois de implementar essa função, você terá um sistema de presença completo com o Firestore. Veja abaixo um exemplo de monitoramento para qualquer usuário que ficar on-line ou off-line usando uma 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);
                // ...
            }
        });
    });

Limitações

O uso do Realtime Database para adicionar presença ao app do Firestore é dimensionável e eficaz, mas tem algumas limitações:

  • Debouncing - é provável que essa solução acione várias mudanças ao detectar alterações em tempo real no Firestore. Se as mudanças acionarem mais eventos do que você quer, faça o debouncing manual dos eventos do Firestore.
  • Conectividade - essa implementação avalia a conectividade com o Realtime Database, e não com o Firestore. Se o status da conexão em cada banco de dados não for igual, essa solução poderá informar um estado de presença incorreto.
  • Android: o Realtime Database se desconecta do back-end após 60 segundos de inatividade, que é a falta de listeners abertos ou operações pendentes. Para manter a conexão aberta, recomendamos que você adicione um listener de evento de valor a um caminho além de .info/connected. Por exemplo, execute FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced() no início de cada sessão. Para mais informações, consulte Como detectar o estado da conexão.