Criar presença no Cloud Firestore

Dependendo do tipo de app que você está criando, pode ser útil detectar quais dos seus usuários ou dispositivos estão efetivamente on-line. Essa atividade 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), 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".

Cloud Firestore não oferece suporte nativo à presença, mas é possível aproveite outros produtos do Firebase para criar um sistema de presença.

Solução: Cloud Functions com Realtime Database

Para conectar o Cloud Firestore ao banco de dados nativo do Firebase Realtime Database recurso de presença do Google, use o Cloud Functions.

Use o Realtime Database para informar o status da conexão e, em seguida, use o Cloud Functions para espelhar esses dados no Cloud 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.

Conectando ao dispositivo: Cloud Firestore

Para implementar uma solução semelhante no Cloud Firestore, use o mesmo código do Realtime Database e, em seguida, use o Cloud Functions para sincronizar o Realtime Database e o Cloud 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ê vai sincronizar o estado de presença para Cloud Firestore por meio de os seguintes métodos:

  1. Localmente, para o cache Cloud Firestore do dispositivo off-line para que o app sabe que está off-line.
  2. Globalmente, usando uma função do Cloud para que todos os outros dispositivos que acessam Cloud Firestore sabe que este dispositivo específico está off-line.

Atualizando o cache local de Cloud Firestore

Vamos dar uma olhada nas mudanças necessárias para resolver o primeiro problema: atualizar Cache 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);
    });
});

Com essas mudanças, garantimos que o estado local Cloud Firestore sempre refletem o status on-line/off-line do dispositivo. Isso significa que você pode ouvir /status/{uid} documento e use os dados para mudar a interface e refletir a conexão o status atual da conta.

Web

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

Como atualizar Cloud Firestore globalmente

Embora nosso aplicativo já consiga reportar a presença on-line para si mesmo corretamente, esse status ainda não é preciso em outros apps Cloud 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 fazer uma contraproposta isso, vamos usar uma função do Cloud que monitora o caminho status/{uid} em tempo real no seu banco de dados. Quando o valor do Realtime Database mudar, ele será sincronizado com Cloud Firestore para que todos os usuários estão corretos.

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

Depois de implementar essa função, você terá um sistema de presença completo em execução com Cloud Firestore. Veja abaixo um exemplo de monitoramento para usuários que fique 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

Usar o Realtime Database para adicionar presença ao seu app Cloud Firestore é escalonável e eficaz, mas tem algumas limitações:

  • Debouncing: ao detectar mudanças em tempo real Cloud Firestore, é provável que essa solução acione várias mudanças. Se essas mudanças acionarem mais eventos do que você quer, manualmente debounce os eventos de Cloud Firestore.
  • Conectividade: essa implementação mede a conectividade com o Realtime Banco de dados, não para Cloud Firestore. Se o status da conexão com 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 eventos 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.