フロントでfirestoreのエラーを検知したい

とあるプロジェクトにて。
こんなかんじでfirestoreのエラー検知をしようとしているコードがありました。

```ts
try {
  connectFirestore();
} catch {
   window.alert('firebase接続失敗!');
}

try {
  watchFirestoreUpdate();
} catch {
   window.alert('firebase参照失敗!');
}

try {
  updateFirestore();
} catch {
   window.alert('firebase更新失敗!');
}
```

しかし、意図的にエラーを起こして動作確認したところ、、、
**検知できてませんでした。**

<br>
この記事は、firestoreのエラー検知をちゃんとやろうとした、格闘の記録です。
firestoreは使い慣れていないため、変なことをしていたらご指摘いただけるとありがたいです。

<br>
<br>
## 前提
* 言語はtsです
* フロントエンドです
* firebaseのバージョンは7です
* firestoreをつかって、別システムと連携をするページです
* 監視や更新がひっそりと失敗するのは、要件上クリティカルにまずいです

<br>
## アウトライン
やろうとしたことの一覧と、結果を記載します。

|やろうとしたこと|結果|何をした?|
|:---|:---|:---|
|初回接続エラーの検知|できた|setTimeout()で接続タイムアウトを検知|
|途中切断の検知|一応できたけど挙動が怪しいので導入見送り|setInterval()で定期的に接続状態をチェック|
|NW切断の検知|できた|window.addEventListener('offline')を設定|
|参照エラーの検知|できた|コールバックを使う|
|更新エラーの検知|できた|コールバックを使う|

<br>
## エラー検知、その前に
以降のコードでは、↓を使います。

```ts
import firebase from 'firebase';

// 本記事の各所で使う汎用モジュール
let singletonApp: firebase.app.App | null = null;
export const getApp = (): firebase.app.App => {
  if (singletonApp === null) {
    // 初回だけinitialize (2回やるとduplicateエラーになる)
    singletonApp = firebase.initializeApp(/*引数省略*/);
  }
  return singletonApp;
};
export const getRef = (path?: string): firebase.database.Reference => {
  return getApp().database().ref(path);
};
```

<br>
## 初回接続エラーの検知
ページ読み込みの直後に行っている、認証や、firebase.database.Reference取得の失敗を検知できるようにしました。
認証エラーは素直にPromiseで検知できたのですが、reference取得エラーの検知が厄介でした。
接続失敗したときに各種イベントが発火するわけもないため、タイムアウトで検知するようにしました。

```ts
// ロード時に実行する関数
//  => 認証エラーと接続タイムアウトでalertを出す
export const onLoad = (): void => {
  getApp()
      .auth()
      .signInWithEmailAndPassword('e-mail@gmail.com', 'password1234')
      .then(() => {
        console.log('firebase: authentication ok');
      })
      .catch((err) => {
        console.log(err);
        window.alert('firebase認証失敗')
      });
  alertOnConnectionTimeout();
};

const alertOnConnectionTimeout = (): void => {
  // TIMEOUT_SEC秒経ってもfirebase接続ができなければalertを出す
  const TIMEOUT_SEC = 15;
  checkConnectionTimeout(TIMEOUT_SEC)
      .then(() => {
        console.log('firebase: connection ok');
      })
      .catch((err) => {
        console.log(err);
        window.alert('firebase接続失敗');
      });
};

const checkConnectionTimeout = (timeoutSec: number): Promise<void> => {
  return new Promise((resolve, reject) => {
    getRef('.info/connected').on('value', (snapshot: any) => {
      // 接続したらここに来る
      if (snapshot.val() == false) {
        return;
      }
      resolve();
    });
    window.setTimeout(() => {
      // タイムアウトしたらここに来る
      reject(new Error('timeout'));
    }, timeoutSec * 1000);
  });
};
```

<br>
なお、以下はダメでした。

