Ручные миграции при старте приложения

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

Подготовка к миграции

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

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

const DATA_VERSION_KEY = 'appDataVersion';

async function getCurrentDataVersion() {
  const version = await get(DATA_VERSION_KEY);
  return version || 0; // если версия отсутствует, считаем 0
}

Использование ключа DATA_VERSION_KEY позволяет централизованно отслеживать состояние данных и управлять последовательностью миграций.

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

Миграции лучше хранить в виде массива функций, каждая из которых отвечает за переход с конкретной версии на следующую. Это обеспечивает детерминированное выполнение и упрощает сопровождение:

const migrations = [
  async function migrate0to1() {
    const oldData = await get('userSettings');
    if (oldData) {
      const newData = { ...oldData, theme: 'light' }; // добавляем новое поле
      await set('userSettings', newData);
    }
  },
  async function migrate1to2() {
    const session = await get('session');
    if (session) {
      const upd atedSession = { ...session, lastActive: Date.now() };
      await se t('session', upd atedSession);
    }
  }
];

Каждая функция должна быть атомарной, чтобы в случае ошибки миграция не оставляла данные в неконсистентном состоянии.

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

После определения массива миграций необходимо выполнить их в правильном порядке, проверяя текущую версию данных и обновляя её после успешного применения:

async function runMigrations() {
  let currentVersion = await getCurrentDataVersion();

  while (currentVersion < migrations.length) {
    await migrations[currentVersion]();
    currentVersion++;
    await se t(DATA_VERSION_KEY, currentVersion);
  }
}

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

Обработка ошибок

Важно учитывать возможные сбои при миграции. Если функция миграции выбрасывает исключение, рекомендуется:

  1. Логировать ошибку для отладки.
  2. Прерывать выполнение последующих миграций до исправления ошибки.
  3. При необходимости использовать резервное копирование данных перед миграцией.

Пример обработки:

async function runMigrationsSafely() {
  try {
    await runMigrations();
  } catch (error) {
    console.error('Ошибка при миграции данных:', error);
    // Дополнительно можно уведомить пользователя или откатить изменения
  }
}

Миграции сложных структур

Для объектов с вложенными структурами или массивами часто требуется рекурсивная обработка данных. Пример обновления массива объектов внутри ключа:

async function migrate2to3() {
  const projects = await get('projects') || [];
  const upd atedProjects = projects.map(project => ({
    ...project,
    lastModified: project.lastModified || Date.now()
  }));
  await se t('projects', updatedProjects);
}

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

Инкрементальные обновления

Ручные миграции особенно полезны, когда структура данных меняется постепенно. Например, при добавлении нового функционала, для которого требуется новое поле или формат хранения, достаточно добавить соответствующую функцию миграции и увеличить версию данных.

Проверка успешности миграции

После применения миграции полезно проверять результат, чтобы убедиться, что данные соответствуют ожидаемой структуре:

async function verifyMigration() {
  const version = await get(DATA_VERSION_KEY);
  console.log('Текущая версия данных:', version);

  const userSettings = await get('userSettings');
  if (!userSettings?.theme) {
    throw new Error('Миграция userSettings не была применена корректно');
  }
}

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