Skip to content

Декоратор withCache

Эта утилита кэширования (декоратор) перехватывает вызовы fetcher. Он проверяет наличие данных в памяти и следит за тем, чтобы они не устарели по времени.

Как использовать с engine.resource и двумя сигналами

Теперь просто оберните ваш fetcher в функцию withCache. Всё остальное взаимодействие с сигналами остаётся прежним.

ts
import { engine } from './yourEngineInstance';
import { withCache } from './withCache';

// 1. Создаем исходные сигналы
const userIdSignal = engine.signal(1, 'userId');
const tabSignal = engine.signal<'posts' | 'photos'>('posts', 'tab');

// 2. Объединяем их в вычисляемый массив зависимостей
const userTabDeps = engine.computed(() => {
  return [userIdSignal.value, tabSignal.value] as const;
});

// 3. Создаем ресурс с автоматическим кэшем на 30 секунд
export const cachedUserResource = engine.resource(
  withCache(
    async ([userId, tab], abortSignal) => {
      console.log(`📡 Делаем реальный сетевой запрос для User: ${userId}, Tab: ${tab}`);
      const res = await fetch(`https://typicode.com{userId}/${tab}`, {
        signal: abortSignal,
      });
      if (!res.ok) throw new Error('Ошибка загрузки данных');
      return res.json();
    },
    { ttl: 30 * 1000 } // Настройка времени жизни кэша: 30 секунд (в миллисекундах)
  ),
  userTabDeps,
  'cachedUserResource'
);

Как это работает на практике (Логика поведения)

  1. Первый выбор:
  • userIdSignal.value = 1, tabSignal.value = 'posts'.
  • Результат: Кэш пуст. В консоли появится лог запроса. Данные сохранятся в кэш под ключом "[1,\"posts\"]".
  1. Переключение:
  • Разработчик меняет вкладку на 'photos'.
  • Результат: Кэш для новой комбинации пуст. Происходит реальный запрос. Данные кэшируются под ключом "[1,\"photos\"]".
  1. Возврат назад (в течение 30 сек):
  • Разработчик возвращает вкладку на 'posts'.
  • Результат: withCache видит ключ "[1,\"posts\"]", проверяет, что 30 секунд еще не прошло, и мгновенно возвращает данные из памяти без вызова сети.
  1. Возврат назад (спустя 40 сек):
  • Разработчик снова открывает вкладку 'posts'.
  • Результат: Время жизни (TTL) истекло. Функция удаляет старый кэш, выполняет новый сетевой запрос и обновляет таймер.

Тестирование

Для тестирования декоратора кэширования withCache отлично подойдут фейковые таймеры Vitest (vi.useFakeTimers). Они позволяют мгновенно перемещаться во времени вперед, чтобы проверять истечение срока жизни кэша (TTL), не дожидаясь реальных секунд на часах.

🔍 Что проверяют эти тесты:

  1. Базовое кэширование: Проверяется, что при повторном вызове функции с одинаковым source оригинальный асинхронный метод не вызывается дважды.
  2. Изоляция ключей: Проверяется сериализация JSON.stringify(source). Объекты с разными вкладками или ID не пересекаются в памяти кэша.
  3. Фейковое время (vi.advanceTimersByTimeAsync): Имитирует старение кэша. Проверяется, что ровно в момент now - timestamp >= ttl утилита очищает старую запись и запрашивает свежие данные.

Практические кейсы применения декоратора withCache

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

В отличие от дебаунса и троттлинга, которые управляют временной частотой вызовов (Time-based), кэширование управляет хранением данных (Data-based). Если для конкретного ключа зависимостей (source) в оперативной памяти Map уже лежит свежий ответ, декоратор возвращает его за 0 миллисекунд, полностью отменяя сетевую активность.


1. Многократные переключения между вкладками или табами (Tab Switching / Navigation)

  • Проблема без декоратора: В интерфейсе личного кабинета есть табы: «Профиль», «Настройки безопасности» и «История заказов». При клике на вкладку «История заказов» приложение делает запрос к API и рендерит список. Пользователь переключился на секунду в «Профиль», а затем вернулся обратно в «Историю заказов». Без кэширования приложение снова покажет спиннер загрузки и заставит сервер заново собирать из базы данных тот же самый список заказов. Это создаёт ощущение медленного и «дерганого» интерфейса.
  • Решение с Cache: Вы выставляете время жизни кэша (ttl: 60000 — 1 минута). Когда пользователь перемещается по табам, данные для каждой вкладки скачиваются строго один раз. Повторные клики мгновенно извлекают буфер из памяти, обеспечивая мгновенный, бесшовный отклик интерфейса (UX уровня десктопного приложения).

2. Загрузка статических справочников и конфигураций (Dictionaries / Static Data)

  • Проблема: В приложении есть множество независимых форм или выпадающих списков, которым требуются справочные данные с сервера (например: список стран и городов, категории товаров, валюты, часовые пояса или системные настройки прав). Если каждый раз при открытии модального окна или рендере поля ввода слать запрос за списком стран — сервер будет тратить ресурсы на отдачу одних и тех же статичных массивов данных.
  • Решение с Cache: Настраивается большой TTL кэша (например, несколько часов или дней, либо до перезагрузки вкладки). Первый открывшийся компонент скачивает справочник из сети, а все последующие формы и компоненты в приложении лениво берут его из готового кэша декоратора.

3. Пагинация и списки с фильтрами (Pagination / Smart Filtering)

  • Проблема: Пользователь ищет товары в каталоге, переходит на 2-ю страницу, затем на 3-ю, а затем решает вернуться на 1-ю страницу, чтобы перепроверить первый товар. Без кэширования каждый шаг назад по страницам пагинации будет заново инициировать fetch-запросы, гонять трафик и заставлять базу данных выполнять повторные тяжелые операции сортировки (OFFSET / LIMIT).
  • Решение с Cache: Декоратор сериализует номер страницы и параметры фильтров в уникальный ключ. При пролистывании страниц назад данные мгновенно подставляются из кэша, исключая моргание интерфейса и лишнюю нагрузку на серверную инфраструктуру.

4. Глобальный стейт-шеринг между изолированными компонентами (Data Sharing via DI)

  • Проблема: На сложной аналитической дашборд-панели находятся 5 разных независимых виджетов (график, круговая диаграмма, мини-таблица и т.д.), и всем им для рендеринга требуются одни и те же агрегированные финансовые метрики за текущий месяц. Если каждый виджет при монтировании независимо вызовет свой метод загрузки, клиент отправит 5 идентичных параллельных запросов к бэкенду.
  • Решение с Cache: Все 5 виджетов обращаются к единому реактивному ресурсу, обёрнутому в withCache. Первый выполнившийся запрос скачает данные и положит их в Map, а остальные 4 виджета мгновенно получат этот же готовый объект из памяти, предотвращая избыточный сетевой оверхед.

Сводная шпаргалка по декораторам ресурсов:

  • withCache — Нужен, когда данные редко меняются, и мы хотим полностью исключить повторные сетевые запросы при возвращении к прежним параметрам (пример: переключение табов, пагинация назад, статичные справочники).
  • withDebounce — Нужен, когда важен только финальный результат после того, как пользователь затих (пример: валидация формы при вводе email).
  • withThrottle — Нужен, когда важен процесс в динамике, но порциями (пример: плавное рисование на холсте, анимация куба Three.js при ресайзе).
  • withThrottleAndCache — Нужен, когда важен процесс в динамике, но данные внутри этого процесса имеют свойство повторяться на коротком промежутке времени (пример: скролл, перемещение карт, живой поиск).