IndexedDBのトランザクションは読み書きを原子的に処理します。durabilityはコミット時に性能と耐久性のどちらを優先するかブラウザーへ伝えるヒントであり、物理媒体への保存や障害後の復旧を保証するものではありません。

ブラウザーの文書カードが円筒形の保存領域へ移る概念イラスト

保存を表す比喩です。IndexedDBのトランザクション状態やstrict・relaxed・defaultの選択、実際のコミット結果は示していません。

オブジェクトストアとインデックス

オブジェクトストアはキーと値からなるレコードを保持し、同じストア内でキーは重複できません。インデックスは保存値の別の項目から検索する経路です。この論理モデルからブラウザーのディスク構造や一律の容量上限を推測することはできません。

トランザクションが要求を受け付ける状態

W3C仕様はactive、inactive、committing、finishedの状態を定めています。生成直後と関連要求のイベントを処理している間はactiveで、追加要求を登録できます。イベント処理が終わるとinactiveになります。要求と結果処理が完了し、新しい要求がなく、abortされていないinactiveトランザクションについて、実装はコミットを試みます。コミットは原子的で、失敗時はabort手順に進み、そのトランザクションの変更を取り消します。タイマーや通信の応答を待った後もトランザクションがactiveだと考えないでください。ただし、setTimeout()やfetch()を呼んだだけで必ず中断されるわけでもありません。その後にデータベース要求を登録する時点の状態が重要です。

仮想例:通信応答の後にトランザクションを作る

サーバーから下書きを取得し、ローカルに保存する場合を考えます。まず通信とJSONの解析を終え、その後でトランザクションを作成します。作成直後にput()を同期的に登録し、最後にcompleteを待って、ブラウザーがトランザクションの完了を通知したことを確認します。

前提: dbはすでに開かれており、draftsオブジェクトストアはkeyPath: "id"で作成済みで、応答オブジェクトにidが含まれます。

function completed(tx) {
  return new Promise((resolve, reject) => {
    tx.addEventListener("complete", resolve, { once: true });
    tx.addEventListener("abort", () => reject(tx.error ?? new Error("Transaction aborted")), { once: true });
  });
}

async function saveDraft(db) {
  const response = await fetch("/api/draft/42");
  if (!response.ok) throw new Error("Draft request failed");
  const draft = await response.json();

  const tx = db.transaction("drafts", "readwrite");
  const done = completed(tx);
  tx.objectStore("drafts").put(draft);
  await done;
}

これは説明用のコードで、実行検証はしていません。順序を逆にし、トランザクションを先に作ってからfetch()をawaitし、その後でput()を呼ぶと、待機中にトランザクションがinactiveまたは完了状態になり、要求の登録に失敗することがあります。W3C仕様では、制御がイベントループへ戻るとトランザクションはinactiveになり、その状態では新しい要求を登録できません。

別の仮想例として、編集21と編集22の通信応答が逆順に届く場合があります。到着順に保存すると、遅れて届いた古い21が新しい22を上書きしかねません。サーバーが単調増加するrevisionを付け、同じreadwriteトランザクション内で保存済みrevisionと比較し、新しい版だけを書き込む設計にします。期待される結果は、古い応答が遅れても最新の保存値を置き換えないことです。この競合と結果は対象アプリで別途試験してください。

`durability`で選べる範囲

strictでは、未処理の変更が永続ストレージに書き込まれたことを確認してからコミット済みとみなすことができます。relaxedでは、変更がOSへ書き込まれた時点で追加確認なしに完了とみなすことができます。defaultではブラウザーが選択します。これは仕様が認めるコミット動作であり、ディスクへの書き込み方式や停電後の保持をアプリから指定する設定ではありません。

再生成できるキャッシュと、直近の書き込みを失うと困るデータを分けてヒントを選び、対応状況も確認してください。完了イベントはブラウザー実装上のトランザクション完了を知らせるもので、バックアップの代わりにはなりません。端末紛失や保存領域の破損後にも必要なデータには、同期や別のバックアップなど復旧手段を用意します。ローカル保存だけで永久に残ると利用者に約束しないでください。

参考資料