はじめに

前回、IndexedDB をモダンに記述する為のライブラリとして、 idb の基本的な記述方法の説明を行いました。

今回は、その応用編として、 idb ライブラリで提供される下記の機能についての深堀を行いたいと思います。

  • wrap / unwrap
  • トランザクション処理での注意点
  • 非同期イテレーター

wrap / unwrap

idb ライブラリの openDB() で戻されるデータベースオブジェクトは、標準 API のデータベースオブジェクトを拡張したものであり、同一のものでは、ありません。

又、idb は、IndexedDB のオブジェクト( IDBDatabase, IDBTransaction, IDBObjectStore など)を Proxy でラップして Promise ベースに拡張しています。(厳密には、 Proxy を使っているだけでなく、IDBRequestPromise に変換する内部キューを持っています。)

idb ライブラリが提供する機能は、十分便利なので、通常のアプリ開発では、不便に感じることは少ないと思います。

しかし、次のようなケースでは 標準 API の IndexedDB オブジェクトが必要になることがあります。

ブラウザの API や別のライブラリが返す IDBDatabase を idb の便利な API で扱いたい場合

const rawDb = someLibraryReturnsIDBDatabase();
const db = wrap(rawDb); // idb の API が使えるようになる

postMessage()structuredClone() など、Proxy を含むオブジェクトを受け付けない API に渡す必要がある場合

const raw = unwrap(db);
worker.postMessage(raw); // 生のオブジェクトなら送れる

標準 API の IDBRequest を直接扱いたい場合

const rawStore = unwrap(store);
const request = rawStore.get("id");
request.onsuccess = ...

👉 wrap / unwrap は、IndexedDB オブジェクトを、標準 API の IndexedDB オブジェクトに戻したり、逆にラップし直したりするための関数です。

  • unwrap(obj)

→ idb が拡張したオブジェクトを 標準 API の IndexedDB オブジェクトに戻す。

  • wrap(obj)

→ 標準 API の IndexedDB オブジェクトを idb の拡張オブジェクトに変換する。

unwrap / wrap の実例

unwrap の例

import { openDB, unwrap } from "idb";

const db = await openDB("mydb", 1);
const rawDb = unwrap(db);

console.log(rawDb instanceof IDBDatabase); // true

wrap の例

import { wrap } from "idb";

const rawDb = indexedDB.open("mydb").result;
const db = wrap(rawDb);

await db.get("store", "key"); // idb の Promise API が使える

👉 ただし、idb は通常のアプリ開発では十分便利なので、unwrap / wrap を使う場面は かなりレアケース です。


トランザクション処理での注意点

基本的に、idb のトランザクションは必要なときだけ自動で行われます。明示的に使うことも可能です。

この場合、トランザクション処理中に非同期処理を挟むと、トランザクションが自動クローズするので、注意が必要です。

let val = 1;
const tx = db.transaction("users", "readwrite");
// This is where things go wrong:
const name = await fetch('/getname?val=' + val);
// And this throws an error:
await tx.store.put({ id: val, name: name });
await tx.done;

👉 一見、正しそうですが、store.putでTransactionInactiveError が発生します。

IndexedDB のトランザクションは、イベントループが空になると自動でクローズされる為、

await fetch(...)

のように 非同期処理でイベントループを返すと、トランザクションが閉じてしまうのです。

このような場合、下記のどちらかの方法をとる必要があります。

トランザクションを自動で行う

let val = 1;
const name = await fetch('/getname?val=' + val);
await db.put("users", { id: val, name: name });

非同期処理をトランザクション処理の外で行う

let val = 1;
const name = await fetch('/getname?val=' + val);
const tx = db.transaction("users", "readwrite");
await tx.store.put({ id: val, name: name });
await tx.done;

また、Promise.all()Promise.allSettled() を利用した並列処理を併用する場合も注意が必要です。

tx.donePromise.all() に引き渡して、書き込みと並列処理させてはいけません。

const tx = db.transaction(storeName, 'readwrite');
await Promise.all([
  tx.store.put('bar', 'foo'),
  tx.store.put('world', 'hello'),
  tx.done, // 誤り
]);

👉 idb の tx.done は 「トランザクションが完了したら resolve する Promise」です。Promise.all() に入れた瞬間に 書き込みより先に待ち始め、TransactionInactiveError が発生します。

正しくは、下記の様にする必要があります。

const tx = db.transaction(storeName, 'readwrite');
await Promise.all([
  tx.store.put('bar', 'foo'),
  tx.store.put('world', 'hello'),
]);

await tx.done;


非同期イテレーター

idb ライブラリは、tx.store を async iterable として扱えるようにしています。

その為、cursor を使った処理を下記のように記述することが可能です。

const tx = db.transaction(storeName);

for await (const cursor of tx.store) {
  console.log(cursor.value);
} 

await tx.done;

つまり、

for await (const cursor of tx.store)

は内部的に:

let cursor = await tx.store.openCursor();
while (cursor) {
  // …
  cursor = await cursor.continue();
}

と同じ意味になります。

👉 idb の async iterator は、cursor.continue() を内部で自動的に呼び出すため、開発者は cursor.continue を書く必要がないのです。

この書き方のメリット

  • コードが圧倒的に短い

標準 API の openCursor よりも読みやすい。

  • await が自然に使える

非同期カーソルを for-await-of で書けるのは idb の強み。

  • トランザクションが自動で閉じる

ループが終わると tx.doneresolve される。

注意点(重要)

  • for await を使うには idb v7 以降が必要です。古いバージョンでは動きません。
  • tx = db.transaction(storeName) は readonly トランザクションの為、書き込みはできません。

まとめ

  • wrap / unwrap

    • idb のラップは「 Proxy + Promise 化された IDBRequest 」の組み合わせ
    • wrap / unwrap はこのラップを付け外しするためのもの
  • トランザクション

非同期処理でイベントループが空になるとトランザクションが閉じる。これが TransactionInactiveError の根本原因

  • Promise.all() / Promise.allSettled()

tx.donePromise.all() / Promise.allSettled() に入れると「書き込みより先に待ち始める」ため、トランザクションが閉じてしまう。

  • 非同期イテレーター

idb の async iterator は cursor.continue() を内部で自動実行する。