コンテンツにスキップ

Push, Pull, Rebase

plasma.start() は、繰り返しのスケジュールで 3 つのフェーズを 実行する sync ループを開きます。各フェーズは Worker への HTTP 呼び出しと、いくらかの IDB 作業です。このページは各フェーズを 順にたどるので、スタックトレースや onError のペイロードが魔法に 感じられなくなります。

トリガー: mutation が outbox に着地したばかり、または online ハンドラが発火した。

ブラウザは、(もしあれば)blob 依存がアップロード済みの outbox エントリをすべて集め、PushRequest に梱包して /sync/push へ POST します。

POST /sync/push
{
"protocolVersion": 1,
"schemaVersion": "todos-v1",
"clientGroupID": "user-42",
"clientID": "aaaa-bbbb",
"mutations": [
{ "id": 12, "name": "markDone", "args": { "id": "t1" }, "timestamp": 1730100000000 }
]
}

Worker は次を行います。

  1. auth() を実行し、{ ok: false } を返したら 401 で拒否 します。
  2. 各 mutation について、(clientGroupID, clientID, id) を _plasma_client_mutations に対して重複チェックします。処理 済みのエントリはスキップされます。
  3. D1 トランザクションに対して mutator を canonical に実行します。 各 db.insert(...) / db.update(...) / db.delete(...) が ユーザーテーブルに書き込みます。
  4. migration 時にインストールされた plasma の AFTER-write トリガー が、その書き込みを単調増加の row_version とともに _plasma_changes へコピーします。
  5. mutator がスローした場合、トランザクションはロールバックされ ますが、_plasma_client_mutations.last_mutation_id は依然として 進みます。そのため client の outbox は poison mutation を落とし ます。

client はサーバーが何をしたかをどう知るか

Section titled “client はサーバーが何をしたかをどう知るか”

push レスポンス自体は最小限です。

{ "ok": true }

個々の mutation の成否はインラインでは報告されません — plasma は push を fire-and-forget として扱います。サーバーは:

  • mutator 成功時: 書き込みは change log に着地し、次の pull が それらを持ち帰り、さらにこの client の lastMutationIDs エントリを進めます。
  • mutator スロー時: トランザクションはロールバックされますが、 _plasma_client_mutations.last_mutation_id は依然として進みます (poison mutation が永遠に再送されないように)。change log の行は 生成されません。client は次の pull で drop を知ります。 lastMutationIDs が「N まで見た」と言うのに、対応する行が patch に現れなかったときです — dropConfirmed が outbox エントリを 取り除き、rebuildOptimistic が生き残った outbox を再実行し、 失敗した mutation の optimistic な効果は消えます。

これが SyncClientErrormutation-error という kind が存在 しない理由です — client が直接見るのは push の HTTP 失敗 (push-http / network)だけです。mutator のスローは、次の rebase で行が静かに元に戻る という形で表面化します。その巻き 戻しに備えた UX を設計してください。呼び出し側での事前チェックは、 たいてい事後の toast に勝ります。

トリガー: ポーリングタイマー(デフォルト 5 秒)、WebSocket の poke、online イベント、または手動の client.pullOnce()

ブラウザは現在の pull cookie(c1: プレフィックス + リージョン ごとのカーソルの base64 JSON)を、/sync/pull?cookie=… クエリパラメータとして送ります。

GET /sync/pull?protocolVersion=1&schemaVersion=todos-v1
&clientGroupID=user-42&clientID=aaaa-bbbb
&cookie=c1:eyJ...

Worker は次を行います。

  1. cookie をパースし、「since」ウォーターマークを計算します。
  2. row_version > ウォーターマークの行を _plasma_changes から SELECT し、各行の auth.read(ctx, row) でフィルタします。
  3. 変更リスト + 新しい cookie + lastMutationIDs(clientID ごとの ウォーターマークで、ブラウザは自分の outbox エントリのどれを サーバーがすでに処理したか分かる)を返します。
{
"cookie": "c1:eyJhIjoiMTIzIn0=",
"patch": [
{ "kind": "put", "table": "todos", "key": "t1", "value": { "id": "t1", "done": 1 } }
],
"lastMutationIDs": { "aaaa-bbbb": 12 },
"hasMore": false
}

hasMore: true の場合、ブラウザは新しい cookie を運んで、 出し切るまでもう一度 pull を続けます。

pull の後、ブラウザは次を保持しています。

  • base ストア — 新しい patch で更新する必要がある
  • outbox — 確認済みエントリを落とす必要がある
  • ユーザーに見えるストア — (base + 生き残った outbox)を、 正しい順序で反映する必要がある

rebase は rebuildOptimistic を実行します。

  1. applyPatchToBase(patch) — すべての put / del<table>_base オブジェクトストアへ書き込みます。
  2. dropConfirmed(clientID, lastMutationIDs[clientID]) — サーバー が確認した outbox エントリを取り除きます。
  3. reactive hub を一時停止する。 live query の通知を保留し、 購読者が中間状態を見ないようにします。
  4. 各ユーザーテーブルについて:
    • ユーザーに見えるストアをクリアします。
    • base ストアからすべての行をそこへコピーします。
  5. 生き残った各 outbox エントリを再生します — 元の mutate() 呼び出しと同じ mutator、同じ args、同じ optimistic エンジンです。
  6. reactive hub を再開する。 rebase が触れたすべてのテーブルが ちょうど 1 回の通知を発火します。live query は自分の window を 再計算します。

いずれかのフェーズが失敗するとき

Section titled “いずれかのフェーズが失敗するとき”

すべてのフェーズのエラーは、あなたの onError ハンドラへ流れる 同じ SyncClientError union を通ります。完全な kind の一覧:

Kind 意味 回復
push-http すべての retry の後、push が非 2xx を返した plasma は retry し続けます。exhaust ごとに 1 イベントを受け取ります
pull-http すべての retry の後、pull が非 2xx を返した 同上
network fetch がスローした(オフライン、DNS、TLS) plasma はバックオフで retry し続けます
rebase-replay キューされた mutator の optimistic 再生が rebuildOptimistic の間にスローした 飲み込まれます。次の push がその mutation を運び、server 側の失敗が静かな巻き戻しを生みます
schema-mismatch サーバーが期待する schemaVersion とともに 409 を送った onSchemaMismatch({ phase }) → "reset" | "stay"
blob-upload-failed R2 の PUT の retry を使い切った client.retryBlobUpload(mutationID) または discardMutation(mutationID)

server 側の mutator のスロー(auth 拒否、バリデーション)は SyncClientError発火しません — 前のセクションを参照して ください。次の rebase で行が元に戻るのを監視してください。