コンテンツにスキップ

clientID vs clientGroupID

plasma には 2 つの identity 概念があります。名前は似ていますが、 同じものではありません。これらを混同することは、sync のバグを 書く最速の方法の 1 つです。

plasma を開くすべてのブラウザタブは、新しい clientID を得ます。 初回利用時に生成される UUID で、sessionStorage に永続化され、 ページリロードは生き延びます — が、新しいタブ / 新しいウィンドウ は生き延びません。

clientID は sync ループが次の目的で使います。

  • outbox を分割する。 すべての outbox エントリは複合キー [clientID, mutationID] のもとに存在します。同じユーザーの 2 つ のタブは、互いを踏むことなく自分の outbox スライスを push します。
  • サーバー側で重複排除する。 サーバーは (clientGroupID, clientID) タプルごとに last_mutation_id を 追跡します。同じ (clientGroupID, clientID, mutationID) を持つ 再送された mutation は no-op になります。
  • pull カーソルを追跡する。 pull レスポンスは lastMutationIDs: Record<clientID, mutationID> を運ぶので、各 タブは自分の outbox エントリのうちどれをサーバーがすでに処理 済みか分かります。

sessionStorage は設計上タブごとのストアです。新しいタブを開くと 新しい clientID が発行され、新しい outbox から始まります。これは 機能です — 同じユーザーの 2 つのタブが同時に編集していると、それ らは 2 つの別個の client として扱われ、その並行書き込みは通常の sync 経路を通じて調停されます。

clientGroupID — インストールごとに 1 つ

Section titled “clientGroupID — インストールごとに 1 つ”

すべての PlasmaClient は、あなたが供給する clientGroupID を 与えて構築されます。

createPlasmaClient({
...,
clientGroupID: currentUser.id,
})

意味論的には、1 つの clientGroupID = 1 人のユーザーにとっての アプリの 1 つの論理的インストール です。そのユーザーが開く すべてのタブは同じ clientGroupID を使います。clientID はタブ を区別し、clientGroupID はそれらをグループ化します。

server 側では、clientGroupID は次のものです。

  • auth.read / auth.write のスコープを決める。 auth() ハンドラが返す ctx は (clientGroupID, clientID) ごとに保存されます。そのため、実効ユーザーを変えるセッション途中 のログアウトは認可を再実行させます。
  • pull で送られる change_log の範囲を区切る。 client は、その clientGroupID について auth.read が許可した行だけを見ます。
  • causal cookie を固定する。 cookie は「この clientGroupID が どのサーバー書き込みを見たか」を追跡します。同じグループに加わる 新しいタブは、IDB 経由で現在の cookie を継承します。

両者が分岐するとき — 具体的な効果

Section titled “両者が分岐するとき — 具体的な効果”

同じユーザーの 2 つのタブ、どちらもサインイン済み。

Tab A: clientGroupID: "user-42" clientID: "aaaa..."
Tab B: clientGroupID: "user-42" clientID: "bbbb..."

Tab A が mutate("markDone", { id: "t1" }) を呼びます。Tab B が まったく同じ瞬間に mutate("editTitle", { id: "t1", title: "new" }) を呼びます。

  • 両方のタブが自分の mutation を push します。outbox は clientID で分割されているので、2 つの outbox は衝突しません。
  • サーバーは (user-42, aaaa, 12)(user-42, bbbb, 33) を見ます — 2 つの別個の mutation で、どちらも canonical に適用されます。
  • pull レスポンスは Tab A に「aaaa の mutation #12 は確認済み」と 伝えます。Tab A の outbox はそのエントリを落とします。
  • 両方のタブが結果のサーバー状態を pull し、それを基準に自分の optimistic ビューを rebase します。

もし 2 つの mutation が可換でない形で同じ行に触れた場合(両方が title を編集)、サーバーのデフォルトの調停(row_version 順に よる last-write wins)が勝者を選びます。テーブルに resolveConflict を宣言するか、CRDT カラムを使って可逆でない マージを保証してください。 Conflict resolution を参照して ください。

resetLocalState()clientID をローテートする

Section titled “resetLocalState() は clientID をローテートする”

client.resetLocalState() はローカルの IDB を消去します — base ストア、user ストア、outbox、cookie、すべてです — かつ clientID をローテートします。これは意図的です。ローテートしない と、古い clientID に対するサーバーの last_mutation_id の ウォーターマークがまだ N のままなので、reset の後に client が 送る最初の mutation(mutationID: 1)が重複として静かに落とされて しまうからです。

clientGroupID は変わりません。