Вложенные транзакции

В Dexie.js транзакции построены поверх механизма IndexedDB, где каждая транзакция представляет собой атомарный контекст доступа к наборам объектов (object stores). В отличие от «плоской» модели многих библиотек, Dexie добавляет слой управления контекстом выполнения, позволяющий повторно использовать уже активную транзакцию и тем самым формировать поведение, которое принято называть вложенными транзакциями.

При вызове:

db.transaction('rw', db.users, db.orders, async () => {
    // операции
});

Dexie создаёт объект транзакции, который становится текущим контекстом выполнения. Внутри этого контекста библиотека отслеживает все обращения к таблицам, автоматически привязывая их к активной транзакции.

Ключевой момент: активная транзакция хранится в контексте исполнения (AsyncLocalStorage-подобная модель на уровне Dexie), и доступна через Dexie.currentTransaction.

Поведение вложенных транзакций

Если внутри уже активной транзакции выполняется ещё один вызов db.transaction, возможны три сценария:

  1. Используются те же таблицы и совместимый режим доступа
  2. Используются другие таблицы, но совместимый режим
  3. Запрашивается несовместимый режим или конфликтующий набор таблиц

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-only
  • rw — readwrite

Правила совместимости:

  • rw внутри rw — допустимо, используется общий контекст
  • r внутри rw — допустимо, расширяет доступ, но не изменяет режим
  • rw внутри r — недопустимо, приводит к ошибке или повышению уровня транзакции невозможному в IndexedDB

Dexie не может «разогнать» 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

Поведение вложенного вызова зависит от контекста:

db.transaction('rw', db.users, async () => {

    await db.transaction('rw', db.users, db.orders, async () => {
        await db.orders.put({ id: 1 });
    });

});

Dexie проверяет:

  • активна ли транзакция
  • совпадает ли DB instance
  • входят ли таблицы во внешний набор
  • совместим ли режим доступа

Если условия выполняются, создаётся псевдовложенность — фактически операции регистрируются в одной IndexedDB-транзакции.

Изоляция и область видимости

Каждая транзакция имеет набор привязанных таблиц. Вложенная транзакция не может «выйти» за пределы этого набора.

Если внутри попытаться обратиться к таблице, не включённой во внешний контекст:

db.transaction('rw', db.users, async () => {
    await db.transaction('rw', db.orders, async () => {
        await db.logs.add({ event: "x" });
    });
});

Dexie проверит, что logs не входит в активный набор, и поведение будет зависеть от конфигурации:

  • либо ошибка
  • либо расширение транзакции (если возможно до её старта)
  • либо создание отдельной транзакции (если нет активного контекста совместимости)

Реальное отсутствие вложенных IndexedDB-транзакций

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

  • реиспользование активной транзакции
  • глобальный контекст currentTransaction
  • объединение запросов в один execution scope

Таким образом, «вложенность» — это логическая абстракция, а не отдельные транзакционные сущности.

Асинхронные цепочки и потеря контекста

Одна из ключевых проблем — потеря транзакционного контекста при асинхронных разрывах:

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 store
  • несколько rw транзакций одного DB могут ожидать друг друга
  • неправильная вложенность может привести к deadlock-подобным сценариям

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 имеет ряд фундаментальных ограничений:

  • нет настоящих child-транзакций
  • невозможно частично откатить вложенную операцию
  • режим доступа фиксируется на старте IndexedDB транзакции
  • набор таблиц должен быть известен заранее или расширяться до начала выполнения

Эти ограничения являются прямым следствием архитектуры IndexedDB.

Поведение при параллельных вложенных вызовах

db.transaction('rw', db.users, async () => {

    await Promise.all([
        db.users.put({ id: 1 }),
        db.users.put({ id: 2 })
    ]);

});

Dexie корректно распределяет операции по одной транзакции, но важно учитывать:

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

Итоговая модель поведения

Вложенные транзакции в Dexie формируются не как иерархия отдельных транзакционных объектов, а как единый разделяемый execution context, в котором:

  • активная транзакция переиспользуется
  • набор таблиц объединяется при возможности
  • режим доступа фиксируется на старте
  • ошибки распространяются на весь контекст
  • асинхронность контролируется через Promise-цепочки

Такая модель позволяет имитировать вложенность при сохранении ограничений IndexedDB и обеспечивает атомарность операций на уровне всей транзакционной области.