✅Websocketの巻き戻り対応
Websocketの巻き戻り対応
はじめに(まず“ものさし”を1本だけ)
「わからない」の一番の原因は、登場人物が多いのに“ものさし”が定まっていないことです。ここでは、すべてを1本の数字でものさし化します。
ものさし = version(サーバで確定した順番を示す連番)
サーバは変更を1つ適用するたびに version を +1 します。
サーバから届く通知は必ずこの数字を持ちます:
items_updated(appliedVersion=V) … 変更Vが確定して全員に配られた合図
mutation_ack(appliedVersion=V) … あなたのコマンドがVとして採用された合図
この“1本の数字”に揃えるだけで、到着順が前後しても整合がとれます。
3つの箱だけ覚えればOK(クライアント側の頭の中)
base(ベース)
サーバが「確定」済みとして教えてくれた最新の状態。baseVersion という数字を一緒に持ちます。
overlay(オーバーレイ)
あなたが送ったが、まだサーバ確定になっていない“仮”変更のメモ。UIは base の上にこれを重ねて“いまの見た目”を作ります。
ackBuffer(ACKバッファ)
ACK(appliedVersion=V) が先に来たけど、baseVersion がまだVに追いついていない時に、ACKを一時的にしまっておく場所。
たったこれだけです。
まずは直感のたとえ話(ホワイトボード+付箋)
サーバ = 教室のホワイトボード担当。書くたびに右上の通し番号(version)を+1して全員に「今はV番だよ」と伝えます。
あなたの端末 = 自分の机のノート。
ノートの本文が base(サーバが確定で教えてくれた内容)。
まだ先生に確認されていないあなたの提案メモが overlay(付箋)。
items_updated(V) = 先生がホワイトボードにV番を書いたよ、という全体アナウンス。
mutation_ack(V) = あなたの提案がV番として採用されたよ、という個別メッセージ。
付箋(overlay)は「先生のボードにまだ書かれていない自分の提案だけ」をノートに仮貼りします。先生のボード(base)がV番まで進んだら、その中に自分の提案が含まれている分の付箋は外せます(確定したのでもう“仮”じゃない)。
この例えいらないな基素.icon
ルールは4つだけ
version は絶対に巻き戻さない(items_updated.appliedVersion <= baseVersion なら無視)
ACK は “数字で” 判定(ack.appliedVersion <= baseVersion なら、該当 pending を外せる)
ACK が先で base がまだ追いついてない(ack.appliedVersion > baseVersion)なら ackBuffer に一時保存
overlay は「baseに未反映の自分の変更だけ」を重ねる(重複加算しない)
いちばん混乱しやすいケースを、ゆっくり順番に
前提:
最初 baseVersion = 10
あなたは連打で2回操作(C1 と C2)。他の人が1回操作(O1)。
サーバの確定順は C1 → O1 → C2 なので、確定の世界の version は 11, 12, 13 と進みます。
採番はサーバーがやる基素.icon
実際にあなたが受け取る順番はバラバラで、ここでは次の順で来たとします:
items_updated(appliedVersion=12)(O1)
mutation_ack(appliedVersion=11, corr=C1)(C1のACK)
items_updated(appliedVersion=11)(遅れてきたC1の更新)
mutation_ack(appliedVersion=13, corr=C2)(C2のACK)
items_updated(appliedVersion=13)(C2の更新)
処理はこうします:
ステップ1: 更新12を受信
12 は baseVersion(10) より新しい → 採用。baseVersion = 12
ここでの注意点:「12の中にはC1の結果が入っている」可能性が高い(実際入ってます)。
なので overlay の再評価で「C1を二重に貼らない」。つまり「baseに未反映な分だけ」付箋を残す思想でOK。
baseに未反映な分だけをどこで判断する?基素.icon
C1はmutation_ack(appliedVersion=11, corr=C1)がきた時に初めてC1を反映するbaseVersionが11であることがわかるのだから、それまでは適用しないってことか
ステップ2: ACK(11) を受信
11 は baseVersion(12) 以下 → C1の付箋を外してよい(確定済みだから)。
ステップ3: 更新11が遅れて到着
11 は baseVersion(12) 以下 → 過去のもの。何もしない(巻き戻さない)。
ステップ4: ACK(13) を受信
13 は baseVersion(12) より先 → まだ base が追いついていないので ackBuffer に一時保存。C2の付箋はまだ残す(見た目は楽観表示)。
ステップ5: 更新13を受信
13 は新しい → 採用。baseVersion = 13
ackBuffer に 13 の ACK がある → C2の付箋を外す。overlayは空に。
結果:
最終 baseVersion = 13、overlayは空。
途中で古い更新(11)が後から来ても、数字比較で無視するので巻き戻りません。
先にACKが来た場合でも、ack.appliedVersion を数字で見て、base が追いついた時点で安全に付箋を外せます。