Перейти к содержанию

Кеш чтения смарт-контракта: ключ и актуальность

Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 15 из 30.

Несколько компонентов страницы могут читать один курс. Если каждый делает отдельный запрос, RPC получает лишнюю нагрузку, а интерфейс иногда показывает ответы разных поколений. В этом уроке введём ключ кеша и разделим повторное использование наблюдения с его актуальностью. Результат — одна запись кеша соответствует строго определённому чтению.

Предполагается согласованный снимок CourseCatalog. Базовый курс использует DOM и viem 2.37.9; кеш пока реализуем небольшим модулем, чтобы понять смысл до подключения React и TanStack Query. Программы и сетевые запросы не запускались. Обсуждаемые интервалы свежести являются учебными примерами политики, а не рекомендованными параметрами для любого production-приложения.

Ключ описывает смысл запроса

Номер 17 недостаточен для ключа. В другом экземпляре тот же номер означает другую запись. Поэтому ключ включает поколение окружения, сеть, адрес реестра, метод и аргумент. Если чтение закреплено за историческим блоком, номер или хеш этого блока тоже входит в ключ. Ответ latest и ответ блока N имеют разную семантику.

Аккаунт не нужен для публичного courses, потому что результат этого getter не зависит от отправителя. Для персонального запроса, который зависит от аккаунта, он становится частью ключа. Добавлять адрес во все ключи без разбора не обязательно: это уменьшает переиспользование и может раскрывать лишнюю связь данных. Исключать его из персонального чтения, напротив, приводит к показу чужого состояния.

Адрес нормализуется заранее, номер курса представляется канонической десятичной строкой. Иначе 17, 017 и 17n могут создать разные записи для одного запроса. Кеш не должен исправлять произвольный невалидный вход; эту задачу выполняет проверка формы до создания ключа.

Минимальный кеш снимков

Создадим snapshot-cache.js. Он объединяет одновременно начатые одинаковые чтения, сохраняет успешный ответ и удаляет неуспешную попытку из списка активных запросов.

export function snapshotKey(deployment, courseId) {
  return [deployment.environmentId, deployment.chainId,
    deployment.catalogAddress.toLowerCase(), 'courses', courseId.toString()].join(':');
}

export function createSnapshotCache() {
  const values = new Map();
  const pending = new Map();
  return {
    get(key) { return values.get(key) ?? null; },
    async load(key, read) {
      if (pending.has(key)) return pending.get(key);
      const task = Promise.resolve().then(read).then(value => {
        values.set(key, { value, savedAt: Date.now() });
        return value;
      }).finally(() => pending.delete(key));
      pending.set(key, task);
      return task;
    },
    invalidate(key) { values.delete(key); },
  };
}

Метод get возвращает последнее сохранённое наблюдение, а load явно выполняет новое чтение или присоединяется к уже активному. Мы не объявляем любую запись свежей навсегда. Вызывающий контроллер решает, можно ли показать кеш сразу, нужно ли обновление и как обозначить возраст значения.

finally удаляет активную попытку и при успехе, и при ошибке. Иначе отклонённый Promise мог бы навсегда остаться в pending, и каждое повторное действие возвращало бы прежний отказ без нового запроса. Это обычная ошибка кеширования асинхронной работы, которая особенно заметна после краткого сбоя RPC.

Инвалидация не отменяет ожидание

Минимальный модуль имеет ограничение: invalidate удаляет сохранённое значение, но не запрещает уже активному запросу снова его записать. Для смены экземпляра ключ изменится, поэтому старый ответ остаётся в старой записи и не заменяет новый. Для принудительной инвалидации того же ключа нужен номер поколения кеша, который проверяется перед сохранением.

Такую защиту стоит добавить, если обновление вызывается событием публикации. Пока старое чтение завершается, контракт уже изменился. Ответ старого запроса нельзя считать новым снимком только потому, что он пришёл позднее события. Номер наблюдаемого блока помогает сравнить происхождение, а поколение управляет тем, какую попытку приложение считает актуальной.

Кешированный снимок не является источником истины для подписи. Перед записью выполняется новое моделирование с текущим контекстом. Видимая карточка может помочь человеку сформировать намерение, но между показом и исполнением состояние способно измениться. Поэтому интерфейс сохраняет разницу между наблюдаемыми данными и подготовленным вызовом.

Возраст и политика обновления

Date.now сообщает локальное время сохранения. Возраст в миллисекундах удобен для пользовательской подписи, но не доказывает свежесть сети. RPC может вернуть старый блок, а часы устройства — быть неправильными. У карточки сохраняются номер и хеш блока, чтобы возраст кеша не стал единственным критерием актуальности.

Для публичного просмотра допустимо показать прежнюю запись сразу и обновить её в фоне. Для решения о деактивации пользователь должен видеть, какой выпуск сейчас предполагается изменить. Эти сценарии требуют разного уровня осторожности. Один глобальный интервал свежести на всё приложение обычно плохо отражает смысл операций.

В фоновой вкладке обновление можно делать реже. При возвращении фокуса выполняется новое чтение, если это соответствует политике. Переход между вкладками не отправляет транзакций и не открывает кошелёк. Автоматизация чтения не должна незаметно превратиться в автоматизацию согласия.

Ограничение объёма кеша

Каталог может содержать тысячи курсов. Карта без ограничения будет расти по мере переходов посетителя. В реальном клиенте задают максимальное число записей или удаление давно неиспользуемых ключей. Исторические снимки можно хранить отдельно от текущих, потому что они имеют другую цель и обычно не нуждаются в постоянном обновлении.

Локальное сохранение кеша между загрузками требует версии схемы и проверки происхождения. Старый environmentId после сброса лаборатории не должен совпадать с новой цепочкой. Пользователь может очистить браузерное хранилище, поэтому оно не заменяет журнал развёртывания или серверную историю событий.

Для упражнения задайте одновременные чтения двух одинаковых ключей и одного другого экземпляра. Ожидается один активный запрос для одинакового чтения и независимый запрос для другого реестра. Затем добавьте ошибку первой попытки: новый вызов должен получить возможность обратиться к RPC заново. В следующем шаге эти наблюдения станут входом формы публикации. Принципы query key развиваются в документации TanStack Query.

Ошибка кеша и ошибка чтения

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

Кеш не должен становиться единственным владельцем черновика публикации. Его очистка может произойти при смене экземпляра или политике объёма, а текст формы остаётся намерением пользователя. Разделение хранилищ предотвращает неожиданную потерю введённого отпечатка при обычном обновлении чтения. В последующих hooks эта же граница проходит между query data и локальным состоянием формы.

Оглавление серии