Cloud Functions を使用してアプリのプレゼンスを構築する

構築しているアプリのタイプによっては、オンラインでアクティブになっているユーザーやデバイスを検出できると便利な場合もあります(「プレゼンス」検出とも呼ばれます)。

たとえば、ソーシャル ネットワークのようなアプリを構築している場合や、一連の IoT デバイスを導入している場合は、この情報を使用してオンラインでチャットできる友だちのリストを表示することや、IoT デバイスを「最終検知」順に並べ替えることができます。

Firestore はプレゼンスをネイティブでサポートしていませんが、他の Firebase プロダクトを利用してプレゼンス システムを構築できます。

ソリューション: Cloud Functions で Realtime Database を使用する

Firestore を Firebase Realtime Database のネイティブ プレゼンス機能に接続するには、Cloud Functions を使用します。

Realtime Database を使用して接続ステータスを報告してから、Cloud Functions を使用してそのデータを 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 のプレゼンス システム全体を示しています。これで複数の切断、クラッシュなどを処理できます。

Firestore への接続

Firestore で同様のソリューションを実装するには、同じ Realtime Database コードを使用したうえで、Cloud Functions を使用して Realtime Database と Firestore を同期させます。

まだ Realtime Database をプロジェクトに追加していない場合は、追加して上記のプレゼンス ソリューションを含めます。

次に、以下の方法でプレゼンス状態を Firestore に同期させます。

  1. ローカルに同期。オフライン デバイスの Firestore キャッシュに同期して、それがオフラインであることをアプリが認識できるようにします。
  2. グローバルに同期。Cloud Function の関数を使用して、Firestore にアクセスする他のすべてのデバイスが、この特定のデバイスがオフラインであることを認識できるようにします。

Firestore のローカル キャッシュを更新する

次に、最初の問題である 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);
    });
});

これらの変更により、ローカルの Firestore の状態が常にデバイスのオンライン / オフライン ステータスを反映するようになりました。つまり、/status/{uid} ドキュメントをリッスンし、そのデータを使用して接続ステータスに合わせて UI を変更できます。

ウェブ

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

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

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

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

この関数をデプロイすると、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 を使用して Firestore アプリにプレゼンスを追加するという方法は、スケーラブルで効果的ですが、いくつかの制限があります。

  • デバウンス - Firestore でリアルタイムの変更をリッスンしている場合、このソリューションは複数の変更を引き起こす可能性があります。変更により必要以上のイベントが発生する場合は、手動で Firestore イベントのデバウンスを行ってください。
  • 接続性 - この実装は Firestore との接続性ではなく、Realtime Database との接続性に対処するものです。各データベースへの接続ステータスが同じでない場合、このソリューションは誤ったプレゼンス状態を報告する可能性があります。
  • Android - Android では、非アクティブ状態が 60 秒間続くと Realtime Database がバックエンドから切断されます。非アクティブとは、オープン リスナーや保留中のオペレーションがないことを意味します。接続を開いたままに維持するために、.info/connected 以外のパスに値イベント リスナーを追加することをおすすめします。たとえば、各セッションの開始時に FirebaseDatabase.getInstance().getReference((new Date()).toString()).keepSynced() を行うことができます。詳しくは、接続状態の検出を参照してください。