Прерывание транзакции: abort()

Транзакции в Dexie.js поддерживают механизм принудительного прерывания выполнения через abort(), который позволяет полностью откатить все изменения, сделанные внутри транзакции, и перевести её в состояние отмены без частичного сохранения данных. Это один из ключевых инструментов контроля целостности данных при работе с IndexedDB, особенно в сценариях сложных цепочек операций, где ошибка на любом этапе делает бессмысленным сохранение промежуточного результата.

Транзакция в Dexie.js имеет чётко определённые жизненные состояния: активная, завершённая и прерванная. Вызов abort() переводит транзакцию в состояние прерывания и инициирует откат всех изменений, сделанных в рамках текущего контекста.

После вызова:

  • все незавершённые операции записи отменяются;
  • транзакция помечается как aborted;
  • дальнейшие операции внутри неё становятся невозможными;
  • все промисы, связанные с транзакцией, отклоняются с ошибкой AbortError;
  • откат происходит атомарно на уровне IndexedDB.

Важно, что abort() влияет не только на текущую операцию, но и на весь транзакционный контекст.

Базовая структура использования abort()

В Dexie.js транзакция создаётся через db.transaction(...), а метод abort() доступен через объект транзакции:

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

    if (someCriticalCondition) {
        tx.abort();
    }

    await db.orders.add({ id: 100, userId: 1 });
});

В момент вызова tx.abort() выполнение транзакции немедленно помечается как прерванное, и последующие операции либо не выполняются, либо игнорируются системой отката.

Отличие abort() от throw

Внутри транзакций Dexie.js прерывание может быть достигнуто двумя способами:

  • tx.abort() — явное программное прерывание;
  • throw Error — аварийное завершение через исключение.

Разница проявляется в семантике:

abort():

  • означает контролируемую отмену;
  • не обязательно сопровождается исключением в логике;
  • используется как бизнес-логика отмены;

throw:

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

Оба механизма приводят к откату транзакции, но abort() предпочтителен, когда отмена является нормальной частью логики.

Состояние после abort()

После вызова abort() транзакция переходит в состояние, при котором:

  • любые await внутри транзакции завершаются с ошибкой;
  • Dexie прекращает выполнение очереди операций;
  • изменения не коммитятся в IndexedDB;
  • события onerror и onabort могут быть вызваны на уровне транзакции.

Пример наблюдаемого поведения:

db.transaction('rw', db.items, async (tx) => {
    tx.on('abort', () => {
        console.log('Транзакция отменена');
    });

    await db.items.add({ id: 1 });

    tx.abort();

    await db.items.add({ id: 2 }); // не выполнится
});

Причины использования abort()

Проверка бизнес-ограничений

Часто транзакция прерывается при нарушении условий целостности:

db.transaction('rw', db.accounts, async (tx) => {
    const acc = await db.accounts.get(1);

    if (acc.balance < 0) {
        tx.abort();
    }

    await db.accounts.update(1, { status: 'invalid' });
});

Валидация входных данных

Если данные не проходят проверку, нет смысла сохранять частичные изменения:

db.transaction('rw', db.users, async (tx) => {
    if (!isValid(userData)) {
        tx.abort();
    }

    await db.users.add(userData);
});

Координация сложных операций

При работе с несколькими store транзакция обеспечивает атомарность. abort() гарантирует откат всего набора операций:

db.transaction('rw', db.users, db.logs, async (tx) => {
    await db.users.add(newUser);
    await db.logs.add({ event: 'create_user' });

    if (externalServiceFailed) {
        tx.abort();
    }
});

Влияние abort() на промисы

В Dexie.js транзакции основаны на промисах, и abort() приводит к отклонению всех связанных промисов:

try {
    await db.transaction('rw', db.items, async (tx) => {
        await db.items.add({ id: 1 });
        tx.abort();
        await db.items.add({ id: 2 });
    });
} catch (e) {
    console.log(e.name); // AbortError
}

После abort() управление переходит в catch, а все незавершённые операции считаются отменёнными.

Особенности отката данных

IndexedDB обеспечивает атомарность на уровне транзакции, поэтому:

  • все записи буферизуются до commit;
  • abort() очищает буфер изменений;
  • состояние базы возвращается к исходному;
  • частично записанные данные невозможны.

Однако важно учитывать, что чтение внутри транзакции до abort() всё ещё возвращает локальные изменения, если они были закешированы в рамках транзакции.

abort() в вложенных сценариях

Если транзакции логически вложены или координируются через общий контекст, abort() верхнего уровня отменяет всё дерево операций:

db.transaction('rw', db.a, db.b, async (tx) => {
    await db.a.add({ id: 1 });

    db.transaction('rw', db.b, async (innerTx) => {
        await db.b.add({ id: 2 });
        tx.abort();
    });
});

В этом случае прерывание внешней транзакции отменяет и внутренние операции.

Ограничения abort()

  • нельзя частично отменить транзакцию;
  • нельзя возобновить после abort();
  • невозможно откатить только одну store внутри транзакции;
  • после abort() транзакция считается завершённой.

Практическая модель поведения

Логика abort() строится вокруг принципа “всё или ничего”:

  • либо все операции транзакции фиксируются;
  • либо все операции отменяются целиком.

Это соответствует атомарной модели ACID, реализуемой через IndexedDB и обёртку Dexie.js, где abort() выступает механизмом принудительного отказа от commit на уровне движка хранения данных.