Последовательные операции и гонки состояний

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

Асинхронность достигается через промисы. Любая операция, такая как get, set или del, возвращает промис, который резолвится после завершения транзакции. Например:

import { get, set } from 'idb-keyval';

set('user', { name: 'Alice' }).then(() => {
    return get('user');
}).then(value => {
    console.log(value); // { name: 'Alice' }
});

В этом примере операции выполняются последовательно, так как получение значения начинается только после того, как промис set завершён.

Проблемы гонок состояний

Гонки состояний возникают, когда несколько асинхронных операций обращаются к одним и тем же данным одновременно, и порядок выполнения не гарантирован.

Пример потенциальной гонки:

import { get, set } from 'idb-keyval';

set('counter', 1);
set('counter', 2);

get('counter').then(value => {
    console.log(value); // может быть 1 или 2
});

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

Последовательные обновления

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

Использование промисов

set('counter', 1)
    .then(() => set('counter', 2))
    .then(() => get('counter'))
    .then(value => {
        console.log(value); // гарантированно 2
    });

Использование async/await

async function updateCounter() {
    await set('counter', 1);
    await set('counter', 2);
    const value = await get('counter');
    console.log(value); // 2
}

updateCounter();

Асинхронные функции с await делают код более читаемым и визуально отражают последовательность операций.

Транзакции и атомарные операции

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

Для сложных последовательных изменений можно создать кастомный контейнер с транзакцией:

import { Store, set, get } from 'idb-keyval';

const myStore = new Store('my-db', 'my-store');

async function atomicUpdate() {
    const current = await get('key', myStore) || 0;
    await set('key', current + 1, myStore);
}

Каждая операция всё ещё выполняется асинхронно, но использование одного Store позволяет управлять локальным контекстом данных.

Очередь операций

Для управления гонками удобно использовать очередь операций. Идея: все запросы к одной ключевой сущности помещаются в очередь и выполняются по порядку.

Пример реализации простой очереди:

let queue = Promise.resolve();

function enqueue(operation) {
    queue = queue.then(operation);
    return queue;
}

enqueue(() => set('counter', 1));
enqueue(() => set('counter', 2));
enqueue(() => get('counter').then(console.log)); // 2

Этот подход гарантирует, что все операции выполняются строго последовательно.

Совместное использование нескольких потоков

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

Для синхронизации можно использовать:

  • BroadcastChannel для уведомления о завершении операции;
  • lock API (например, navigator.locks) для организации критических секций.

Пример использования BroadcastChannel:

const channel = new BroadcastChannel('counter');

async function increment() {
    const value = await get('counter') || 0;
    await set('counter', value + 1);
    channel.postMessage('updated');
}

channel.onmess age = () => {
    console.log('Counter updated in another tab');
};

Практические рекомендации

  • Всегда оборачивать последовательные операции через await или цепочки промисов.
  • Для атомарных изменений использовать отдельный Store.
  • Для ключей с высокой частотой обновления — очередь операций или механизмы синхронизации между вкладками.
  • Избегать параллельного вызова set на один ключ без контроля порядка выполнения.

Резюме подходов к гонкам состояний

  1. Последовательные промисы — простой и эффективный способ.
  2. async/await — более читаемая альтернатива.
  3. Кастомный Store для локальных транзакций — контроль области данных.
  4. Очередь операций — строгий порядок выполнения.
  5. Межвкладочная синхронизация — для работы в нескольких потоках или вкладках.

Эти техники позволяют безопасно работать с Idb-keyval в сложных сценариях, где требуется точный порядок операций и защита от гонок состояния.