Пошаговая миграция данных в хранилище, построенном на localForage, опирается на идею контролируемого преобразования состояния между версиями схемы без потери совместимости и с минимальным временем простоя логики приложения. В условиях асинхронного API и возможного использования разных драйверов (IndexedDB, WebSQL, localStorage) особое значение приобретает предсказуемость переходов и детерминированность преобразований.
Базовым элементом любого миграционного процесса становится явное версионирование структуры данных. В localForage отсутствует встроенная схема, поэтому версия должна храниться отдельно — как служебный ключ.
Типовая практика — выделение отдельного мета-ключа:
import localforage from "localforage";
const SCHEMA_VERSION_KEY = "__schema_version__";
Версия хранится как число, увеличивающееся при каждом изменении структуры:
Получение версии:
async function getSchemaVersion() {
const version = await localforage.getItem(SCHEMA_VERSION_KEY);
return typeof version === "number" ? version : 0;
}
Установка версии:
async function setSchemaVersion(version) {
await localforage.setItem(SCHEMA_VERSION_KEY, version);
}
Миграции удобно представлять как последовательность функций, каждая из которых преобразует данные из одной версии в следующую. Такой подход исключает пропуск промежуточных состояний и делает процесс воспроизводимым.
const migrations = {
1: async () => {
// начальная структура, миграция не требуется
},
2: async () => {
const keys = await localforage.keys();
for (const key of keys) {
if (key === SCHEMA_VERSION_KEY) continue;
const value = await localforage.getItem(key);
if (value && typeof value === "object") {
value.createdAt = value.createdAt || Date.now();
await localforage.setItem(key, value);
}
}
},
3: async () => {
const keys = await localforage.keys();
for (const key of keys) {
if (key === SCHEMA_VERSION_KEY) continue;
const value = await localforage.getItem(key);
if (value?.type === "legacy") {
value.type = "modern";
value.flags = [];
await localforage.setItem(key, value);
}
}
}
};
Такой реестр обеспечивает линейное продвижение от одной версии к другой без ветвлений.
Основной алгоритм миграции строится как цикл от текущей версии к целевой:
async function migrateToLatest(latestVersion) {
let currentVersion = await getSchemaVersion();
while (currentVersion < latestVersion) {
const nextVersion = currentVersion + 1;
const migration = migrations[nextVersion];
if (!migration) {
throw new Error(`Migration for version ${nextVersion} not found`);
}
await migration();
currentVersion = nextVersion;
await setSchemaVersion(currentVersion);
}
}
Ключевой принцип — фиксация версии только после успешного завершения шага. Это предотвращает частично применённые миграции.
При инициализации приложения выполняется полный прогон миграций:
const LATEST_VERSION = 3;
export async function initStorage() {
await migrateToLatest(LATEST_VERSION);
}
Характеристика подхода:
Альтернативная стратегия — миграция отдельных записей при обращении к ним.
async function getEntity(key) {
const value = await localforage.getItem(key);
if (!value) return null;
if (value._version === 1) {
value.createdAt = Date.now();
value._version = 2;
await localforage.setItem(key, value);
}
return value;
}
Этот подход распределяет нагрузку во времени, но усложняет контроль консистентности.
На практике часто используется комбинация:
При работе с большим объёмом данных критично избегать блокирующих операций. Вместо обработки всего хранилища целиком применяется пакетная обработка.
async function migrateInBatches(batchSize = 50) {
const keys = await localforage.keys();
let batch = [];
for (const key of keys) {
if (key === SCHEMA_VERSION_KEY) continue;
batch.push(key);
if (batch.length >= batchSize) {
await processBatch(batch);
batch = [];
}
}
if (batch.length > 0) {
await processBatch(batch);
}
}
async function processBatch(keys) {
for (const key of keys) {
const value = await localforage.getItem(key);
if (!value) continue;
if (value.needsNormalization) {
value.needsNormalization = false;
await localforage.setItem(key, value);
}
}
}
Пакетирование снижает риск блокировки event loop и уменьшает пик нагрузки на IndexedDB.
В переходных версиях часто требуется поддержка двух форматов данных одновременно. Для этого используется стратегия двойной записи:
async function setEntity(key, value) {
const legacyValue = transformToLegacy(value);
const modernValue = transformToModern(value);
await Promise.all([
localforage.setItem(`${key}:v1`, legacyValue),
localforage.setItem(`${key}:v2`, modernValue)
]);
}
Позже старая форма постепенно выводится из эксплуатации.
Любая миграция должна учитывать частичную отказоустойчивость операций localForage. Ошибка на любом этапе требует предотвращения неконсистентного состояния.
Типовой подход:
async function safeMigration(stepFn) {
try {
await stepFn();
} catch (e) {
console.error("Migration failed:", e);
throw e;
}
}
В более строгих сценариях применяется откат логической версии:
await setSchemaVersion(currentVersion);
throw new Error("Migration aborted");
Однако физический откат данных в localForage часто невозможен без резервных копий.
Для сложных приложений вводится контекст миграции, фиксирующий текущее состояние процесса:
const migrationState = {
inProgress: false,
step: 0,
errors: []
};
Использование флагов позволяет:
При эволюции схемы неизбежны случаи устаревших или некорректных значений. Стратегия обработки включает дефолтирование:
function normalize(value) {
return {
id: value.id || crypto.randomUUID(),
createdAt: value.createdAt || Date.now(),
type: value.type || "unknown",
payload: value.payload || {}
};
}
Нормализация часто становится частью каждой миграции, а не отдельным этапом.
При значительном объёме данных применяется фоновая миграция с использованием отложенного выполнения:
function scheduleMigrationStep(fn) {
return new Promise(resolve => {
requestIdleCallback(async () => {
await fn();
resolve();
});
});
}
Это снижает нагрузку на UI-поток и делает процесс незаметным для пользователя.
Одним из наиболее сложных типов миграции является переименование ключей:
async function renameKeys(prefixOld, prefixNew) {
const keys = await localforage.keys();
for (const key of keys) {
if (key.startsWith(prefixOld)) {
const value = await localforage.getItem(key);
const newKey = key.replace(prefixOld, prefixNew);
await localforage.setItem(newKey, value);
await localforage.removeItem(key);
}
}
}
Такая операция требует строгого контроля, так как является потенциально разрушительной при сбое посередине выполнения.
Пошаговая миграция не ограничивается одноразовым процессом. Она превращается в постоянный механизм сопровождения состояния, при котором каждая новая версия данных:
Это формирует устойчивую модель, в которой localForage выступает не просто хранилищем ключ-значение, а эволюционирующей системой данных с контролируемой историей изменений.