JavaScript でのオフライン機能の有効化

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