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/awaitasync 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 разделяет данные между окнами, но транзакции остаются независимыми. Следовательно, гонки могут возникнуть при параллельной работе разных вкладок или веб-воркеров.
Для синхронизации можно использовать:
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 на один ключ без
контроля порядка выполнения.async/await — более читаемая
альтернатива.Store для локальных
транзакций — контроль области данных.Эти техники позволяют безопасно работать с Idb-keyval в
сложных сценариях, где требуется точный порядок операций и защита от
гонок состояния.