IndexedDB построена вокруг модели транзакций с жёстко ограниченным временем жизни, и это одно из ключевых ограничений, которое напрямую влияет на архитектуру приложений, использующих Dexie.js. Транзакция в IndexedDB не является «долгоживущей» сущностью: она существует ровно до тех пор, пока в её рамках выполняются запросы к базе данных и пока поток выполнения не возвращается в цикл событий без активной очереди операций. Как только движок понимает, что дальнейших запросов не ожидается, транзакция автоматически закрывается, даже если код JavaScript ещё продолжает выполняться.
Транзакция в IndexedDB имеет несколько состояний: активное выполнение запросов, ожидание завершения запросов и завершение. Ключевая особенность заключается в том, что её «жизнь» не контролируется напрямую разработчиком. Вместо этого она управляется браузером на основании активности request-очереди.
Когда создаётся транзакция, например в Dexie.js через
db.transaction('rw', table, () => {...}), браузер
резервирует ресурсы и открывает доступ к объектным хранилищам. Пока
внутри транзакции создаются и выполняются запросы (get,
put, add, delete,
openCursor), она остаётся активной. Но как только все
запросы завершены и стек выполнения выходит в следующий тик event loop,
транзакция становится кандидатом на закрытие.
Это поведение фундаментально отличается от классических
SQL-транзакций, где время жизни контролируется явно через
BEGIN и COMMIT.
На практике наиболее проблемный аспект связан с асинхронностью
JavaScript. Любая операция await, которая не связана
напрямую с IndexedDB-запросом, создаёт паузу, во время которой
управление возвращается в event loop. В этот момент браузер может
завершить транзакцию.
Типичный сценарий:
await на внешнем Promise (например, сетевой
запрос или таймер)store вызывают
TransactionInactiveErrorЭто поведение часто становится источником нестабильных ошибок, которые сложно воспроизвести.
Dexie.js упрощает работу с IndexedDB, но не отменяет базовую модель транзакций. Внутри Dexie транзакции создаются через обёртки над нативным API, однако срок их жизни остаётся подчинённым правилам браузера.
Dexie отслеживает цепочки операций и старается удерживать транзакцию
активной, пока выполняются связанные IndexedDB-запросы. Однако любое
внешнее ожидание (await на не-IDB промисах) разрывает эту
связь.
Особенно важно, что Dexie не может «заморозить» транзакцию или предотвратить её закрытие, если браузер уже считает её завершённой по спецификации.
Наиболее характерное проявление проблемы — ошибка
TransactionInactiveError. Она возникает в момент, когда код
пытается выполнить операцию над хранилищем, но транзакция уже
закрыта.
Типичный пример причины:
await fetch(...)table.put(...)На момент выполнения put транзакция уже недоступна, так
как между операциями произошёл разрыв в асинхронной цепочке.
Продолжительность жизни транзакции зависит не от времени выполнения JavaScript-кода как такового, а от плотности IndexedDB-операций внутри одного синхронного/логически связанного потока.
Транзакция остаётся активной, пока:
Как только цепочка IDB-вызовов прерывается ожиданием внешнего Promise, начинается потенциальное окно закрытия.
Наиболее проблемные конструкции:
await между операциями базы данныхsetTimeout внутри транзакционного
блокаКаждый из этих паттернов создаёт разрыв, в котором IndexedDB считает транзакцию завершённой.
При использовании bulkPut, bulkAdd,
bulkDelete транзакции обычно работают стабильно, поскольку
Dexie формирует плотный поток IDB-запросов без промежуточных пауз. В
таких случаях транзакция удерживается активной до завершения всей
пакетной операции.
Это один из немногих сценариев, где ограничение времени жизни транзакции практически не проявляется, так как отсутствуют точки возврата в event loop.
Современный синтаксис async/await создаёт иллюзию
последовательного выполнения, но внутри IndexedDB-транзакции это не
гарантирует непрерывность контекста.
Каждый await — это потенциальный разрыв, после которого
выполнение продолжается уже вне первоначального контекста транзакции.
Даже если код выглядит линейно, он фактически разбивается на несколько
микрозадач.
Ограничение времени жизни транзакции приводит к необходимости проектировать код так, чтобы:
Dexie.js поощряет модель, где транзакция используется как атомарный блок исключительно для работы с базой данных, без смешивания с побочными асинхронными процессами.
Если транзакция закрылась преждевременно, повторный доступ к store в рамках старого контекста не восстанавливает её. Вместо этого требуется создание новой транзакции. IndexedDB не поддерживает «реанимацию» транзакции или её расширение после закрытия.
Это усиливает важность контроля границ транзакции: её нельзя расширить задним числом, можно только создать заново с тем же набором object store.
Ограничение времени жизни транзакции связано с архитектурой IndexedDB:
Такой дизайн делает систему предсказуемой для браузера, но накладывает строгие ограничения на разработчика.
Dexie.js не устраняет ограничение, а лишь делает его более заметным
за счёт удобного API. Чем выше уровень абстракции и чем чаще
используется async/await, тем проще случайно выйти за
границы жизненного цикла транзакции.
Поэтому модель работы с Dexie фактически требует дисциплины: транзакция должна быть короткой, плотной и изолированной от любых внешних асинхронных задержек.