Firebase アプリケーションは、ご使用のアプリのネットワーク接続が一時的に切断された場合でも機能します。Firebase では、プレゼンスをモニタリングしたり、ローカルの状態をサーバーの状態と同期したりするツールがいくつも用意されています。ここでは、そうしたツールについて説明していきます。
プレゼンスの管理
リアルタイム アプリケーションでは、クライアントが接続したり接続解除したりするタイミングを検出できると役に立つことがよくあります。たとえば、クライアントが接続を解除したときにそのユーザーを「オフライン」としてマークする場合などに有用です。
Firebase Database クライアントには、クライアントが Firebase Database サーバーから接続解除されたときにデータベースにデータを書き込むことができるシンプルなプリミティブが用意されています。これらの更新処理はクライアントが正常に接続を解除したかどうかにかかわらず行われるため、接続が失われた場合やクライアントがクラッシュした場合でもデータが確実にクリーンアップされます。また、設定、更新、削除を含むあらゆる書き込みオペレーションを接続の解除時に実行できます。
onDisconnect
プリミティブを使用して、接続の解除と同時にデータを書き込むシンプルな例を次に示します。
ウェブ モジュラー API
import { getDatabase, ref, onDisconnect } from "firebase/database"; const db = getDatabase(); const presenceRef = ref(db, "disconnectmessage"); // Write a string when this client loses connection onDisconnect(presenceRef).set("I disconnected!");
名前空間が指定されたウェブ API
var presenceRef = firebase.database().ref("disconnectmessage"); // Write a string when this client loses connection presenceRef.onDisconnect().set("I disconnected!");
onDisconnect の仕組み
onDisconnect()
オペレーションを確立すると、そのオペレーションは Firebase Realtime Database サーバーで継続的に実行されます。サーバーがセキュリティをチェックして、リクエストされた書き込みイベントを実行する権限がユーザーにあることを確認し、書き込みイベントが無効な場合はアプリに通知します。その後、サーバーが接続をモニタリングします。接続がタイムアウトした場合、または Realtime Database クライアントによって接続が能動的に閉じられた場合、サーバーはセキュリティをもう一度チェックして(オペレーションがまだ有効であることを確認して)からイベントを呼び出します。
書き込みオペレーションに対するコールバックを使用すれば、onDisconnect
が正しくアタッチされたことをアプリから確認することができます。
ウェブ モジュラー API
onDisconnect(presenceRef).remove().catch((err) => { if (err) { console.error("could not establish onDisconnect event", err); } });
名前空間が指定されたウェブ API
presenceRef.onDisconnect().remove((err) => { if (err) { console.error("could not establish onDisconnect event", err); } });
.cancel()
を呼び出して onDisconnect
イベントをキャンセルすることもできます。
ウェブ モジュラー API
const onDisconnectRef = onDisconnect(presenceRef); onDisconnectRef.set("I disconnected"); // some time later when we change our minds onDisconnectRef.cancel();
名前空間が指定されたウェブ API
var onDisconnectRef = presenceRef.onDisconnect(); onDisconnectRef.set("I disconnected"); // some time later when we change our minds onDisconnectRef.cancel();
接続状態の検出
プレゼンス関連の多くの機能において、アプリが現在オンラインであるかオフラインであるかがわかると便利です。Firebase Realtime Database には、クライアントの接続状態が変わるたびに更新される特別なロケーション(/.info/connected
)が用意されています。次に例を示します。
ウェブ モジュラー API
import { getDatabase, ref, onValue } from "firebase/database"; const db = getDatabase(); const connectedRef = ref(db, ".info/connected"); onValue(connectedRef, (snap) => { if (snap.val() === true) { console.log("connected"); } else { console.log("not connected"); } });
名前空間が指定されたウェブ API
var connectedRef = firebase.database().ref(".info/connected"); connectedRef.on("value", (snap) => { if (snap.val() === true) { console.log("connected"); } else { console.log("not connected"); } });
/.info/connected
はブール値です。この値はクライアントの状態に依存するため、異なる Realtime Database クライアント間では同期されません。言い換えると、あるクライアントで読み取った /.info/connected
の値が false であったとしても、別のクライアントで読み取った値も false であるとは限らないということです。
レイテンシ対応
サーバーのタイムスタンプ
Firebase Realtime Database サーバーには、サーバー上で生成されたタイムスタンプをデータとして挿入するメカニズムが用意されています。この機能を onDisconnect
と組み合わせることで、Realtime Database クライアントの接続が解除された日時を確実かつ簡単に記録できます。
ウェブ モジュラー API
import { getDatabase, ref, onDisconnect, serverTimestamp } from "firebase/database"; const db = getDatabase(); const userLastOnlineRef = ref(db, "users/joe/lastOnline"); onDisconnect(userLastOnlineRef).set(serverTimestamp());
名前空間が指定されたウェブ API
var userLastOnlineRef = firebase.database().ref("users/joe/lastOnline"); userLastOnlineRef.onDisconnect().set(firebase.database.ServerValue.TIMESTAMP);
クロックのずれ
firebase.database.ServerValue.TIMESTAMP
は精度が高く、ほとんどの読み取りと書き込みのオペレーションに適していますが、一方で Firebase Realtime Database サーバーとクライアントの相対的なクロックのずれを推定することが有用な場合があります。ロケーション /.info/serverTimeOffset
にコールバックをアタッチしてミリ秒単位の値を取得し、この値をローカルのレポート時刻(ミリ秒単位のエポックタイム)に加算すればサーバー時刻を推定することができます。このオフセットの精度は、ネットワークのレイテンシによる影響を受ける可能性があるため、主に、クロック時刻の大きな(1 秒を超える)不一致の検出に役立ちます。
ウェブ モジュラー API
import { getDatabase, ref, onValue } from "firebase/database"; const db = getDatabase(); const offsetRef = ref(db, ".info/serverTimeOffset"); onValue(offsetRef, (snap) => { const offset = snap.val(); const estimatedServerTimeMs = new Date().getTime() + offset; });
名前空間が指定されたウェブ API
var offsetRef = firebase.database().ref(".info/serverTimeOffset"); offsetRef.on("value", (snap) => { var offset = snap.val(); var estimatedServerTimeMs = new Date().getTime() + offset; });
プレゼンスのサンプルアプリ
接続解除オペレーションを接続状態のモニタリングやサーバーのタイムスタンプと組み合わせることで、ユーザー プレゼンス システムを構築することができます。ユーザー プレゼンス システムでは、Realtime Database クライアントがオンラインであるかどうかを示すデータを、各ユーザーがデータベース上の特定のロケーションに保存します。クライアントは、オンラインになったときにこのロケーションを true に設定し、接続を解除したときにタイムスタンプを設定します。このタイムスタンプは、特定のユーザーがオンラインであった最後の時刻を示します。
両方のコマンドがサーバーに送信される前にクライアントのネットワーク接続が失われた場合に発生する競合状態を避けるため、ユーザーをオンラインとしてマークする前に接続解除オペレーションをキューに入れることをおすすめします。
次に、シンプルなユーザー プレゼンス システムを示します。
ウェブ モジュラー API
import { getDatabase, ref, onValue, push, onDisconnect, set, serverTimestamp } from "firebase/database"; // Since I can connect from multiple devices or browser tabs, we store each connection instance separately // any time that connectionsRef's value is null (i.e. has no children) I am offline const db = getDatabase(); const myConnectionsRef = ref(db, 'users/joe/connections'); // stores the timestamp of my last disconnect (the last time I was seen online) const lastOnlineRef = ref(db, 'users/joe/lastOnline'); const connectedRef = ref(db, '.info/connected'); onValue(connectedRef, (snap) => { if (snap.val() === true) { // We're connected (or reconnected)! Do anything here that should happen only if online (or on reconnect) const con = push(myConnectionsRef); // When I disconnect, remove this device onDisconnect(con).remove(); // Add this device to my connections list // this value could contain info about the device or a timestamp too set(con, true); // When I disconnect, update the last time I was seen online onDisconnect(lastOnlineRef).set(serverTimestamp()); } });
名前空間が指定されたウェブ API
// Since I can connect from multiple devices or browser tabs, we store each connection instance separately // any time that connectionsRef's value is null (i.e. has no children) I am offline var myConnectionsRef = firebase.database().ref('users/joe/connections'); // stores the timestamp of my last disconnect (the last time I was seen online) var lastOnlineRef = firebase.database().ref('users/joe/lastOnline'); var connectedRef = firebase.database().ref('.info/connected'); connectedRef.on('value', (snap) => { if (snap.val() === true) { // We're connected (or reconnected)! Do anything here that should happen only if online (or on reconnect) var con = myConnectionsRef.push(); // When I disconnect, remove this device con.onDisconnect().remove(); // Add this device to my connections list // this value could contain info about the device or a timestamp too con.set(true); // When I disconnect, update the last time I was seen online lastOnlineRef.onDisconnect().set(firebase.database.ServerValue.TIMESTAMP); } });