Под постом набралось 5000 комментариев. Отдать их одним ответом нельзя: сервер, сеть и браузер захлебнутся. Поступают, как с книгой: читают страницами, а закладка показывает, где остановились. Это пагинация, а закладка называется курсором. Есть и вторая задача: пока вы читаете, под постом появляются новые комментарии. Спрашивать сервер каждые пять секунд (polling) неудобно, лучше услышать звонок: это подписка (subscription). В уроке вы сделаете ленту комментариев с кнопкой «Загрузить ещё» и с новыми комментариями в реальном времени.
Offset или курсор
Самый простой способ — offset: «пропусти 10, дай следующие 10» (skip и take, как в теме про Prisma). Он позволяет сразу перейти на страницу 50, но у него два недостатка. Глубокие страницы медленные: база всё равно перебирает пропущенные строки. И страницы «съезжают», если во время чтения что-то добавили: элементы дублируются или теряются.
Курсор говорит иначе: «дай 10 записей после этой закладки». Запрос идёт по индексу, не зависит от глубины и не ломается от новых записей. Платить приходится тем, что на страницу 50 сразу не перейти: только «дальше».
Relay connection: edges, node, cursor, pageInfo
В GraphQL у курсорной пагинации есть общая договорённость — connection из спецификации Relay. Поле возвращает не список, а объект с тремя частями:
edges— список «конвертов»; в каждомnode(сам комментарий) иcursor(закладка именно на него). Конверт нужен, чтобы хранить данные о связи, а не об объекте.pageInfo— навигация:hasNextPage(«есть ли ещё?») иendCursor(«закладка на последний элемент»).totalCount— частое дополнение к спецификации: сколько всего элементов (в нашей схеме поле обязательное).
В спецификации у pageInfo есть ещё hasPreviousPage и startCursor (для движения назад), они нам не нужны. Поле Post.comments из прошлых уроков теперь возвращает connection (в учебном проекте внешних клиентов нет, в живом API так ломать схему нельзя):
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
type CommentEdge {
cursor: String!
node: Comment!
}
type CommentConnection {
edges: [CommentEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type Post {
# ...the other fields as before
comments(first: Int = 10, after: String): CommentConnection!
}Сервер: курсор в Prisma
Курсор для клиента — непрозрачная строка: он не должен разбирать её и догадываться о содержимом. Мы кодируем в base64url id комментария. Три правила: порядок опирается на уникальное неизменное поле (id), размер страницы ограничен, а читаем на одну запись больше, чтобы узнать про hasNextPage. Запрос Prisma тот же, что в теме про Prisma: cursor, skip: 1, take.
// src/resolvers/post.ts
const encodeCursor = (id: number) =>
Buffer.from(String(id)).toString('base64url');