Ограничения времени жизни транзакции в IndexedDB

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 (например, сетевой запрос или таймер)
  • управление возвращается event loop
  • IndexedDB считает, что активных запросов больше нет
  • транзакция закрывается
  • последующие обращения к store вызывают TransactionInactiveError

Это поведение часто становится источником нестабильных ошибок, которые сложно воспроизвести.

Как Dexie.js взаимодействует с жизненным циклом транзакции

Dexie.js упрощает работу с IndexedDB, но не отменяет базовую модель транзакций. Внутри Dexie транзакции создаются через обёртки над нативным API, однако срок их жизни остаётся подчинённым правилам браузера.

Dexie отслеживает цепочки операций и старается удерживать транзакцию активной, пока выполняются связанные IndexedDB-запросы. Однако любое внешнее ожидание (await на не-IDB промисах) разрывает эту связь.

Особенно важно, что Dexie не может «заморозить» транзакцию или предотвратить её закрытие, если браузер уже считает её завершённой по спецификации.

Ошибка TransactionInactiveError как следствие

Наиболее характерное проявление проблемы — ошибка TransactionInactiveError. Она возникает в момент, когда код пытается выполнить операцию над хранилищем, но транзакция уже закрыта.

Типичный пример причины:

  • внутри транзакции выполняется await fetch(...)
  • после получения ответа выполняется table.put(...)

На момент выполнения put транзакция уже недоступна, так как между операциями произошёл разрыв в асинхронной цепочке.

Влияние структуры кода на продолжительность транзакции

Продолжительность жизни транзакции зависит не от времени выполнения JavaScript-кода как такового, а от плотности IndexedDB-операций внутри одного синхронного/логически связанного потока.

Транзакция остаётся активной, пока:

  • есть незавершённые IDB-запросы
  • управление не вернулось в event loop без pending операций

Как только цепочка IDB-вызовов прерывается ожиданием внешнего Promise, начинается потенциальное окно закрытия.

Паттерны, приводящие к преждевременному закрытию

Наиболее проблемные конструкции:

  • await между операциями базы данных
  • смешивание сетевых запросов и операций хранения внутри одной транзакции
  • использование setTimeout внутри транзакционного блока
  • последовательное выполнение операций с паузами, не связанными с Dexie

Каждый из этих паттернов создаёт разрыв, в котором IndexedDB считает транзакцию завершённой.

Поведение при пакетных операциях

При использовании bulkPut, bulkAdd, bulkDelete транзакции обычно работают стабильно, поскольку Dexie формирует плотный поток IDB-запросов без промежуточных пауз. В таких случаях транзакция удерживается активной до завершения всей пакетной операции.

Это один из немногих сценариев, где ограничение времени жизни транзакции практически не проявляется, так как отсутствуют точки возврата в event loop.

Асинхронные функции и «ложная последовательность»

Современный синтаксис async/await создаёт иллюзию последовательного выполнения, но внутри IndexedDB-транзакции это не гарантирует непрерывность контекста.

Каждый await — это потенциальный разрыв, после которого выполнение продолжается уже вне первоначального контекста транзакции. Даже если код выглядит линейно, он фактически разбивается на несколько микрозадач.

Практическое следствие для архитектуры

Ограничение времени жизни транзакции приводит к необходимости проектировать код так, чтобы:

  • все операции IndexedDB выполнялись без внешних пауз
  • транзакционные блоки были максимально «плотными»
  • внешние зависимости (сеть, таймеры, вычисления) выносились за пределы транзакции

Dexie.js поощряет модель, где транзакция используется как атомарный блок исключительно для работы с базой данных, без смешивания с побочными асинхронными процессами.

Поведение при повторных попытках доступа

Если транзакция закрылась преждевременно, повторный доступ к store в рамках старого контекста не восстанавливает её. Вместо этого требуется создание новой транзакции. IndexedDB не поддерживает «реанимацию» транзакции или её расширение после закрытия.

Это усиливает важность контроля границ транзакции: её нельзя расширить задним числом, можно только создать заново с тем же набором object store.

Внутренние причины ограничения

Ограничение времени жизни транзакции связано с архитектурой IndexedDB:

  • транзакции привязаны к event loop
  • предотвращается блокировка UI потоков
  • гарантируется освобождение ресурсов при отсутствии активности
  • упрощается конкурентная модель доступа к данным

Такой дизайн делает систему предсказуемой для браузера, но накладывает строгие ограничения на разработчика.

Итоговое влияние на использование Dexie.js

Dexie.js не устраняет ограничение, а лишь делает его более заметным за счёт удобного API. Чем выше уровень абстракции и чем чаще используется async/await, тем проще случайно выйти за границы жизненного цикла транзакции.

Поэтому модель работы с Dexie фактически требует дисциплины: транзакция должна быть короткой, плотной и изолированной от любых внешних асинхронных задержек.