```ts
// ダメだったケース
export const onLoad = (): void => {
  checkConnectionOnce();
};
const checkConnectionOnce = (): void => {
  getRef('.info/connected').once('value', (snapshot: any) => {
    // 接続失敗なら、そもそもここに来ない
    if (snapshot.val() == false) {
      // 接続後、1回目のvalueイベントでここにくる (=正常系でアラートが出てしまう)
      window.alert('Firebaseの接続に失敗しました');
      return;
    }
    // 接続後、2回目のvalueイベントでここにくる (=onceだとここに到達しない)
    // => よって、↑ではonceではなくonで見ることにした
    console.log('firebase: connection ok');
  });
};
```

<br>
## 途中切断の検知
自分 <-(a)-> firebase <-(b)-> 他人　のうち、(a)の途中切断を検知しようとしました。
一応、以下のコードで検知できるとは思うのですが、

* devtoolで確認したところメモリリークしているっぽい挙動だった
* 次項のNW切断エラーを入れれば大半のケースで検知できるし、シンプルになる

という理由から、導入はやめました。
(NW切断の検知だけだと、NW正常でfirebase側に障害があった場合は検知できません。とはいえ参照・更新のタイミングでエラー検知できるため、大過ないだろうと考えました)

```ts
// ロード時に実行する関数
export const onLoad = (): void => {
  // INTERVAL_SEC秒ごとに、firebaseの接続状態をチェック
  const INTERVAL_SEC = 30;
  setInterval(
    checkConnectionTimeout, // 中身は前項を参照
    INTERVAL_SEC * 1000
  );
};
```

なお、[onDisconnect](https://firebase.google.com/docs/reference/js/firebase.database.Reference#ondisconnect)といういかにも今回のケースに使えそうな名前の関数がありましたが、これは使えませんでした。

```ts
// onDisconnect()を使うと、
// 切断時に、firebase側でfirestoreの値を更新してくれる。
// ただ、値を更新をするためのインターフェースしか用意されていないため、
// window.alert()を出す、みたいなことには使えない。
getRef('my/app/path/is_connected').onDisconnect().set(false);

/* 余談
 * onDisconnect()による値の更新には罠があります。
 * タブを閉じた場合は、即時値が反映されますが、
 * NW切断の場合は、3分程たたないと値が反映されません。
 * NW切断も含めてリアルタイムに値を反映したい場合は、何かしら別の手段が必要です。
 */
```

<br>
## NW切断の検知
firebaseではなく、ブラウザの機能をつかいました。超簡単。

```ts
window.addEventListener('offline', () => {
  window.alert('ネットワーク切断');
}, false);
```

<br>
## 参照エラーの検知
[Reference](https://firebase.google.com/docs/reference/js/firebase.database.Reference)を使う場合、promiseではなく、なつかしのコールバックを使ってエラーを検知します。この仕様に気づかずそこそこハマりました。
(改めて型定義を見返すまで気づけなかったです。。。盲点でした。)

```ts
const watchMyAppPath = () => {
  getRef('my/app/path').on('value', (snapshot) => {
    console.dir(snapshot.val());
  }, captureWatchError);
};

const captureWatchError = (err: any) => {
  if (err) {
    console.log(err);
    window.alert('firebase参照失敗');
  }
};
```

なお、試してませんが、以下を使えば普通にtry/catchできるかもしれません。

* [promisify](https://www.npmjs.com/package/promisify)を使ってpromise化する
* [ThenableReference](https://firebase.google.com/docs/reference/js/firebase.database.ThenableReference)を使ってみる   
  (名前から想像するに、thenやcatchを使えるようにしたReferenceと思われます。ドキュメントをみても使い方が分からなかったのと、変更の影響がでかそうだったため導入を見送りました)

<br>
## 更新エラーの検知
参照エラーと同じく、コールバックを使いました。

```ts
const updateMyAppPath = () => {
  getRef('my/app/path/boolean').set(true, captureWatchError);
};

const captureUpdateError = (err: any) => {
  if (err) {
    console.log(err);
    window.alert('firebase更新失敗');
  }
};
```

<br>
## まとめ
* 異常系もちゃんと動作確認をしよう
* .info/connectedを見ればfirebaseの接続状態が分かる
* window.addEventListener('offline') でNW切断を検知できる
* onDisconnect()でfirebase切断時に値を更新できるが、NW切断だとラグがあるので注意
* firestoreの参照、更新エラーにはコールバック関数を使う

<br>
以上です。