Web3: пагинация истории публикаций
Оглавление курса «Web3-интерфейс: реестр в браузере» · Урок 25 из 30.
Карточка показывает текущий выпуск, а история объясняет последовательность публикаций и деактиваций. Для короткой локальной лаборатории можно получить ограниченный диапазон событий и разбить его на страницы в браузере. В этом уроке зададим порядок и стабильные ключи строк. Результат — посетитель видит историю курса 17 без попытки выгрузить всю сеть одним запросом.
Предполагается ABI событий и известный блок развёртывания. Все чтения истории в тексте ожидаемые: программа не запускалась. Для большого production-каталога понадобится индексатор и серверная пагинация, которые рассматриваются в следующем курсе. Наш браузерный пример специально ограничен объёмом лаборатории.
Диапазон является частью запроса
Не следует запрашивать от нулевого блока до бесконечного будущего при каждом открытии страницы. Адрес реестра существует начиная с собственного развёртывания, а endpoint может ограничивать число журналов и диапазон. Сохраняем fromBlock из контролируемой записи экземпляра и явный toBlock текущего наблюдения.
События двух типов сортируются вместе по номеру блока и позиции журнала. В пределах одной транзакции несколько журналов имеют определённый порядок. В пределах блока также существует порядок транзакций, который отражается в позиции лога. Использование только времени блока не различает эти строки и создаёт нестабильное представление.
Хеш транзакции недостаточен для идентификатора строки: одна транзакция может испустить несколько событий. Ключ включает сеть, экземпляр, хеш блока, хеш транзакции и logIndex. Хеш блока позволяет отличить наблюдения при смене ветви. Для окончательной канонической истории индексатор отдельно управляет реорганизацией.
Ограниченное чтение
Ниже функция получает две группы событий и объединяет их. Входной диапазон заранее ограничивается политикой интерфейса. Показанный код не обещает обработку миллионов блоков или неограниченного ответа RPC.
export async function readCourseHistory(address, courseId, fromBlock, toBlock) {
if (toBlock < fromBlock || toBlock - fromBlock > 500n) {
throw new Error('Для лаборатории выберите диапазон до 500 блоков');
}
const options = { address, abi: catalogAbi, args: { courseId }, fromBlock, toBlock };
const groups = await Promise.all([
publicClient.getContractEvents({ ...options, eventName: 'Published' }),
publicClient.getContractEvents({ ...options, eventName: 'Deactivated' }),
]);
return groups.flat().sort((a, b) => {
if (a.blockNumber !== b.blockNumber) return a.blockNumber < b.blockNumber ? -1 : 1;
return a.logIndex - b.logIndex;
});
}
Импорты publicClient и catalogAbi берутся из уже созданных модулей. Число 500 является локальным пределом упражнения, а не гарантированным лимитом какого-либо публичного сервиса. Даже меньший диапазон может содержать слишком много событий; endpoint имеет собственные ограничения размера ответа.
Логи ожидаются включёнными в известные блоки выбранного диапазона. Если транспорт возвращает неполные поля или неподходящую схему, адаптер не продолжает сортировку через произвольные null. На границе чтения проверяются обязательные данные. Неопределённый результат не должен превращаться в красивый, но выдуманный порядок истории.
Пагинация короткого набора
После получения небольшого набора можно показывать десять строк на страницу. Это пагинация уже загруженной истории, а не уменьшение RPC-запроса. Пользователь должен понимать, что диапазон ограничен. Подпись «События блоков X–Y» точнее универсального «вся история».
При появлении новых событий первая страница может измениться. Если человек читает старые строки, автоматический переход к новому началу мешает работе. Можно закрепить снимок списка и показать кнопку «Есть новые события». Номер страницы относится к этому снимку, а не к постоянно изменяющемуся массиву.
Сортировка по убыванию полезна для последних действий, но внутренний порядок проекции обычно строится по возрастанию. Не следует вычислять текущее состояние, просто взяв первую строку произвольного списка. Deactivated не содержит отпечатка, поэтому восстановление записи требует учитывать предыдущую публикацию или читать getter.
Значение события для читателя
Строка публикации показывает новый отпечаток. Строка деактивации показывает факт смены активности, но не придумывает отсутствующий параметр причины. Нельзя приписать ей «материал удалён за ошибку», если такая причина хранится только во внешней редакционной системе и не была получена приложением.
В строке можно дать ссылку на подробности транзакции. Для локальной сети публичный explorer может отсутствовать, поэтому достаточно скопировать хеш и показать блок. Если приложение поддерживает внешнюю сеть, URL explorer берётся из контролируемой сетевой конфигурации, а не формируется из пользовательской строки.
Поля события имеют публичный характер, но интерфейс всё равно применяет безопасное отображение. Имена методов и отпечатки выводятся текстом. События стороннего контракта с тем же ABI не добавляются в список автоматически: источник ограничен выбранным адресом и сетью.
Реорганизация и обновление диапазона
Если блок из показанного диапазона перестал быть каноническим, старые строки могут больше не описывать текущую историю. Небольшой браузерный пример может перечитать весь свой ограниченный диапазон и заменить снимок после проверки происхождения. Для большого набора такой подход слишком дорог и нужен курсор индексатора.
Повторное чтение не должно дублировать прежние события. Объединение выполняется по стабильному ключу наблюдения, а при смене blockHash каноничность рассматривается отдельно. Простое добавление новых строк в массив по времени получения приведёт к дублям и неправильной истории после восстановления связи.
Переход к серверной истории
Браузерный предел полезен для обучения, потому что показывает запрос и порядок. Когда курсов много, серверный индексатор читает небольшие диапазоны, сохраняет checkpoint, строит проекцию и предоставляет курсорную пагинацию. Страница получает готовый ограниченный результат вместе с номером индексированного блока и возможным отставанием.
Для упражнения задайте две публикации в одном блоке, одну деактивацию в следующем и повтор получения первого диапазона. Ожидаются три самостоятельные строки без дублирования и понятное происхождение снимка. Затем добавьте новые события во время чтения второй страницы: интерфейс предлагает обновить список, не переставляя его внезапно. API событий описан в getContractEvents.
Стабильность строк после обновления
При повторном чтении одна строка списка сохраняет ключ своего наблюдения. React-компонент или DOM-контроллер может переиспользовать её без потери раскрытых деталей. Номер позиции в массиве для такого ключа не подходит: после появления нового события прежняя строка может переместиться, а раскрытая область окажется связанной с другим действием.
В ограниченной лаборатории удобно закреплять верхний блок диапазона до явного обновления. Тогда переход между страницами рассматривает один снимок. Если endpoint ответил неполным набором из-за собственного ограничения, интерфейс не должен просто считать массив завершённой историей. Требуется уменьшить диапазон и повторить чтение либо использовать индексатор, который управляет такой загрузкой отдельно.