Паттерн проверки окружения перед использованием

Почему проверка окружения критична

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

Типовые проблемные сценарии:

  • серверный рендеринг (SSR), где window и document отсутствуют
  • тестовые окружения Node.js без полифилов DOM
  • старые браузеры без IndexedDB
  • приватные режимы браузеров с ограничениями на storage API
  • строгие CSP или корпоративные политики, блокирующие IndexedDB

Корректная архитектура работы с localForage начинается не с вызова методов setItem/getItem, а с определения доступности среды и выбора стратегии инициализации.


Базовая идея паттерна окружения

Паттерн проверки окружения заключается в разделении логики на два слоя:

  1. Environment layer — определяет, можно ли использовать localForage
  2. Storage layer — выполняет операции только при гарантированной поддержке

Такой подход исключает:

  • падения при SSR
  • race conditions при инициализации
  • непредсказуемый fallback на localStorage

Проверка наличия браузерного окружения

Первый и самый простой шаг — проверка существования window:

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

Эта проверка решает только проблему SSR и Node.js, но не гарантирует доступность IndexedDB.

Более строгая версия:

const isBrowser = () =>
  typeof window !== 'undefined' &&
  typeof window.indexedDB !== 'undefined';

Однако даже наличие indexedDB не гарантирует корректную работу (например, Safari в приватном режиме).


Проверка поддержки storage API

localForage предоставляет встроенную возможность проверки поддержки через driver() и setDriver().

Использование driver()

import localforage from "localforage";

async function isStorageAvailable() {
  try {
    await localforage.setItem("__test__", "ok");
    await localforage.removeItem("__test__");
    return true;
  } catch (e) {
    return false;
  }
}

Этот подход проверяет реальную работоспособность, а не только наличие API.


Проверка через setDriver

localForage позволяет явно задать драйвер:

  • localforage.INDEXEDDB
  • localforage.WEBSQL
  • localforage.LOCALSTORAGE

Паттерн устойчивой инициализации:

import localforage from "localforage";

async function initStorage() {
  if (typeof window === "undefined") {
    return null;
  }

  try {
    await localforage.setDriver([
      localforage.INDEXEDDB,
      localforage.WEBSQL,
      localforage.LOCALSTORAGE
    ]);

    return localforage;
  } catch (e) {
    return null;
  }
}

Здесь реализуется принцип деградации: от IndexedDB к более простым механизмам.


SSR-safe инициализация

В архитектуре SSR (Next.js, Nuxt, Remix) важно не импортировать и не инициализировать storage на сервере.

Паттерн ленивой загрузки:

let lfInstance = null;

export async function getStorage() {
  if (typeof window === "undefined") {
    return null;
  }

  if (lfInstance) {
    return lfInstance;
  }

  const localforage = (await import("localforage")).default;

  await localforage.setDriver([
    localforage.INDEXEDDB,
    localforage.LOCALSTORAGE
  ]);

  lfInstance = localforage;
  return lfInstance;
}

Ключевая идея — динамический import только на клиенте.


Абстракция безопасного доступа

Чтобы исключить повторение проверок, строится слой-обёртка:

export class SafeStorage {
  constructor(instance) {
    this.instance = instance;
  }

  static async create() {
    const lf = await getStorage();
    return new SafeStorage(lf);
  }

  async set(key, value) {
    if (!this.instance) return null;
    return this.instance.setItem(key, value);
  }

  async get(key) {
    if (!this.instance) return null;
    return this.instance.getItem(key);
  }

  async remove(key) {
    if (!this.instance) return null;
    return this.instance.removeItem(key);
  }
}

Такой слой гарантирует отсутствие runtime-ошибок при деградации окружения.


Проверка через feature detection вместо user-agent

Антипаттерн:

if (navigator.userAgent.includes("Safari")) { ... }

Корректный подход:

async function supportsLocalForage() {
  if (typeof window === "undefined") return false;

  try {
    const testKey = "__lf_test__";
    await localforage.setItem(testKey, "1");
    await localforage.removeItem(testKey);
    return true;
  } catch {
    return false;
  }
}

Feature detection всегда предпочтительнее эвристик по браузеру.


Паттерн graceful degradation

Если storage недоступен, система должна переключаться на:

  • in-memory cache
  • временный Map
  • отключение persistence

Пример fallback:

class MemoryStorage {
  constructor() {
    this.store = new Map();
  }

  async setItem(key, value) {
    this.store.set(key, value);
  }

  async getItem(key) {
    return this.store.get(key) ?? null;
  }

  async removeItem(key) {
    this.store.delete(key);
  }
}

Интеграция с localForage:

let storage;

export async function init() {
  const lf = await getStorage();

  if (lf) {
    storage = lf;
  } else {
    storage = new MemoryStorage();
  }

  return storage;
}

Проверка quota и ограничений браузера

Даже при наличии IndexedDB возможны ошибки quota exceeded.

Паттерн предварительной проверки:

async function canWrite() {
  try {
    const key = "__quota_test__";
    const payload = new Array(1000).fill("x").join("");

    await localforage.setItem(key, payload);
    await localforage.removeItem(key);

    return true;
  } catch (e) {
    return false;
  }
}

Ленивый доступ к storage как стандарт

Жёсткая инициализация на уровне модуля приводит к ошибкам:

// плохо в SSR
const lf = localforage.createInstance({...});

Корректный подход — lazy factory:

export function createStorage() {
  return {
    async getInstance() {
      if (typeof window === "undefined") return null;

      const lf = await import("localforage");
      return lf.default;
    }
  };
}

Инициализация с учетом multiple drivers

Приоритет драйверов должен задаваться явно:

await localforage.setDriver([
  localforage.INDEXEDDB,
  localforage.WEBSQL,
  localforage.LOCALSTORAGE
]);

Паттерн окружения должен учитывать:

  • IndexedDB как основной вариант
  • WebSQL только как legacy fallback
  • localStorage как последний резерв

Централизованный guard для всего приложения

Оптимальная архитектура:

let readyPromise = null;

export function getReadyStorage() {
  if (!readyPromise) {
    readyPromise = (async () => {
      if (typeof window === "undefined") return null;

      const lf = (await import("localforage")).default;

      try {
        await lf.setDriver([
          lf.INDEXEDDB,
          lf.LOCALSTORAGE
        ]);

        return lf;
      } catch {
        return null;
      }
    })();
  }

  return readyPromise;
}

Такой подход:

  • предотвращает повторную инициализацию
  • защищает от race conditions
  • гарантирует единое состояние storage слоя

Итоговая архитектурная модель паттерна

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

  1. Runtime detection

    • typeof window
    • наличие IndexedDB
  2. Capability detection

    • тестовая запись/чтение
    • проверка driver fallback
  3. Operational safety layer

    • lazy initialization
    • singleton instance
    • memory fallback

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