0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

壊れたcurrentをbackupから復旧する――Recoveryの実装フロー【第36回】

0
Last updated at Posted at 2026-09-15

4Hえんぴつ|React+TypeScriptでWeb RPGを作る
第7章「テストと品質管理」
第36回:壊れたcurrentをbackupから復旧する――Recoveryの実装フロー

連載トップ・第0回はこちら

1. 今回のテーマ

前回は、

current
↓
通常利用する現在のセーブ

backup
↓
復旧候補

quarantine
↓
壊れたデータの隔離先

という3つの役割を整理しました。

今回は、

その役割を使って、

currentが壊れている
↓
backupは正常

という状態から、

どの順番で復旧するか

を考えます。

Recoveryで大切なのは、

単に、

backup
↓
currentへコピー

することではありません。

壊れたデータを失わず、正常なbackupだけを使い、復旧後のcurrentも確認する

ところまでを一連の処理として考えます。


2. Recoveryはロード処理の途中で始まる

通常のロードは、

current取得
↓
validation
↓
valid
↓
GameStateをロード

です。

しかし、

validationの結果が、

invalid

だった場合、

通常ロードには進みません。

ここから、

Recovery処理へ分岐します。

current取得
↓
validation
↓
invalid
↓
Recovery

です。


3. currentが壊れているからといって、すぐ消さない

最初に避けたいのは、

current
↓
invalid
↓
削除

です。

壊れたcurrentには、

まだ調査に使える情報が残っている可能性があります。

例えば、

appVersion
saveSchemaVersion
slotId
savedAt
壊れる直前までのGameState

などです。

そのため、

Recoveryの最初の考え方は、

消すのではなく、守る

です。


4. 最初に破損currentをquarantineへ退避する

Recoveryでは、

まず、

invalid current
↓
quarantine

へ退避します。

これにより、

通常ロード対象から外しながら、

元データを残せます。

slot_1

current
↓
破損

quarantine
↓
破損currentを保持

という状態です。


5. なぜbackup復旧より先にquarantineするのか

例えば、

先に、

backup
↓
currentへ上書き

したとします。

すると、

元の壊れたcurrentが失われる可能性があります。

そのため、

考え方としては、

壊れたcurrentを保存
↓
その後でbackupから復旧

の順にします。

Recoveryの途中でも、

元データを失わない

ことを優先します。


6. quarantineへ入れれば復旧成功ではない

ここで注意します。

current
↓
quarantine

できたからといって、

ゲームを再開できるわけではありません。

quarantineは、

破損データを通常利用から外した

だけです。

次に、

正常な復旧元があるかを確認します。


7. backupを探す

次は、

同じスロットのbackupを確認します。

slot_1
├ current → invalid
├ backup  → ?
└ quarantine

backupがなければ、

自動復旧はできません。

backupが存在する場合も、

まだ復旧できるとは限りません。


8. backupも無条件には信用しない

例えば、

backupあり

だけで、

そのデータをcurrentへ戻すのは危険です。

backupも、

壊れている可能性があります。

そこで、

backup取得
↓
validation
↓
valid?

とします。


9. backupにも第34回と同じvalidationを使う

backupだからといって、

別の特別な検証方法を用意する必要はありません。

基本的には、

currentへ使った、

parse
schema判定
validation

を利用できます。

つまり、

current
↓
同じvalidation

backup
↓
同じvalidation

です。

保存場所ではなく、

SaveDataとして成立しているか

を確認します。


10. backupがfuture schemaなら復旧に使わない

例えば、

backupが、

saveSchemaVersion = 99

だったとします。

これは、

壊れているとは限りません。

しかし、

現在のアプリでは、

unsupported

です。

そのため、

current
↓
invalid

backup
↓
unsupported

なら、

通常Recoveryには使いません。


11. backupがold schemaならMigration可能性を考える

一方、

backupが、

old / migratable

だった場合があります。

この場合、

backup
↓
旧schemaとしてvalidation
↓
Migration
↓
現在schema
↓
validation

を経て、

正常な現在形式にできるなら、

復旧候補として扱えます。


12. RecoveryとMigrationを混ぜすぎない

ただし、

