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 Function を使用して、Cloud Firestore にアクセスする他のすべての端末が、この特定の端末がオフラインであることを認識できるようにします。

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} ドキュメントをリッスンし、そのデータを使用して接続ステータスに合わせて UI を変更できます。

ウェブ

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

Cloud Firestore をグローバルに更新する

アプリケーションは自身にはオンライン プレゼンスを正しく報告しますが、他の Cloud Firestore アプリではこのステータスは正確ではありません。「オフライン」ステータスでの書き込みはローカルのみで行われ、接続が復元されたときに同期されないからです。これに対処するために、Realtime Database の status/{uid} パスを監視する Cloud Function を使用します。Realtime Database の値が変更されると、その値が Cloud Firestore に同期され、すべてのユーザーのステータスが正しくなるようにします。

Node.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp();

// Since this code will be running in the Cloud Functions enviornment
// 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(
    (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.
      return data.after.ref.once('value').then((statusSnapshot) => {
        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);
      });
    });

この関数をデプロイすると、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);
                // ...
            }
        });
    });

制限事項

上記のソリューションは、Cloud Firestore でプレゼンス システムを作成するスケーラブルな方法ですが、Cloud Firestore でのリアルタイム変更をリッスンしているときに複数の変更がトリガーされる可能性があることに注意してください。変更により必要以上のイベントが発生する場合は、手動で Cloud Firestore イベントのデバウンスを行ってください。

この実装は、Cloud Firestore ではなく Realtime Database への接続性を測定するものであるため、常に正しく機能するとは限らない可能性があります。

フィードバックを送信...

ご不明な点がありましたら、Google のサポートページをご覧ください。