Dexie.js предоставляет строгую модель транзакций поверх IndexedDB, где транзакция привязана к асинхронному контексту выполнения и автоматически становится недоступной после завершения цепочки промисов. Это приводит к ключевой архитектурной задаче: повторное использование одной и той же транзакции между несколькими вызовами функций и слоями приложения без нарушения её жизненного цикла.
Транзакция в Dexie создаётся как ограниченный по времени контекст.
Она активна только внутри текущего микротаска-цепочки, и любые
асинхронные разрывы, которые выходят за рамки оригинального
Promise, приводят к её деактивации.
Типичная проблема проявляется при попытке косвенного использования:
await db.transaction('rw', db.users, async () => {
await createUser(); // функция не знает о транзакции
});
Если createUser() выполняет операции без явного
привязывания к транзакции, Dexie может рассматривать их как внешние
вызовы, создавая новую транзакцию или вызывая
TransactionInactiveError.
Наиболее устойчивый способ повторного использования транзакции — явная передача объекта транзакции в функции нижнего уровня.
db.transaction('rw', db.users, db.orders, async (tx) => {
await createUser(tx, { name: 'Alice' });
await createOrder(tx, { userId: 1, total: 100 });
});
Функции становятся чистыми операциями над транзакцией:
async function createUser(tx, data) {
return tx.users.add(data);
}
async function createOrder(tx, data) {
return tx.orders.add(data);
}
Ключевая особенность подхода — отсутствие скрытых зависимостей от глобального состояния. Транзакция становится явным параметром, что позволяет безопасно повторно использовать её в разных сценариях.
В архитектуре с разделением слоёв транзакция часто проходит через несколько уровней абстракции.
async function serviceCreateUser(tx, input) {
const id = await tx.users.add(input);
await auditLog(tx, { action: 'create_user', id });
return id;
}
async function auditLog(tx, entry) {
return tx.logs.add(entry);
}
Такой подход позволяет одному и тому же сервису работать как внутри транзакции, так и быть частью более крупной транзакции, создаваемой на уровне контроллера.
db.transaction как композиционный
кореньDexie допускает создание транзакции как композиционного корня, внутри которого вызываются переиспользуемые функции:
await db.transaction('rw', db.users, db.logs, async (tx) => {
await serviceCreateUser(tx, { name: 'Bob' });
});
Это формирует строгую иерархию: транзакция создаётся один раз, а все операции наследуют её контекст через явную передачу.
Dexie.currentTransaction как неявного контекстаDexie предоставляет доступ к текущей активной транзакции через глобальный контекст:
import { Dexie } fr om 'dexie';
async function createUser(data) {
const tx = Dexie.currentTransaction;
if (!tx) throw new Error('No active transaction');
return tx.table('users').add(data);
}
Этот подход позволяет не передавать транзакцию явно, но он увеличивает скрытую связанность кода и усложняет повторное использование функций вне транзакции.
При использовании Dexie.currentTransaction возникает ряд
проблем:
В результате повторное использование ухудшается, несмотря на кажущуюся простоту.
Для сложных сценариев часто применяется композиционный стиль, при котором транзакция передаётся через цепочку функций:
async function createUserFlow(tx, user) {
const id = await createUser(tx, user);
await sendWelcomeEvent(tx, id);
return id;
}
async function sendWelcomeEvent(tx, userId) {
await tx.events.add({
type: 'welcome',
userId
});
}
Такой стиль позволяет переиспользовать отдельные шаги в разных сценариях без изменения их логики.
Иногда требуется разделить бизнес-логику на несколько независимых функций, но сохранить одну транзакцию:
await db.transaction('rw', db.users, db.orders, async (tx) => {
const userId = await createUser(tx, { name: 'Eve' });
const orderId = await createOrder(tx, { userId, total: 50 });
await updateStats(tx, userId);
await notify(tx, orderId);
});
Здесь транзакция выступает как единый ресурс, используемый многократно без повторного создания.
Базовый архитектурный принцип сводится к тому, что транзакция должна рассматриваться как обычный параметр:
Это обеспечивает предсказуемость повторного использования:
function buildRepository(tx) {
return {
addUser: (data) => tx.users.add(data),
addOrder: (data) => tx.orders.add(data),
};
}
Повторное использование транзакции особенно важно при каскадных операциях:
async function deleteUserCascade(tx, userId) {
const orders = await tx.orders.wh ere('userId').equals(userId).toArray();
for (const order of orders) {
await tx.items.where('orderId').equals(order.id).delete();
}
await tx.orders.where('userId').equals(userId).delete();
await tx.users.delete(userId);
}
Одна транзакция используется многократно в глубину вызовов без повторного открытия.
Распространённый приём — создание фабрики, которая «захватывает» транзакцию:
function createUserOps(tx) {
return {
async createUser(data) {
return tx.users.add(data);
},
async deleteUser(id) {
return tx.users.delete(id);
}
};
}
Использование:
await db.transaction('rw', db.users, async (tx) => {
const ops = createUserOps(tx);
const id = await ops.createUser({ name: 'Max' });
await ops.deleteUser(id);
});
Такой подход позволяет многократно использовать одну транзакцию через связанный API.
Нарушение повторного использования транзакции обычно возникает в следующих случаях:
tx;db.table вместо tx.table
внутри транзакции;await, приводящий к завершению
контекста.Корректная модель строится вокруг следующих принципов:
tx;db внутри бизнес-логики минимизируется;В библиотеках поверх Dexie часто вводится абстракция:
export async function withTransaction(db, tables, fn) {
return db.transaction('rw', tables, async (tx) => {
return fn(tx);
});
}
Использование:
await withTransaction(db, [db.users, db.logs], async (tx) => {
await serviceCreateUser(tx, data);
});
Это обеспечивает централизованное управление транзакционным контекстом при сохранении повторного использования функций.
В Dexie транзакция становится переносимым контекстом, который:
Такая модель делает транзакцию не локальной операцией, а сквозным ресурсом исполнения, обеспечивающим согласованность данных при сложных сценариях взаимодействия.