責務としては分けて考えます。

Recovery
↓
正常な復旧元を探す

と、

Migration
↓
旧schemaを現在schemaへ変換する

です。

Recovery処理が、

Migrationの細かい変換内容まで持つ必要はありません。


13. Recovery側は「使えるSaveData」を受け取る

考え方としては、

backup
↓
Compatibility Check
↓
必要ならMigration
↓
Current Schema Validation
↓
復旧可能なSaveData

までを別処理に任せます。

Recovery側は、

このbackupは安全に使える

という結果を受けて、

currentへ復元します。


14. Recovery結果を型で表す

Recoveryも、

成功・失敗だけではなく、

いくつかの状態があります。

以下は考え方を説明するため、実装を簡略化しています。

type RecoveryResult =
  | {
      status: "recovered";
      saveData: SaveData;
    }
  | {
      status: "no_backup";
    }
  | {
      status: "backup_unusable";
    }
  | {
      status: "error";
    };

これなら、

呼び出し側で、

recovered
no_backup
backup_unusable
error

を区別できます。


15. booleanだけでは理由が分からない

例えば、

以下は考え方を説明するため、実装を簡略化しています。

const success = await recoverSave();

だけだと、

falseだったときに、

backupがなかった

backupも壊れていた

書き込みに失敗した

のどれなのか分かりません。

Recoveryは複数の処理をまたぐため、

結果の意味を分けておくと扱いやすくなります。


16. 復旧可能なbackupが見つかったらcurrentへ戻す

backupが正常なら、

次に、

backup
↓
current

へ復元します。

ただし、

単純にオブジェクトをメモリ上へ返すだけではありません。

以後の通常ロードでも使えるよう、

保存基盤上のcurrentとして戻します。


17. currentを正式な状態へ戻す

例えば、

Recovery前が、

slot_1

current
↓
invalid

backup
↓
valid

なら、

Recovery後は、

slot_1

current
↓
backupから復旧した正常データ

quarantine
↓
元のinvalid current

という状態になります。


18. backupをそのまま残すかは設計次第

復旧後、

backupをどう扱うかは、

保存世代の設計によって変わります。

例えば、

backupをそのまま保持する

方法もありますし、

新しい正常currentを基準に、

次回保存時にbackupを更新する方法もあります。

重要なのは、

Recoveryの途中で、

復旧元まで不用意に失わないことです。


19. currentへ書けたら終わりではない

ここが重要です。

backup
↓
currentへ書き込み成功

しただけでは、

Recovery成功と断定しません。

書き込んだcurrentが、

本当に正常に読み戻せるかを確認します。


20. 復旧後のcurrentを再validationする

流れとしては、

backup
↓
currentへ保存
↓
currentを再取得
↓
validation
↓
valid

です。

これで初めて、

正常なcurrentを再構築できた

と判断できます。


21. 書き込み成功とデータ正常は別

例えば、

Storage APIとしては、

write success

でも、

アプリが期待する構造になっていない可能性はあります。

そのため、

書けた
=
復旧成功

ではなく、

書けた
+
正常に読めた
=
復旧成功

と考えます。


22. 復旧後にGameStateをロードする

currentが正常だと確認できたら、

通常ロードへ戻ります。

Recovered current
↓
SaveData
↓
GameState
↓
Application

です。

Recovery専用のゲーム状態を作る必要はありません。

最終的には、

通常のGameStateへ戻ります。


23. UI側から見ると通常ロードに近づける

理想的には、

プレイヤーから見ると、

つづきから
↓
ロード

という操作の裏側で、

必要ならRecoveryが動きます。

つまり、

通常ならcurrentをロード

壊れていたらbackupから復旧してロード

です。

復旧可能であれば、

できるだけ通常の再開へ戻します。


24. ただしRecoveryした事実は内部で分かるようにする

プレイヤー操作を複雑にしなくても、

内部では、

通常ロードだった

backupからRecoveryした

を区別できる方が便利です。

例えば、

ログやテストで、

recovered_from_backup

と確認できます。


25. Recoveryで元の破損データを上書きしない

Recovery時に避けたいのは、

invalid current
↓
その場でbackupに置換
↓
元データ消失

