「正常終了」とクラッシュを同じにしない:MV3拡張の復旧判定を2つの記録に分ける
Chrome 拡張でクラッシュ復旧を作るとき、「前回のセッションが残っているなら復旧画面を出す」という判定は簡単に見えます。しかし、通常のブラウザ終了でも直前のチェックポイントは残ります。保存データがあることと、前回の終了が異常だったことは別の事実です。
私は Chrome のセッション管理拡張 Tabwell を開発しています。Tabwell 1.1 の復旧世代では、この二つを別々の永続的な記録として扱いました。1.1.0 が複数ウィンドウの復旧バンドル、復旧ジャーナル、Gentle Restore を導入し、公開版 1.1.1 は外部テストで見つかった終了判定や復旧操作の正しさ・使いやすさを補強した版です。構成と日本語の推敲には生成 AI を使い、技術的な記述は公開版 1.1.1、固定した変更履歴、テスト証拠に照合しました。
1つの didCrash では情報が足りない
復旧に必要なのは少なくとも次の二つです。
- 完了済みチェックポイント — どのウィンドウ、タブ、グループを復旧できるかを表すデータ。
- 通常終了のレシート — 全ウィンドウを閉じる流れが始まったことを、終了の途中で永続化した証拠。
前者は「復旧できる候補がある」を示します。後者は「前回は普通に閉じた可能性が高い」を示します。同じ真偽値に押し込むと、通常終了のたびにクラッシュ画面を出すか、本当のクラッシュを見逃すかのどちらかになります。
type RecoveryDecisionInput = {
checkpoint?: {
generation: number;
completedAt: number;
};
gracefulCloseReceipt?: {
generation: number;
observedAt: number;
};
};
function decideRecovery(input: RecoveryDecisionInput) {
if (!input.checkpoint) return 'nothing-to-offer';
if (matches(input.checkpoint, input.gracefulCloseReceipt)) {
return 'normal-close';
}
return 'offer-latest-completed-checkpoint';
}
これは説明用の擬似コードです。重要なのは、復旧データの存在と終了理由の証拠を別の寿命・別の識別子で管理することです。
図1 — 実際の製品 UI ではなく、終了の証拠と復旧データを分離する release-neutral な概念図です。
レシートは「最後の瞬間」に書かない
ブラウザプロセスが消える直前にだけレシートを書こうとすると、Manifest V3 の service worker がその瞬間まで生きている保証はありません。また、複数ウィンドウの最後の一つが閉じた時点では、前のイベントを失っている可能性があります。
そこで通常終了のシグナルは、全ウィンドウが閉じる流れを観測した早い段階で記録します。そのレシートをチェックポイントの generation と結び付ければ、古い終了イベントが新しいチェックポイントを誤って「通常終了済み」にするのを防げます。
ここでレシートを復旧データそのものに埋め込まないことも重要です。チェックポイントは読み取り可能な復旧候補として残し、レシートは終了判定の入力として扱います。通常終了だったからといって、利用者が明示的に保存したスナップショットまで消す必要はありません。
「書き始めた」と「完了した」を分ける
クラッシュ復旧で使えるのは完了済みチェックポイントだけです。タブ一覧を書いた後でグループ情報の永続化に失敗した中間状態を、最新という理由だけで選んではいけません。
ジャーナルには、少なくとも次の境界が必要です。
- operation を作成した
- 復旧対象の構造を検証した
- チェックポイントを永続化した
- 完了マーカーを同じ generation に対して確定した
起動時の判定は、最後に触られたレコードではなく、最後に完了した generation を読むべきです。これにより、service worker の停止やプロファイル再起動の途中に残った操作を、成功したバックアップとして提示しません。
UI の約束も小さくする
復旧画面では「完全に元に戻ります」と約束しません。安全に言えるのは、設定で保護が有効であり、完了済みチェックポイントが存在し、通常終了のレシートと矛盾しない場合に、その候補を提示できることです。
さらに、段階的な Gentle Restore の Stop は新しいタブの調整を止めますが、すでに作成した placeholder tab を自動で閉じません。後から利用者がそのタブを別用途に使った可能性があり、永続的な ownership proof なしに削除すると、復旧機能自身がデータ損失を起こすからです。
最低限のテスト行列
終了判定は単体テストだけでは足りません。少なくとも次を別ケースにします。
- 複数ウィンドウを順番に普通に閉じた。
- service worker が途中で停止してから通常終了した。
- 完了済みチェックポイントの後でプロセスが落ちた。
- 未完了の新しい operation と、完了済みの古い checkpoint が同時に残った。
- 古い graceful-close receipt が新しい generation に届いた。
- 復旧の途中でプロファイル全体を再起動し、同じ operation を再開した。
Tabwell 1.1.1 は Chrome Web Store で公開されています。製品との関係を明示すると、私はその開発者です。この記事の中心は製品紹介ではなく、復旧できるデータと正常終了だった証拠を別々に持つことで、MV3 の不安定な実行寿命に耐える判定を作る、という設計境界です。
公開版: https://chromewebstore.google.com/detail/tabwell/ckkifiiepifeihfoblhhjmbigjcjpfdk
