Push, Pull, Rebase
plasma.start() は、繰り返しのスケジュールで 3 つのフェーズを
実行する sync ループを開きます。各フェーズは Worker への HTTP
呼び出しと、いくらかの IDB 作業です。このページは各フェーズを
順にたどるので、スタックトレースや onError のペイロードが魔法に
感じられなくなります。
Phase 1 — Push
Section titled “Phase 1 — Push”トリガー: 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 は次を行います。
auth()を実行し、{ ok: false }を返したら 401 で拒否 します。- 各 mutation について、(
clientGroupID,clientID, id) を_plasma_client_mutationsに対して重複チェックします。処理 済みのエントリはスキップされます。 - D1 トランザクションに対して mutator を canonical に実行します。
各
db.insert(...)/db.update(...)/db.delete(...)が ユーザーテーブルに書き込みます。 - migration 時にインストールされた plasma の AFTER-write トリガー
が、その書き込みを単調増加の
row_versionとともに_plasma_changesへコピーします。 - 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 な効果は消えます。
これが SyncClientError に mutation-error という kind が存在
しない理由です — client が直接見るのは push の HTTP 失敗
(push-http / network)だけです。mutator のスローは、次の
rebase で行が静かに元に戻る という形で表面化します。その巻き
戻しに備えた UX を設計してください。呼び出し側での事前チェックは、
たいてい事後の toast に勝ります。
Phase 2 — Pull
Section titled “Phase 2 — Pull”トリガー: ポーリングタイマー(デフォルト 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 は次を行います。
- cookie をパースし、「since」ウォーターマークを計算します。
row_version> ウォーターマークの行を_plasma_changesからSELECTし、各行のauth.read(ctx, row)でフィルタします。- 変更リスト + 新しい 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 を続けます。
Phase 3 — Rebase
Section titled “Phase 3 — Rebase”pull の後、ブラウザは次を保持しています。
- base ストア — 新しい patch で更新する必要がある
- outbox — 確認済みエントリを落とす必要がある
- ユーザーに見えるストア — (base + 生き残った outbox)を、 正しい順序で反映する必要がある
rebase は rebuildOptimistic を実行します。
applyPatchToBase(patch)— すべてのput/delを<table>_baseオブジェクトストアへ書き込みます。dropConfirmed(clientID, lastMutationIDs[clientID])— サーバー が確認した outbox エントリを取り除きます。- reactive hub を一時停止する。 live query の通知を保留し、 購読者が中間状態を見ないようにします。
- 各ユーザーテーブルについて:
- ユーザーに見えるストアをクリアします。
- base ストアからすべての行をそこへコピーします。
- 生き残った各 outbox エントリを再生します — 元の
mutate()呼び出しと同じ mutator、同じ args、同じ optimistic エンジンです。 - 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 で行が元に戻るのを監視してください。
次に読むもの
Section titled “次に読むもの”- Change log and cookies — 各フェーズを可能にするデータ構造
- Sync errors (Troubleshooting) —
エラーの
kindによるトリアージ - Testing — 実際の Worker なしで各フェーズを シミュレートするテストを書く