В Dexie.js транзакции построены поверх механизма IndexedDB, где каждая транзакция представляет собой атомарный контекст доступа к наборам объектов (object stores). В отличие от «плоской» модели многих библиотек, Dexie добавляет слой управления контекстом выполнения, позволяющий повторно использовать уже активную транзакцию и тем самым формировать поведение, которое принято называть вложенными транзакциями.
При вызове:
db.transaction('rw', db.users, db.orders, async () => {
// операции
});
Dexie создаёт объект транзакции, который становится текущим контекстом выполнения. Внутри этого контекста библиотека отслеживает все обращения к таблицам, автоматически привязывая их к активной транзакции.
Ключевой момент: активная транзакция хранится в контексте исполнения
(AsyncLocalStorage-подобная модель на уровне Dexie), и доступна через
Dexie.currentTransaction.
Если внутри уже активной транзакции выполняется ещё один вызов
db.transaction, возможны три сценария:
Dexie старается не создавать новую транзакцию, а «переиспользовать» уже существующую, если это безопасно. Это и формирует модель вложенности.
Пример:
await db.transaction('rw', db.users, async () => {
await db.transaction('rw', db.users, async () => {
await db.users.add({ name: "Alice" });
});
});
Во многих случаях внутренняя транзакция не создаёт новую IndexedDB-транзакцию. Вместо этого Dexie обнаруживает активный контекст и присоединяет операции к нему.
Это возможно, потому что IndexedDB запрещает «истинные» вложенные транзакции, но позволяет множественным операциям выполняться в рамках одной транзакции, если они относятся к тем же object stores.
Режимы транзакций:
r — read-onlyrw — readwriteПравила совместимости:
rw внутри rw — допустимо, используется
общий контекстr внутри rw — допустимо, расширяет доступ,
но не изменяет режимrw внутри r — недопустимо, приводит к
ошибке или повышению уровня транзакции невозможному в IndexedDBDexie не может «разогнать» read-only транзакцию до readwrite, поскольку IndexedDB фиксирует режим на этапе создания.
Dexie предоставляет механизм явного доступа к текущей транзакции:
db.transaction('rw', db.orders, async () => {
const tx = Dexie.currentTransaction;
await db.orders.add({ id: 1 });
await someHelper(tx);
});
Функции, принимающие транзакцию явно:
async function someHelper(tx) {
await tx.table('orders').put({ id: 2 });
}
Такой подход важен для сохранения контекста при декомпозиции бизнес-логики.
Поведение вложенного вызова зависит от контекста:
db.transaction('rw', db.users, async () => {
await db.transaction('rw', db.users, db.orders, async () => {
await db.orders.put({ id: 1 });
});
});
Dexie проверяет:
Если условия выполняются, создаётся псевдовложенность — фактически операции регистрируются в одной IndexedDB-транзакции.
Каждая транзакция имеет набор привязанных таблиц. Вложенная транзакция не может «выйти» за пределы этого набора.
Если внутри попытаться обратиться к таблице, не включённой во внешний контекст:
db.transaction('rw', db.users, async () => {
await db.transaction('rw', db.orders, async () => {
await db.logs.add({ event: "x" });
});
});
Dexie проверит, что logs не входит в активный набор, и
поведение будет зависеть от конфигурации:
IndexedDB не поддерживает настоящую вложенность транзакций. Dexie решает эту проблему через:
currentTransactionТаким образом, «вложенность» — это логическая абстракция, а не отдельные транзакционные сущности.
Одна из ключевых проблем — потеря транзакционного контекста при асинхронных разрывах:
db.transaction('rw', db.users, async () => {
setTimeout(async () => {
await db.users.put({ id: 1 }); // может потерять контекст
}, 100);
});
Dexie не может гарантировать сохранение транзакции через произвольные таймеры или внешние события. Контекст сохраняется только внутри цепочки Promise, управляемой Dexie.
Корректный вариант:
db.transaction('rw', db.users, async () => {
await new Promise(resolve => {
setTimeout(resolve, 100);
});
await db.users.put({ id: 1 });
});
Любая ошибка внутри вложенной структуры приводит к откату всей транзакции:
db.transaction('rw', db.users, async () => {
await db.transaction('rw', db.users, async () => {
throw new Error("fail");
});
await db.users.put({ id: 2 }); // не выполнится
});
Механизм отката основан на IndexedDB: если транзакция помечена как aborted, все операции отменяются автоматически.
IndexedDB использует блокировочную модель. Вложенные транзакции могут усиливать конкуренцию за ресурсы:
rw транзакции блокируют object storerw транзакций одного DB могут ожидать друг
другаDexie минимизирует риск, объединяя транзакции при совпадении контекста.
Если внутри транзакции происходит обращение к таблице, не указанной в списке:
db.transaction('rw', db.users, async () => {
await db.orders.toArray();
});
Dexie попытается расширить набор таблиц, если транзакция ещё не зафиксирована на уровне IndexedDB. Если транзакция уже стартовала — возникает ошибка доступа.
Вложенные транзакции часто используются для композиции бизнес-логики:
async function createUser(db, user) {
await db.users.add(user);
}
async function createOrder(db, order) {
await db.orders.add(order);
}
db.transaction('rw', db.users, db.orders, async () => {
await createUser(db, { id: 1 });
await createOrder(db, { id: 10 });
});
Если каждая функция вызывает собственную транзакцию, Dexie объединяет их в один контекст при совместимости.
Более строгий подход исключает скрытые вложенности:
async function createUser(tx, user) {
await tx.table('users').add(user);
}
db.transaction('rw', db.users, async () => {
const tx = Dexie.currentTransaction;
await createUser(tx, { id: 1 });
});
Это устраняет неоднозначность и делает поток выполнения полностью детерминированным.
Модель Dexie имеет ряд фундаментальных ограничений:
Эти ограничения являются прямым следствием архитектуры IndexedDB.
db.transaction('rw', db.users, async () => {
await Promise.all([
db.users.put({ id: 1 }),
db.users.put({ id: 2 })
]);
});
Dexie корректно распределяет операции по одной транзакции, но важно учитывать:
Вложенные транзакции в Dexie формируются не как иерархия отдельных транзакционных объектов, а как единый разделяемый execution context, в котором:
Такая модель позволяет имитировать вложенность при сохранении ограничений IndexedDB и обеспечивает атомарность операций на уровне всей транзакционной области.