Подписки и реальное время
Подписки — это операции реального времени: клиент один раз подписывается на событие, а сервер сам присылает новые данные, как только они появляются.
Query и Mutation работают по схеме «вопрос-ответ». Subscription переворачивает её: теперь сервер сам стучится к клиенту, когда есть что сказать.
Запросы и мутации — это всегда один цикл «запрос-ответ»: клиент спросил, сервер ответил, соединение закрылось. Но что если данные меняются, и клиент должен узнавать об этом сразу — новое сообщение в чате, изменение цены, статус заказа? Опрашивать сервер каждую секунду (polling) расточительно. Для этого есть третий тип операций — subscription.
type Subscription {
messageAdded(roomId: ID!): Message!
}
Клиент подписывается так же, как делает запрос, но операция остаётся живой: соединение держится открытым (обычно через WebSocket), и каждый раз, когда в комнате появляется сообщение, сервер сам отправляет клиенту новый Message.
subscription OnNewMessage($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
text
author { name }
}
}
Как работает под капотом
В отличие от запроса, подписка возвращает не одно значение, а поток событий. Сервер хранит «шину событий» (PubSub): мутации публикуют события, подписки на них реагируют. Когда происходит публикация, сервер выполняет выборку полей подписки и проталкивает результат всем подписчикам этого события. Схематично:
Клиент A --subscribe(room:1)--> [ Сервер: PubSub ]
Клиент B --subscribe(room:1)--> ^
| publish("MSG_ADDED:1")
Клиент C --mutation sendMessage--> ------+
|
push нового Message <--------------+--> A и B (не C-источнику обязательно)
Смоделируем такую шину событий на чистом JS: подписчики — это колбэки, мутация публикует событие, и все подписчики получают данные:
// мини-PubSub
function createPubSub() {
const subs = {};
return {
subscribe(topic, cb) { (subs[topic] ||= []).push(cb); },
publish(topic, payload) { (subs[topic] || []).forEach(cb => cb(payload)); }
};
}
const pubsub = createPubSub();
// два клиента подписались на комнату 1
pubsub.subscribe("ROOM:1", m => console.log("Клиент A получил:", m.text));
pubsub.subscribe("ROOM:1", m => console.log("Клиент B получил:", m.text));
// мутация sendMessage публикует событие
function sendMessage(text) {
pubsub.publish("ROOM:1", { id: 1, text });
}
sendMessage("Привет, чат!"); // оба клиента получат сообщение
sendMessage("Как дела?");
Попробуй сам ▶ — добавь третьего подписчика на "ROOM:2" и опубликуй туда событие. Подписчики комнаты 1 его не получат — события маршрутизируются по topic.
Когда подписки нужны, а когда нет
- Подходят: чаты, уведомления, живые ленты, статусы заказов/доставки, котировки, совместное редактирование.
- Часто избыточны: данные, которые обновляются редко. Иногда дешевле перезапросить query по кнопке или с интервалом, чем держать постоянное соединение.
Частые ошибки
- Подписка вместо обычного запроса. Если данные нужны один раз — это query, а не subscription. Подписка — про повторяющиеся события.
- Забыть про масштабирование. Тысячи открытых WebSocket-соединений — это нагрузка и состояние на сервере. Нужна продуманная инфраструктура (брокер событий, sticky-сессии).
- Слать тяжёлые объекты в каждом событии. Часто достаточно прислать id изменившейся сущности, а клиент сам решит, надо ли дозапрашивать детали.
Best practices
- Используй подписки точечно — только там, где реально нужно реальное время; для остального хватает query/mutation.
- Связывай подписки с мутациями через PubSub: мутация изменила данные -> опубликовала событие -> подписчики обновились.
- Для масштаба выноси шину событий во внешний брокер (например Redis PubSub), чтобы события доходили между несколькими инстансами сервера.
Итоги
Подписки — третий тип операций GraphQL для реального времени: клиент подписывается один раз, а сервер через постоянное соединение проталкивает новые данные по мере событий. Работают они поверх шины событий (PubSub), которую обычно дёргают мутации. Применяй их там, где нужен живой поток, и помни о цене постоянных соединений. Дальше — как устроены резолверы и сам сервер.