Инициализация хранилища при старте приложения

Инициализация клиентского хранилища через localForage при старте приложения определяет стабильность работы всей слоя персистентности: от загрузки пользовательских настроек до восстановления кэша данных, синхронизационных очередей и офлайн-состояния. Ошибки на этом этапе часто проявляются не сразу, а в виде «плавающих» дефектов, связанных с недоступностью IndexedDB, падением на Safari Private Mode или некорректным выбором драйвера.


Базовая проблема заключается в том, что localForage работает поверх нескольких backend-хранилищ (IndexedDB, WebSQL, localStorage), и выбор конкретного драйвера происходит асинхронно. Это означает, что обращение к хранилищу до завершения инициализации может привести к неконсистентному состоянию.

На практике выделяются три стратегии:

  • Синхронная конфигурация до старта приложения
  • Асинхронная инициализация перед рендером UI
  • Ленивая инициализация при первом обращении

Базовая конфигурация перед запуском приложения

Наиболее распространённый подход — настройка глобального экземпляра до монтирования приложения.

import localForage from "localforage";

localForage.config({
  name: "myApp",
  storeName: "app_storage",
  version: 1.0,
  description: "Main persistent storage"
});

Эта конфигурация должна выполняться максимально рано: до подключения сервисов, стора и UI-слоя. Важно понимать, что конфигурация не блокирует выполнение кода — она лишь задаёт параметры будущего подключения к backend.


Явный выбор драйверов

Для предсказуемости поведения часто фиксируют порядок предпочтений драйверов:

import localForage from "localforage";

localForage.setDriver([
  localForage.INDEXEDDB,
  localForage.WEBSQL,
  localForage.LOCALSTORAGE
]);

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


Ожидание готовности хранилища

Ключевой момент инициализации — подтверждение того, что backend выбран и готов к операциям.

import localForage from "localforage";

async function initStorage() {
  await localForage.ready();
  return true;
}

Метод ready() гарантирует, что:

  • выбран драйвер;
  • выполнена инициализация внутреннего адаптера;
  • доступ к storage API подтверждён.

Игнорирование этого шага приводит к состояниям гонки между UI и storage-слоем.


Централизованный bootstrap слоя хранения

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

import localForage from "localforage";

let storageReadyPromise = null;

export function initStorage() {
  if (!storageReadyPromise) {
    storageReadyPromise = localForage.ready();
  }
  return storageReadyPromise;
}

Такой подход предотвращает повторную инициализацию и гарантирует единый lifecycle хранилища во всём приложении.


Singleton-обёртка над localForage

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

import localForage from "localforage";

class StorageService {
  constructor() {
    this.readyPromise = null;
  }

  init() {
    if (!this.readyPromise) {
      this.readyPromise = localForage.ready();
    }
    return this.readyPromise;
  }

  getItem(key) {
    return localForage.getItem(key);
  }

  setItem(key, value) {
    return localForage.setItem(key, value);
  }
}

export const storage = new StorageService();

Такой слой позволяет:

  • централизовать контроль инициализации;
  • подменять backend в тестах;
  • добавлять миграции и логирование без изменения бизнес-кода.

Инициализация в SPA-архитектуре

В одностраничных приложениях критично разделять этапы:

  1. создание runtime окружения;
  2. инициализация storage;
  3. загрузка состояния приложения;
  4. рендер UI.

Пример последовательности:

import { initStorage } from "./storage";
import { hydrateStore } from "./store";

async function bootstrap() {
  await initStorage();
  await hydrateStore();
  startApp();
}

bootstrap();

Любая попытка рендера до завершения initStorage() создаёт риск несогласованного состояния между UI и persisted state.


Интеграция с состоянием приложения

При использовании Redux-подобных или Vuex-подобных систем storage инициализируется до восстановления state:

await localForage.ready();

const savedState = await localForage.getItem("app_state");

store.replaceState(savedState || initialState);

Важно, что восстановление состояния не должно происходить до гарантированной готовности backend, иначе возможны частичные чтения или null-результаты из-за неинициализированного драйвера.


Защита от SSR и окружений без window

localForage зависит от браузерных API, поэтому при SSR необходимо разделять инициализацию:

const isBrowser = typeof window !== "undefined";

export async function initStorageSafe() {
  if (!isBrowser) return false;
  await localForage.ready();
  return true;
}

Это предотвращает попытки обращения к IndexedDB в Node.js-окружении, где соответствующие API отсутствуют.


Миграция версий хранилища при старте

Инициализация часто включает проверку версии схемы данных:

async function migrate() {
  const version = await localForage.getItem("schema_version");

  if (!version) {
    await localForage.setItem("schema_version", 1);
  }

  if (version === 1) {
    const oldData = await localForage.getItem("cache");
    await localForage.setItem("cache_v2", transform(oldData));
    await localForage.removeItem("cache");
    await localForage.setItem("schema_version", 2);
  }
}

Такая логика должна выполняться строго после ready(), иначе возможна потеря данных из-за обращения к неинициализированному backend.


Параллельная инициализация с другими сервисами

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

await Promise.all([
  localForage.ready(),
  initApiClient(),
  loadConfig()
]);

Однако такой подход требует гарантии, что ни один сервис не обращается к storage до завершения общего Promise. Нарушение этого правила приводит к race condition на уровне приложения.


Повторная инициализация и её предотвращение

localForage не предназначен для повторного «пересоздания» после старта приложения. Однако архитектурные ошибки часто приводят к повторным вызовам конфигурации.

Защита реализуется через модульный singleton:

let initialized = false;

export async function safeInit() {
  if (initialized) return;

  await localForage.ready();
  initialized = true;
}

Логирование состояния инициализации

Для диагностики часто добавляют контрольные точки:

async function initStorageWithLogs() {
  console.log("storage:init:start");

  await localForage.ready();

  console.log("storage:init:ready");

  const driver = localForage.driver();

  console.log("storage:driver:", driver);
}

Это позволяет выявлять случаи fallback на localStorage или неожиданный выбор WebSQL в старых браузерах.


Инициализация в многомодульных приложениях

При наличии нескольких независимых модулей (например, кэш, офлайн-очередь, настройки) важно избегать создания нескольких экземпляров:

import localForage from "localforage";

const cacheStore = localForage.createInstance({
  name: "myApp",
  storeName: "cache"
});

const settingsStore = localForage.createInstance({
  name: "myApp",
  storeName: "settings"
});

Каждый экземпляр требует собственной инициализации ready(), что необходимо учитывать при старте приложения:

await Promise.all([
  cacheStore.ready(),
  settingsStore.ready()
]);

Ошибки раннего доступа и их природа

Типичная проблема инициализации — обращение к storage до завершения bootstrap:

  • чтение null вместо ожидаемого объекта;
  • падение при сериализации undefined;
  • некорректный fallback на localStorage;
  • потеря данных при параллельной записи.

Все эти ошибки не связаны с самим API localForage напрямую, а являются следствием неправильного порядка инициализации в приложении.