はじめに
難しいことではありませんでしたが、プラグアンドプレイとは言えません。主な難点はサンプルやチュートリアルが不足していることで、基礎部分を正しく動かすのに数週間かかりました。Powersync + Supabase や Firebase を使う場合と比べると、間違いなくセットアップが必要です。
実装
Appwrite + RxDB を実現する方法について、git のサンプル付きの公式ブログ記事がありました。しかし、試してみるとややバグが多く、リアルタイム同期もできませんでした。そこで、Appwrite と RxDB の両方のドキュメントを読み、ゼロから自分で構築することにしました。
主な障害
RxDB と状態管理の扱い
私は LegendState を使っていますが、rxdb コレクションのインスタンスを .set() しようとするとビューでループが発生することがあります。そのため、rxdb コレクションには .get() を使い、更新の読み取りと監視には LegendState を使うようにしています。一方、viewModel には rxdb を直接操作する関数をすべて置いています。Appwrite レプリカの変更は viewModel の関数内で行われます。
リアルタイム同期によるビューのループ更新
前の点と関連しています。LegendState には詳しくなかったのですが、今はだいぶ慣れてきました。この問題は debounce を追加して解決しました。
同期の遅延がローカル状態を乱す
RxDB の Appwrite レプリケーションは一度に 1 つの書き込みしか実行できないため、短時間に複数の操作を行うと、ローカル状態に競合の問題が発生します。リアルタイムでコレクションを購読しているのに、ローカルで変更・受信したデータが同期されていないことが原因です。そこで、競合を処理するために updatedAt を作成しました。
削除されたフィールド
レプリケーション用に deleted フィールドを設定できます。削除は .remove() を使って行うよう指示されています。しかし、この削除メソッドでは「削除の取り消し」ができません。RxDB は .remove() メソッドでソフト削除されたものを隠してしまうため、クライアントは !deleted をチェックするだけではその項目にアクセスできないからです。削除されたファイルを取得するためだけの get/findone メソッドがあり、やや面倒ですが、rxdb に独自のソフト削除の仕組みが必要なのは理解できます。そこで、自分用に追加の deleted フィールドを設けました。つまり、合計で 3 つの削除フィールドがあります - rxDeleted、isDeleted、deleted(Appwrite のデフォルト)です。
ユーザーが項目を削除したときは isDeleted を切り替えるだけにして、復元できるようにゴミ箱に入るようにしています。クライアント側では、isDeleted を 30 日間チェックし、rxdb のソフト削除を実行して rxDeleted を切り替えます。そしてサーバー側では、rxDeleted をさらに 30 日間チェックし、実際に削除します。
rxDeleted によるソフト削除は、RxDB がデバイス間で deleted フィールドを同期するために必要です。しかし、ユーザーが簡単に項目を復元できるようにもしたいので、その上に isDeleted を追加しました。
まとめ
私にとっては簡単ではなく、これらすべてを解決するのに数週間かかりました。もともとは SwiftUI から始め、Apple の CoreData が同期の手順をすべて処理してくれていました。同期のためだけにこれほど多くのことをする必要があったのは初めてです。しかし、やってよかったと思っています。
さあ、次の大きなことに取りかかれます!