Блокировки и конкурентный доступ из нескольких вкладок

Idb-keyval — это легковесная обёртка над IndexedDB, которая упрощает работу с асинхронным хранилищем ключ-значение. Основная задача библиотеки — предоставить удобный интерфейс для операций get, set, del и clear без необходимости писать сложный код с транзакциями и открытием базы данных.

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

Транзакции и атомарность

Каждая операция set или del в idb-keyval фактически создаёт отдельную транзакцию с режимом readwrite. Транзакции в IndexedDB атомарны: либо все изменения внутри транзакции фиксируются, либо откатываются.

Пример записи:

import { set } from 'idb-keyval';

await set('user', { name: 'Alice', age: 25 });

Если два вызова set('user', {...}) происходят одновременно в разных вкладках, конечное значение будет зависеть от того, какая транзакция завершится последней. Библиотека не предотвращает потерю данных, если параллельные операции выполняются на одном ключе.

Проблемы конкурентного доступа

Основные сценарии гонки данных:

  1. Одновременное обновление одного ключа

    • Вкладка A читает значение, модифицирует его и пишет обратно.
    • Вкладка B делает то же самое в тот же момент.
    • Последняя запись перезаписывает предыдущую, теряя изменения.
  2. Удаление ключа во время чтения

    • Вкладка A читает значение для обработки.
    • Вкладка B удаляет этот ключ.
    • Вкладка A может работать с устаревшими данными, создавая неконсистентное состояние.

Механизмы синхронизации между вкладками

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

1. LocalStorage или BroadcastChannel как сигнал

Можно использовать localStorage или BroadcastChannel для уведомления других вкладок о начале или завершении изменения:

const channel = new BroadcastChannel('idb-keyval-sync');

async function safeSet(key, value) {
    channel.postMessage({ key, action: 'start' });
    await set(key, value);
    channel.postMessage({ key, action: 'end' });
}

channel.onmess age = (event) => {
    const { key, action } = event.data;
    if (action === 'start') {
        console.log(`Ключ ${key} занят другой вкладкой`);
    }
};

Этот метод не блокирует запись на уровне IndexedDB, но позволяет реализовать мягкие блокировки и предупредить гонки.

2. Использование версии данных или атомарных счетчиков

Можно хранить для каждого ключа поле version и проверять его при записи:

const data = await get('settings') || { value: {}, version: 0 };
data.value.theme = 'dark';
data.version++;
await set('settings', data);

Если другая вкладка изменила version между чтением и записью, изменения можно откатить или повторить с актуальной версией. Это эмулирует оптимистичную блокировку.

3. IndexedDB с транзакцией на несколько операций

Для операций, которые должны быть атомарны внутри одной вкладки, можно использовать транзакции напрямую через idb (основу idb-keyval) и объединять несколько ключей в одной транзакции. Пример:

import { openDB } from 'idb';

const db = await openDB('app-db', 1, {
    upgrade(db) {
        db.createObjectStore('keyval');
    }
});

await db.transaction('keyval', 'readwrite', async tx => {
    const store = tx.objectStore('keyval');
    const valueA = await store.get('a');
    const valueB = await store.get('b');
    store.put(valueA + valueB, 'c');
});

Здесь операции на ключах a, b, c выполняются атомарно внутри одной транзакции, что предотвращает неконсистентность в пределах одной вкладки, но не между вкладками.

Рекомендации по предотвращению конфликтов

  • Минимизировать параллельные записи одного ключа: объединять операции и использовать единый источник правды.
  • Использовать версионные счетчики или временные метки: помогают реализовать стратегию «последний выиграл» или откатить старые изменения.
  • Сигнализация между вкладками: BroadcastChannel предпочтительнее localStorage, так как не генерирует событие storage при записи в той же вкладке.
  • Объединять операции в транзакции внутри одной вкладки для сложных изменений.

Заключение по конкурентному доступу

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

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

Только сочетание этих подходов позволяет безопасно работать с общими данными в нескольких вкладках без потери информации и гонок данных.