です。

そのため、

invalid current
↓
quarantine
↓
backupからcurrent復旧

という順番に意味があります。


26. 処理順序をまとめる

基本フローを整理すると、

current取得
↓
validation
↓
invalid
↓
currentをquarantine
↓
backup取得
↓
backup validation
↓
valid
↓
backupをcurrentへ復元
↓
current再取得
↓
current validation
↓
valid
↓
GameStateをロード

となります。


27. Recovery途中で失敗したらどうするか

例えば、

quarantine成功
↓
backupも正常
↓
currentへの書き込み失敗

ということも考えられます。

このとき、

たぶん書けただろう

としてゲームを続けてはいけません。

Recovery自体が完了していないからです。


28. 中途半端なRecoveryを成功扱いしない

考え方としては、

全工程成功
↓
recovered

です。

途中で失敗したら、

error

として、

通常ロードには進みません。

ここでもfail-closed寄りに扱います。


29. quarantineに残っていることが役立つ

Recovery途中で失敗しても、

最初に元currentをquarantineへ保存できていれば、

元データが残ります。

つまり、

Recovery失敗
↓
元データも消失

という最悪の状態を避けやすくなります。


30. backupがない場合は自動Recoveryしない

次に、

current
↓
invalid

backup
↓
なし

の場合です。

このケースでは、

復旧元がありません。

そのため、

壊れたcurrentを推測で修復するのではなく、

自動Recoveryを止めます。


31. currentはquarantineへ残す

backupがなくても、

invalid current
↓
quarantine

は行えます。

その結果、

通常currentがなくなれば、

スロットを空きとして再利用できます。


32. 復旧できないことと、スロットを失うことは別

例えば、

slot_2
↓
current破損
↓
backupなし

でも、

破損currentをquarantine
↓
slot_2 currentなし
↓
新しいセーブを作成可能

とできます。

一つの事故で、

そのスロットを永久に失わせないようにします。


33. backupも壊れている場合

例えば、

current
↓
invalid

backup
↓
invalid

の場合です。

このとき、

backupを復旧元には使いません。

正常ではないデータから、

正常なcurrentを作れる保証がないためです。


34. backupの破損も別途保持を考えられる

設計によっては、

壊れたbackupについても、

調査用として隔離・記録することが考えられます。

ただし、

重要なのは、

backupという名前だから安全

と思わないことです。

backupもSaveDataの一つとしてvalidationします。


35. Recoveryをslot単位に閉じ込める

例えば、

slot_1
↓
valid

slot_2
↓
Recovery中

slot_3
↓
valid

の場合、

slot_1とslot_3は、

通常利用できるようにします。

一つのRecovery失敗を、

セーブシステム全体へ広げません。


36. 他スロットを書き換えない

Recovery処理には、

対象スロットを明確に渡します。

以下は考え方を説明するため、実装を簡略化しています。

async function recoverSlot(
  slotId: SaveSlotId
): Promise<RecoveryResult> {
  // 指定されたslotだけを対象にする
}

これにより、

slot_2の復旧

が、

slot_1
slot_3

へ影響しないようにします。


37. Recoveryは冪等性も意識したい

Recovery処理の途中で、

ブラウザを閉じたり、

再実行されたりする可能性があります。

そのため、

同じRecoveryをもう一度実行したら
さらに壊れる

ような設計は避けたいところです。


38. すでにRecovery済みなら通常ロードへ進む

例えば、

1回目で正常に復旧できていれば、

次回起動時には、

current
↓
valid

です。

そのため、

Recoveryには入りません。

valid current
↓
通常ロード

となります。


39. quarantineを再びcurrentへ戻さない

また、

一度quarantineした破損データを、

次回起動時に、

以前のセーブがある

として自動的にcurrentへ戻してはいけません。

quarantineは、

通常利用対象から外した状態です。


40. 保存場所がIndexedDBでも考え方は同じ

現在の正式公開版では、

永続化基盤は、

IndexedDB
+
Dexie.js

です。

しかし、

Recoveryの中心にある考え方は、

Storage APIそのものではありません。

current
backup
quarantine
validation
Recovery

