Курсорная пагинация
Задача на постраничную загрузку событий в ленте, где новые элементы могут постоянно появляться в начале списка.
Задача
Сделать пагинацию событий, которая:
- стабильно работает при изменяющейся ленте;
- не дает дублей и пропусков между страницами;
- подходит для бесконечной прокрутки.
Контекст
Обычная offset-пагинация плохо подходит для динамической ленты. Пока пользователь догружает следующую страницу, в начало списка могут прийти новые события, из-за чего смещаются позиции уже существующих записей.
В таком сценарии offset легко приводит к дублям, пропускам и нестабильному порядку результатов.
Ключевое решение
Для API использовал курсорный подход с композитным курсором.
Сервер возвращает:
limit;has_more;next_cursor.
next_cursor содержит указатель на последний загруженный элемент в виде пары <timestamp, uuid>, закодированной в base64. Следующий запрос получает только события, которые идут строго после этой пары значений.
Такой подход позволяет избежать дублей и пропусков при изменении ленты и сохраняет стабильный порядок даже при одинаковых timestamp.
Что сделал на backend
- добавил поддержку курсорной пагинации в
/api/eventsи/api/events/previewс сохранением обратной совместимости; - учёл сортировку по
timestamp, который хранится строкой, и использовал приведение типа вSQL; - добавил endpoint
/api/events/type-countsдля получения количества событий по типам; - подготовил документацию и тесты для новых API.
Что сделал на frontend
- организовал параллельную загрузку счётчиков и первых страниц событий по типам;
- реализовал догрузку через
IntersectionObserverсrootMargin=200px; - добавил передачу
next_cursorпри загрузке следующих страниц; - обработал пограничные сценарии, когда скролл не появляется сразу, observer не срабатывает или позиция прокрутки мешает автоматической догрузке;
- реализовал плавный infinite scroll для динамической ленты, где новые события могут появляться сверху, а старые подгружаются по мере прокрутки вниз.
Результат
В результате удалось убрать прежнее ограничение на 100 событий и перейти к динамической загрузке полного списка событий из базы по мере прокрутки.