Crea la presenza nelle app con Cloud Functions

A seconda del tipo di app che stai creando, potrebbe essere utile rilevare quali utenti o dispositivi sono attivamente online, il che è noto come rilevare la "presenza".

Ad esempio, se stai creando un'app come un social network o stai implementando un parco di dispositivi IoT, potresti utilizzare queste informazioni per visualizzare un elenco di amici online e disponibili a chattare oppure ordinare i dispositivi IoT in base all'ultimo accesso.

Firestore non supporta la presenza in modo nativo, ma puoi sfruttare altri prodotti Firebase per creare un sistema di presenza.

Soluzione: Cloud Functions con Realtime Database

Per connettere Firestore alla funzionalità di presenza nativa di Firebase Realtime Database, usa Cloud Functions.

Utilizza Realtime Database per segnalare lo stato della connessione, quindi usa Cloud Functions per eseguire il mirroring dei dati in Firestore.

Utilizzo della presenza in Realtime Database

Innanzitutto, considera il funzionamento di un sistema di presenza tradizionale in 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);
    });
});

Questo esempio è un sistema completo di presenza con Realtime Database. Gestisce più disconnessioni, arresti anomali e così via.

Connessione a Firestore

Per implementare una soluzione simile in Firestore, utilizza lo stesso codice Realtime Database, quindi usa Cloud Functions per mantenere sincronizzati Realtime Database e Firestore.

Se non lo hai già fatto, aggiungi Realtime Database al tuo progetto e includi la soluzione per la presenza di persone indicata sopra.

Successivamente, sincronizzerai lo stato della presenza con Firestore utilizzando i seguenti metodi:

  1. A livello locale, nella cache di Firestore del dispositivo offline in modo che l'app sappia che è offline.
  2. A livello globale, utilizzo di una Cloud Function in modo che tutti gli altri dispositivi che accedono a Firestore sappiano che questo dispositivo specifico è offline.

Aggiornamento della cache locale di Firestore in corso...

Vediamo le modifiche necessarie per soddisfare il primo problema: l'aggiornamento della cache locale di 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 queste modifiche abbiamo assicurato che lo stato di Firestore locale rifletterà sempre lo stato online/offline del dispositivo. Ciò significa che puoi ascoltare il documento /status/{uid} e utilizzare i dati per modificare la UI in modo che rifletta lo stato della connessione.

Web

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

Aggiornamento di Firestore a livello globale

Anche se la nostra applicazione segnala correttamente la presenza online, questo stato non sarà ancora preciso in altre app Firestore perché la nostra scrittura dello stato "offline" è solo locale e non verrà sincronizzata quando viene ripristinata una connessione. Per contrastare questo problema, utilizzeremo una Cloud Function che monitora il percorso status/{uid} in Realtime Database. Quando il valore di Realtime Database cambia, il valore viene sincronizzato con Firestore in modo che gli stati di tutti gli utenti siano corretti.

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 volta eseguito il deployment di questa funzione, avrai un sistema di presenza completo in esecuzione con Firestore. Di seguito è riportato un esempio di monitoraggio di tutti gli utenti che sono online o sono offline utilizzando una query 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);
                // ...
            }
        });
    });

Limitazioni

L'utilizzo di Realtime Database per aggiungere la presenza all'app Firestore è scalabile ed efficace, ma presenta alcune limitazioni:

  • Debouncing: quando ascolti modifiche in tempo reale in Firestore, è probabile che questa soluzione attivi più modifiche. Se queste modifiche attivano più eventi di quanto vuoi, esegui il debounce manuale degli eventi Firestore.
  • Connettività: questa implementazione misura la connettività a Realtime Database, non a Firestore. Se lo stato della connessione a ogni database non è lo stesso, questa soluzione potrebbe segnalare uno stato di presenza non corretto.
  • Android: su Android, il Realtime Database si disconnette dal backend dopo 60 secondi di inattività. Inattività significa che non esistono listener o operazioni in attesa aperti. Per mantenere aperta la connessione, ti consigliamo di aggiungere un listener di eventi valore a un percorso diverso da .info/connected. Ad esempio, potresti eseguire FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced() all'inizio di ogni sessione. Per ulteriori informazioni, consulta Rilevamento dello stato della connessione.