という役割です。


41. Repositoryの責務としてまとめやすい

例えば、

ゲームUIから、

IndexedDBのテーブル操作を直接行うのではなく、

Save Service
↓
Save Repository
↓
IndexedDB / Dexie.js

という境界を使います。

Recovery処理も、

保存層へ寄せることで、

UIからストレージ固有の処理を分離できます。


42. Recoveryを単体テストする

まずは、

関数・Repositoryレベルで、

例えば、

current invalid
backup valid

を用意します。

そして、

currentが正常データになる

元currentがquarantineへ残る

他slotは変化しない

ことを確認します。


43. backupなしケースもテストする

次に、

current invalid
backupなし

を用意します。

期待する結果は、

currentを通常利用しない
quarantineへ残る
slotを再利用できる
他slotは影響を受けない

です。


44. future schemaとの取り違えもテストする

例えば、

current
↓
Schema 99

なら、

Recoveryを始めてはいけません。

これは、

invalid

ではなく、

unsupported

だからです。

つまり、

future schema
↓
quarantine
↓
backup復旧

には進みません。


45. Recoveryのテストは結果だけでは足りない

例えば、

画面上で、

ロードできた

だけ確認すると、

内部で、

壊れたcurrentを削除しているかもしれません。

そのため、

テストでは、

ロード成功
+
current正常
+
quarantine存在
+
他slot不変

まで見る価値があります。


46. E2Eではプレイヤー操作として確認する

このRecoveryは、

PlaywrightによるE2Eでも確認している重要なシナリオです。

note #21でも、主要E2Eの一つとして、

current破損
+
backup正常
↓
復旧
+
quarantine

を確認しています。([note][1])


47. なぜE2Eまで必要なのか

Recovery処理単体が正しくても、

セーブ一覧画面

ロードボタン

起動時復元

IndexedDB

React状態管理

をつないだときに、

別の問題が出る可能性があります。

E2Eでは、

それらを含めて、

実際に続きを遊べるか

を確認できます。


48. 正式公開版ではRecoveryまで受入済み

現在の正式公開版では、

破損セーブ検出・backup・quarantine・復旧がPhase Gの正式完了項目になっています。

公開品質として、

単にセーブできるだけでなく、

壊れたときにどう戻るか

までを受入対象にしています。


49. 今回のポイント

今回のポイントは3つです。

  • 破損currentをすぐ上書きせず、先にquarantineへ退避して元データを残す
  • backupもvalidationし、安全だと確認できた場合だけ復旧元として使う
  • backupからcurrentへ戻したあとも再validationし、正常なcurrentになったことを確認してからロードする

整理すると、

current
↓
invalid
↓
quarantine

backup
↓
validation
↓
valid
↓
currentへ復旧
↓
再validation
↓
valid
↓
GameStateをロード

です。

Recoveryで大切なのは、

backupをコピーすることではなく、安全な状態へ戻れたことを確認すること

です。


50. 次回

次回は、

セーブ処理をVitestでどこまでテストするか【第37回】

です。

ここまで、

validation
current
backup
quarantine
Migration
future schema
Recovery

と、

セーブ処理がかなり増えてきました。

これらを、

更新のたびにすべて手作業で確認するのは現実的ではありません。

次回はまず、

どこまでをVitestで確認するか

を整理します。

そのうえで、

第38回では、

PlaywrightによるゲームE2E

へ進みます。


この記事を最後まで読んでいただき、ありがとうございます。

いいね」を押していただけるとうれしいです。これからの記事づくりの励みになります。

もし気に入っていただけましたら、フォローもよろしくお願いします。


前の記事

第35回 current/backup/quarantineでセーブを守る

次の記事

第37回 セーブ処理をVitestでどこまでテストするか

連載トップ

第0回 React+TypeScriptで異世界RPGを作る――連載の目的と開発ロードマップ

note関連記事

壊れたセーブデータから、どう復旧するか #20

noteでは、破損currentを検出し、正常なbackupがあれば復旧し、元の破損データはquarantineへ隔離する考え方を、制作側の視点から書いています。([note][2])

note「壊れたセーブデータから、どう復旧するか #20」を読む

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?