Тестирование миграций

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

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

// Запись значения
await set('username', 'Alice');

// Чтение значения
const username = await get('username');

// Удаление значения
await del('username');

// Очистка всего хранилища
await clear();

Создание кастомного хранилища

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

import { Store } from 'idb-keyval';

const storeV1 = new Store('app-db', 'store-v1');
const storeV2 = new Store('app-db', 'store-v2');

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

Организация миграций

Миграции в IndexedDB отличаются от обычных SQL-баз. В Idb-keyval нет встроенных средств версионирования, поэтому миграции реализуются вручную через проверку наличия старых данных и их преобразование.

Пример схемы миграции

  1. Проверка существования старой версии данных
const oldData = await get('userSettings', storeV1);
if (!oldData) return; // Данных нет, миграция не нужна
  1. Трансформация данных в новый формат
const newData = {
    theme: oldData.darkMode ? 'dark' : 'light',
    language: oldData.lang || 'en',
};
  1. Сохранение в новое хранилище
await set('settings', newData, storeV2);
await del('userSettings', storeV1);

Таким образом, данные сохраняются в актуальном формате, а старые записи удаляются, предотвращая конфликт версий.

Работа с асинхронными миграциями

Все операции Idb-keyval возвращают промисы, что позволяет безопасно выполнять последовательные шаги миграции, избегая состояния гонки.

async function migrateV1toV2() {
    const oldData = await get('userSettings', storeV1);
    if (!oldData) return;

    const newData = transform(oldData);
    await set('settings', newData, storeV2);
    await del('userSettings', storeV1);
}

Использование async/await делает код линейным и читаемым, упрощая обработку ошибок:

try {
    await migrateV1toV2();
} catch (error) {
    console.error('Ошибка миграции данных', error);
}

Миграции с несколькими шагами

Иногда необходимо поддерживать несколько последовательных миграций при обновлении приложения с версий v1 → v2 → v3. Рекомендуется хранить текущую версию данных в хранилище и выполнять миграции последовательно.

const currentVersion = await get('dbVersion', storeV2) || 1;

if (currentVersion === 1) {
    await migrateV1toV2();
    await set('dbVersion', 2, storeV2);
}

if (currentVersion === 2) {
    await migrateV2toV3();
    await set('dbVersion', 3, storeV2);
}

Рекомендации по тестированию миграций

  • Изолированное тестовое хранилище: Создавать отдельный экземпляр Store для тестов, чтобы не влиять на реальные данные.
  • Набор тестовых данных: Перед миграцией заранее записывать старые данные для проверки корректности трансформации.
  • Проверка консистентности: После миграции проверять наличие всех ключей, правильность формата значений и отсутствие устаревших записей.
  • Автоматизация: Использовать фреймворки тестирования с асинхронными тестами (Jest, Mocha) для проверки промисов и обработки ошибок.
test('Миграция V1 → V2 корректно преобразует данные', async () => {
    await set('userSettings', { darkMode: true, lang: 'ru' }, storeV1);
    await migrateV1toV2();

    const newData = await get('settings', storeV2);
    expect(newData).toEqual({ theme: 'dark', language: 'ru' });

    const oldData = await get('userSettings', storeV1);
    expect(oldData).toBeUndefined();
});

Поддержка отката миграций

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

const backup = await get('userSettings', storeV1);
await set('userSettings_backup', backup, storeV1);

try {
    await migrateV1toV2();
} catch (error) {
    console.error('Ошибка миграции, откат данных');
    await set('userSettings', backup, storeV1);
}

Такой подход гарантирует сохранность данных, если процесс миграции прервётся или завершится ошибкой.

Оптимизация работы с большими объёмами данных

При миграции крупных объектов стоит разбивать процесс на части, чтобы не блокировать главный поток браузера. Использование for...of с await позволяет постепенно записывать и удалять записи:

for (const [key, value] of Object.entries(largeData)) {
    const transformed = transformValue(value);
    await set(key, transformed, storeV2);
    await del(key, storeV1);
}

Это предотвращает перегрузку памяти и снижает вероятность ошибок при миграции огромных массивов данных.