Подписки и реальное время

Подписки — это операции реального времени: клиент один раз подписывается на событие, а сервер сам присылает новые данные, как только они появляются.

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), которую обычно дёргают мутации. Применяй их там, где нужен живой поток, и помни о цене постоянных соединений. Дальше — как устроены резолверы и сам сервер.

Проверьте себя
1. Чем подписка отличается от запроса?
AНичем, это синонимы
BПодписка держит соединение и сервер сам проталкивает новые данные по мере событий
CПодписка изменяет данные
DПодписка работает только локально
2. Когда подписка избыточна?
AДля чата
BДля живой ленты уведомлений
CКогда данные нужны один раз или меняются редко — проще query
DДля котировок акций