clientID vs clientGroupID
plasma には 2 つの identity 概念があります。名前は似ていますが、 同じものではありません。これらを混同することは、sync のバグを 書く最速の方法の 1 つです。
clientID — タブごとに 1 つ
Section titled “clientID — タブごとに 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 は変わりません。
次に読むもの
Section titled “次に読むもの”- Push, pull, rebase — identity の配管がどう sync ループに供給されるか
- Auth and permissions —
server 側の
auth()ハンドラを書く