Использование в Svelte: store с персистентностью

Библиотека idb-keyval предоставляет простой интерфейс для работы с IndexedDB, а реактивная система Svelte позволяет удобно управлять состоянием через сторы. Их сочетание даёт устойчивое решение для хранения данных с персистентностью без усложнения архитектуры.

Базовая идея персистентного стора

Обычный стор в Svelte создаётся через writable. Однако его состояние теряется при перезагрузке страницы. Персистентный стор расширяет стандартный, добавляя:

  • загрузку начального значения из IndexedDB
  • автоматическое сохранение при изменениях

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

Установка зависимостей

npm install idb-keyval

Импорт необходимых функций:

import { writable } from 'svelte/store';
import { get, set } from 'idb-keyval';

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

Персистентный стор реализуется как фабричная функция:

export function persistentStore(key, initialValue) {
    const store = writable(initialValue);

    // загрузка из IndexedDB
    get(key).then(value => {
        if (value !== undefined) {
            store.set(value);
        }
    });

    // подписка на изменения
    store.subscribe(value => {
        set(key, value);
    });

    return store;
}

Ключевые аспекты

  • key — уникальный идентификатор в IndexedDB
  • initialValue используется до загрузки данных
  • асинхронная загрузка не блокирует инициализацию стора

Использование в компонентах

import { persistentStore } from './stores';

export const userSettings = persistentStore('settings', {
    theme: 'light',
    language: 'ru'
});

В Svelte-компоненте:

<script>
    import { userSettings } from './stores';
</script>

<sel ect bind:value={$userSettings.theme}>
    <option value="light">Светлая</option>
    <option value="dark">Тёмная</option>
</select>

Изменение автоматически сохраняется в IndexedDB.

Управление асинхронной инициализацией

Проблема: значение загружается асинхронно, и компонент сначала получает initialValue.

Решение — добавить флаг загрузки:

export function persistentStore(key, initialValue) {
    const { subscribe, set: internalSet } = writable({
        data: initialValue,
        loaded: false
    });

    get(key).then(value => {
        internalSet({
            data: value ?? initialValue,
            loaded: true
        });
    });

    return {
        subscribe,
        set(value) {
            internalSet({ data: value, loaded: true });
            set(key, value);
        }
    };
}

Использование:

{#if $store.loaded}
    <p>{$store.data}</p>
{:else}
    <p>Загрузка...</p>
{/if}

Двусторонняя синхронизация

При наличии нескольких вкладок браузера возникает задача синхронизации состояния.

idb-keyval не предоставляет события изменений, поэтому возможны подходы:

1. BroadcastChannel

const channel = new BroadcastChannel('store-sync');

store.subscribe(value => {
    set(key, value);
    channel.postMessage(value);
});

channel.onmess age = (event) => {
    store.set(event.data);
};

2. Storage Event (fallback)

Менее надёжный, но возможный способ — использование localStorage как триггера.

Оптимизация записи

Частые изменения могут привести к избыточным операциям записи.

Дебаунс

import { debounce } fr om 'lodash-es';

const save = debounce(value => set(key, value), 300);

store.subscribe(value => {
    save(value);
});

Проверка изменений

let lastValue;

store.subscribe(value => {
    if (JSON.stringify(value) !== JSON.stringify(lastValue)) {
        lastValue = value;
        set(key, value);
    }
});

Работа с вложенными структурами

При хранении сложных объектов важно учитывать:

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

Пример:

persistentStore('cart', {
    items: [],
    total: 0
});

Изменения вложенных свойств требуют замены объекта:

cart.update(state => {
    return {
        ...state,
        items: [...state.items, newItem]
    };
});

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

Операции IndexedDB могут завершаться с ошибками:

get(key)
    .then(value => {
        if (value !== undefined) store.set(value);
    })
    .catch(err => {
        console.error('Ошибка загрузки:', err);
    });

store.subscribe(value => {
    set(key, value).catch(err => {
        console.error('Ошибка сохранения:', err);
    });
});

Очистка данных

Удаление значения:

import { del } from 'idb-keyval';

del('settings');

Сброс стора:

store.set(initialValue);

Множественные сторы и namespace

Для избежания конфликтов ключей используется соглашение:

persistentStore('app:user');
persistentStore('app:settings');
persistentStore('app:cache');

Либо динамическое формирование:

const createKey = (module, name) => `${module}:${name}`;

Кастомизация базы данных

idb-keyval позволяет использовать собственный store:

import { Store } from 'idb-keyval';

const customStore = new Store('app-db', 'app-store');

set('key', value, customStore);
get('key', customStore);

Интеграция:

export function persistentStore(key, initialValue, db = customStore) {
    const store = writable(initialValue);

    get(key, db).then(value => {
        if (value !== undefined) store.set(value);
    });

    store.subscribe(value => {
        set(key, value, db);
    });

    return store;
}

Lazy-загрузка стора

Иногда требуется отложенная инициализация:

let store;

export function getStore() {
    if (!store) {
        store = persistentStore('lazy', {});
    }
    return store;
}

Использование с derived store

Можно строить производные сторы:

import { derived } from 'svelte/store';

export const user = persistentStore('user', null);

export const isLoggedIn = derived(user, $user => !!$user);

Частичная персистентность

Не всегда требуется сохранять весь стор:

store.subscribe(value => {
    const { tempData, ...persisted } = value;
    set(key, persisted);
});

Версионирование данных

При изменении структуры данных:

const VERSION = 2;

get(key).then(value => {
    if (!value || value.version !== VERSION) {
        store.set({ ...initialValue, version: VERSION });
    } else {
        store.set(value);
    }
});

Производительность и ограничения

  • IndexedDB асинхронна — не блокирует UI
  • операции медленнее, чем работа с памятью
  • большие объёмы данных требуют аккуратного управления

Оптимизации:

  • минимизация частоты записи
  • хранение только необходимых данных
  • использование батч-операций при необходимости

Итоговая архитектура

Персистентный стор на базе idb-keyval в Svelte обеспечивает:

  • реактивность интерфейса
  • автоматическое сохранение состояния
  • устойчивость к перезагрузкам
  • расширяемость (синхронизация, версии, кастомные базы)

Такой подход особенно полезен для:

  • пользовательских настроек
  • кэша API
  • оффлайн-данных
  • промежуточного состояния форм