オフライン対応は「失敗したら無限に再試行する」ことではない。電車内で編集した作業票はアプリを再起動しても残り、通信回復後に二重登録されず、別端末の変更とは明示的に競合として扱われる必要がある。
ローカルの記録を画面の基準にする
画面はローカル DB から読む。一つのトランザクションで下書きと保留中の操作を保存する。操作には ID、対象 ID、基準版、変更内容、作成時刻、状態を持たせる。操作 ID をサーバーにも冪等キーとして渡し、タイムアウト後の再試行で重複作成しないようにする。
対象ごとに順番に同期する。ネットワーク障害なら待機して再試行し、認証失効ならキューを止め、恒久的な拒否なら下書きを残して理由を示す。一件の不正データで無関係な操作まで塞がない。
編集 → ローカル取引で下書きと保留操作を保存 → 同期待ちを表示
通信回復 → 対象ごとに順番に送信 → サーバー版を確認 → 同期済みにする
版の競合 → 差分を提示し、再送信の操作を用意する
項目ごとに競合を解決する
追記型のメモと金額・承認結果では統合ルールが違う。後者を安易に最終書き込み優先にしない。サーバーが確定した内容でローカルのスナップショットを更新する。バックグラウンド処理は継続を保証されないため、画面復帰と手動同期でもキューを進める。
プロセス終了、重複応答、二台同時編集、権限失効を検証する。参考:Flutter オフライン優先設計。
作業指示を編集から確定まで追う
まずローカルトランザクションを決める。保存時には作業指示の下書きと pending_operation を同時に記録する。操作には安定した operationId、対象 ID、baseVersion、変更内容、作成時刻、状態が必要だ。下書きはすぐ表示し、「同期待ち」と示す。保存前に HTTP を送ると、サーバー処理後にプロセスが終了した場合、端末には送信の記録が残らない。
列車がトンネルに入り、サーバーは作成に成功したが応答が端末に届かなかったとする。再試行では同じ冪等キーを使う。サーバー側も一定期間、キーと結果を保持しなければならない。クライアントだけで UUID を作っても重複は防げない。確認を受けたらサーバー版、正式なスナップショット、操作状態を一つのローカルトランザクションで更新する。応答後、書き込み前に落ちても元のキーで同じ結果を取得できる。
local draft + operation(id=K, base=7)
→ POST /work-orders Idempotency-Key: K
→ timeout: keep pending, retry K
→ 200(version=8): atomically replace snapshot and acknowledge K
→ 409(current=9): stop this entity's queue and surface a conflict
失敗を分類する
タイムアウトと 5xx はバックオフして再試行する。401 は認証まで停止し、403 と入力検証エラーは利用者が対応できる失敗状態にする。409 ではローカル変更を残し、項目ごとの差を示す。別の対象は独立して進められるが、同じ対象の操作は因果順を守り、「名前変更」が「作成」より先に届かないようにする。未送信操作を取り消す前には、後続の操作が依存していないか確認する。
送信前、サーバー処理後かつ応答前、応答後かつローカル保存前の各時点でアプリを終了させる。さらに 2 台で版の競合を作る。画面だけでなく、同期バッジ、サーバーの件数、操作テーブルを確認する。下書きが見えるだけでは同期の安全性を証明できない。
