Az IndexedDB-t sokan úgy kezelik, mintha egy kicsit kényelmetlen, de azért rendes adatbázis lenne a böngészőben. Aztán jön egy offline mentés, egy szerverről kért token vagy egy gyors await fetch(), és a következő put() már TransactionInactiveError-ral dől el. Nem a hálózat a hibás, és nem is az a gép, amelyikre persze „nálam működött”. A tranzakció közben csendben befejeződött.
Ez nem IndexedDB-ízű véletlen: az API tranzakciói szándékosan rövid, eseményciklushoz kötött egységek. Ha lokális adatból számolsz új értéket, majd a szervertől még kérsz valamit, nem kapsz egy kényelmes, hosszan nyitva tartott SQL-tranzakciót. És ez fontosabb különbség, mint amilyennek elsőre hangzik.

A tranzakció nem egy hosszú async függvény
Az IndexedDB specifikációja szerint új kérést akkor tehetsz a tranzakcióba, amikor az aktív. Kezdetben aktív, majd egy hozzá tartozó kérés siker- vagy hibaeseményének kezelése alatt ismét az. Ha nincs függő kérés, a vezérlés visszakerül az eseményciklushoz, és nem bővítetted tovább, a tranzakció inaktívvá válhat, majd automatikusan commitol. Az MDN útmutatója ezt meglepően őszintén fogalmazza: a tranzakció élettartama szorosan az event loophoz kötött.
Ezért működik az, hogy egy IndexedDB-kérés eredményét await-eled, majd ugyanabban a folytatásban új kérést indítasz. A kérés még „életben tartja” a tranzakciót. Viszont egy hálózati kérés, időzítő, animációs frame, üzenet egy workertől vagy bármi más, nem IndexedDB-s aszinkron munka alatt nincs ilyen függő IDB-kérés. Mire visszaérsz, már elkéstél.
const tx = db.transaction("drafts", "readwrite");
const store = tx.objectStore("drafts");
const draft = await requestAsPromise(store.get(id)); // ez még rendben van
const policy = await fetch("/api/editor-policy").then((r) => r.json());
store.put({ ...draft, policy }); // TransactionInactiveError lehet
Na, ezt nem tudtam, amikor először belefutottam: nem az számít, hogy ugyanabban az async függvényben vagy-e. Az számít, hogy a böngésző kapott-e közben alkalmat lezárni a tranzakciót. Az idb könyvtár dokumentációja is külön kiemeli ezt a csapdát; a promise-os API nem írja át az IndexedDB időmodelljét.
A jó minta: előbb a lassú világ, utána a rövid írás
Az egyszerű szabály: hálózati és egyéb külső await legyen a tranzakción kívül. Utána nyiss rövid readwrite tranzakciót, indítsd el benne az összetartozó írásokat, végül a tranzakció végét várd meg. Egy offline szerkesztő például együtt mentheti a lokális dokumentumot és a későbbi szinkronhoz tartozó outbox-bejegyzést:
function transactionDone(tx) {
return new Promise((resolve, reject) => {
tx.addEventListener("complete", resolve, { once: true });
tx.addEventListener("abort", () => reject(tx.error), { once: true });
});
}
async function saveDraft(db, id) {
const policy = await fetch("/api/editor-policy").then((r) => r.json());
const tx = db.transaction(["drafts", "outbox"], "readwrite");
const drafts = tx.objectStore("drafts");
const outbox = tx.objectStore("outbox");
drafts.put({ id, policy, savedAt: Date.now() });
outbox.add({ type: "sync-draft", id, policy });
await transactionDone(tx);
}
Az egyes put()/add() hívásokhoz külön kérésobjektum tartozik; ha hibára pontosan szeretnél reagálni, azokat is kezeld. A végső sikerjelzés mégis a tranzakció complete eseménye: csak ekkor tudod, hogy az egység egésze nem abortált. Az explicit tx.commit() sem varázsragasztó: gyorsabban elindíthatja a lezárást, de nem tartja nyitva a tranzakciót egy fetch() idejére.
Ha a szerver válaszához kell a lokális adat
Itt szokott előkerülni a vágy, hogy „akkor tartsuk nyitva, amíg visszaér a szerver”. Ne. Egyrészt nem erre való, másrészt a readwrite tranzakciók ugyanazon object store-on sorosíthatók; az MDN szerint egy store-hoz egyszerre csak egy ilyen írási tranzakció fut. Egy percekig hálózatra váró tranzakció pont úgy hiányzott a UI-ból, mint egy villogó loading overlay 2012-ből.
Inkább két fázisban gondolkodj: egy rövid readonly olvasás, aztán a hálózati kérés, majd egy rövid readwrite tranzakció. Ha az első olvasás alapján számított válasz versenyezhet másik fül vagy worker módosításával, a második fázisban olvasd vissza a rekord verzióját, és csak egyezéskor írj. Ez nem egyetlen, böngészőn átívelő atomikus tranzakció — olyan nincs —, de legalább láthatóvá teszi a versenyhelyzetet ahelyett, hogy egy véletlen await mögé rejtené.
Mikor használnám?
Offline-first felületen, kliensoldali outboxnál, letöltési metaadatoknál és összetartozó cache-bejegyzéseknél igen: egy rövid IndexedDB-tranzakció jó eszköz arra, hogy a kapcsolódó lokális változások együtt történjenek meg vagy egyik se. A tranzakción belüli hálózati workflow-ra viszont nem használnám productionben. A legtöbb ilyen kód valójában túlkomplikált kétfázisú folyamat, amelyet egy túl hosszú tranzakciónak öltöztettek fel. Vedd le róla a jelmezt: a lassú munkát intézd előbb, az adatbázisban pedig írj gyorsan, egyben, majd várd meg a complete-et.