Как создать статус в Cloud Firestore

В зависимости от типа создаваемого приложения вам может быть полезно определить, какие из ваших пользователей или устройств активно находятся в сети, что также известно как определение «присутствия».

Например, если вы создаете приложение, похожее на социальную сеть, или развертываете парк устройств IoT, вы можете использовать эту информацию, чтобы отобразить список друзей, которые находятся в сети и могут общаться, или отсортировать устройства IoT по времени последнего посещения.

Cloud Firestore не поддерживает статус присутствия, но вы можете использовать другие продукты Firebase, чтобы создать систему статусов.

Решение: Cloud Functions с Realtime Database

Чтобы подключить Cloud Firestore к встроенной функции присутствия Firebase Realtime Database, используйте Cloud Functions.

Используйте Realtime Database, чтобы сообщать о статусе подключения, а затем используйте Cloud Functions, чтобы отразить эти данные в Cloud Firestore.

Как использовать статус присутствия в Realtime Database

Сначала рассмотрим, как работает традиционная система присутствия в Realtime Database.

Веб-приложение

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

В этом примере показана полная система статусов Realtime Database. Она справляется с многочисленными отключениями, сбоями и т. д.

Подключение к устройству "Cloud Firestore"…

Чтобы реализовать аналогичное решение в Cloud Firestore, используйте тот же код Realtime Database, а затем используйте Cloud Functions для синхронизации Realtime Database и Cloud Firestore.

Если вы ещё этого не сделали, добавьте Realtime Database в проект и включите указанное выше решение для отслеживания статуса.

Затем вы синхронизируете статус присутствия с Cloud Firestore следующими способами:

  1. Локально, в кеш Cloud Firestore офлайн-устройства, чтобы приложение знало, что оно находится в офлайн-режиме.
  2. Глобально, с помощью функции Cloud Functions, чтобы все остальные устройства, обращающиеся к Cloud Firestore, знали, что это устройство офлайн.

Функции, рекомендуемые в этом руководстве, не могут выполняться в клиентском приложении. Их необходимо развернуть в Cloud Functions for Firebase, и для них требуется логика на стороне сервера из Firebase Admin SDK. Подробные инструкции можно найти в документации по Cloud Functions.

Обновление локального кеша пользователя Cloud Firestore

Давайте посмотрим, какие изменения нужно внести, чтобы решить первую проблему – обновить локальный кеш Cloud Firestore.

Веб-приложение

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

Теперь локальное состояние Cloud Firestore всегда будет отражать статус устройства (онлайн или офлайн). Это значит, что вы можете прослушивать документ /status/{uid} и использовать данные, чтобы изменять интерфейс в зависимости от статуса подключения.

Веб-приложение

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

Обновление Cloud Firestore глобально

Хотя наше приложение правильно сообщает о присутствии в интернете, этот статус пока не будет точным в других приложениях Cloud Firestore, поскольку запись статуса "офлайн" является только локальной и не будет синхронизирована при восстановлении подключения. Чтобы этого избежать, мы будем использовать функцию Cloud Functions, которая отслеживает путь status/{uid} в Realtime Database. Когда значение в Realtime Database меняется, оно синхронизируется с Cloud Firestore, чтобы статусы всех пользователей были правильными.

Node.js

const functions = require('firebase-functions/v1');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
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 = getFirestore();

// Create a new function which is triggered on changes to /status/{uid}
// Note: This is a Realtime Database trigger, *not* 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();
      functions.logger.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);
    });

После развертывания этой функции у вас будет полностью работающая система присутствия с Cloud Firestore. Ниже приведен пример отслеживания пользователей, которые входят в сеть или выходят из нее, с помощью запроса where().

Веб-приложение

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

Ограничения

Использование Realtime Database для добавления статуса присутствия в приложение Cloud Firestore – это масштабируемое и эффективное решение, но у него есть некоторые ограничения:

  • Устранение дребезга. При отслеживании изменений в Cloud Firestore в реальном времени это решение может вызвать несколько изменений. Если эти изменения вызывают больше событий, чем вы хотите, вручную устраните дребезг событий Cloud Firestore.
  • Подключение. Эта реализация измеряет подключение к Realtime Database, а не к Cloud Firestore. Если статус подключения к каждой базе данных не совпадает, это решение может сообщать о неверном статусе присутствия.
  • Android. На устройствах Android Realtime Database отключается от серверной части после 60 секунд бездействия. Бездействие означает отсутствие открытых прослушивателей или ожидающих операций. Чтобы соединение не закрывалось, рекомендуем добавить прослушиватель событий value к пути, отличному от .info/connected. Например, вы можете выполнять FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced() в начале каждого занятия. Подробнее о том, как определить состояние подключения…