Подписка
Подписка — это один долгоживущий GET, который потоково отдаёт пакеты как server-sent
events. Эта страница подробно описывает формат передачи, контракт keepalive, возобновление по
курсору и операционные реалии соединений, остающихся открытыми часами.
Запрос
Отправьте Accept: text/event-stream. Именно этот заголовок превращает обычный
метаданный GET в подписку.
curl -N "https://runnev.dev/v1/streams/$STREAM?cursor=head" \
-H "Accept: text/event-stream" \
-H "Authorization: Bearer $RUNNEV_API_KEY"
Заголовки ответа
content-type: text/event-stream; charset=utf-8
cache-control: no-store
x-accel-buffering: no
x-request-id: req_0Kj2wq8ULn4mAe1s
connection: keep-alive
Два из этих заголовков достаточно важны, чтобы их пояснить:
cache-control: no-storeговорит каждому кэшу на пути, включая любой CDN, не буферизовать и не хранить ответ. Закэшированный поток событий — это сломанный поток событий.x-accel-buffering: noговорит обратным прокси, которые его соблюдают, сбрасывать каждую запись немедленно, а не накапливать буфер. Без него прокси может удерживать ваши пакеты, пока не заполнится его буфер, добавляя секунды задержки или полностью стопоря простаивающий поток.
Формат передачи
Пакеты приходят как SSE-события. У каждого есть id (номер), тип event и
строка data с JSON-пакетом:
: runnev
id: 41823
event: batch
data: {"seq":41823,"ts":"2026-08-04T11:02:13.418Z","events":[{"type":"order.paid","id":"o_5521"}]}
id: 41824
event: batch
data: {"seq":41824,"ts":"2026-08-04T11:02:16.902Z","events":[{"type":"order.created","id":"o_5523"}]}
: keepalive
Типы событий, которые вы увидите:
| event | Значение |
|---|---|
batch | Пакет событий. data — это JSON-объект пакета. |
end | Поток был удалён. Затем соединение закрывается. |
| (комментарий) | Строка, начинающаяся с :, — это keepalive или баннер. Игнорируйте её. |
Кадр keepalive
Когда поток простаивает, Runnev отправляет кадр-комментарий (: keepalive) каждые
15 секунд. Он не несёт данных и не является событием; его единственная задача —
держать соединение наблюдаемо живым, чтобы ни клиент, ни какой-либо посредник не приняли тихий
поток за мёртвый. Считайте приход любых байтов, включая комментарий, доказательством
жизнеспособности, а промежуток дольше примерно 30 секунд вообще без байтов — поводом
переподключиться.
Курсор и возобновление
Параметр запроса cursor управляет тем, откуда начинается подписка:
| Значение | Начинается с |
|---|---|
?cursor=head (по умолчанию) | Только новые пакеты, с этого момента |
?cursor=0 | Старейший уцелевший пакет |
?cursor=N | Первый пакет после номера N |
Чтобы возобновить после разрыва, переподключитесь с ?cursor=, равным номеру
последнего полностью обработанного вами пакета. Runnev также соблюдает стандартный заголовок
запроса Last-Event-ID, который нативный браузерный EventSource
отправляет автоматически при переподключении; если присутствуют оба, побеждает параметр запроса.
curl -N "https://runnev.dev/v1/streams/$STREAM?cursor=41823" \
-H "Accept: text/event-stream" \
-H "Authorization: Bearer $RUNNEV_API_KEY"
Долгоживущие соединения
Здоровый подписчик держит один ответ открытым неограниченно долго. Это нормальный, поддерживаемый режим работы, а не крайний случай — подписка может оставаться открытой часами, — и SDK построены так, чтобы держать одну открытой и прозрачно переподключаться, когда сеть к этому вынуждает. Если вы ставите перед сервисом собственный обратный прокси, задайте ему дружелюбные к стримингу настройки:
- Отключите буферизацию ответов на любом обратном прокси на пути. Runnev
выставляет
x-accel-buffering: no; убедитесь, что ваш прокси его соблюдает, либо отключите буферизацию явно. - Поднимите таймауты простоя и чтения выше 15-секундного интервала keepalive, с запасом. 10-секундный таймаут чтения у прокси оборвёт каждый простаивающий поток. Разумно ставить минуту и больше.
- Не кэшируйте
/v1. Ответы помеченыno-storeне просто так; кэширующий слой, игнорирующий это, сломает и публикацию, и подписку.
В полевом отчёте о длинных HTTP-ответах разобраны дружелюбные к стримингу настройки, которые нужны обратному прокси.
Непотоковый запасной вариант
Клиент, который не может держать потоковый ответ или предпочёл бы опрос, может вовсе обойтись без
SSE. Тот же GET без заголовка Accept: text/event-stream
возвращает метаданные потока, включая его текущий cursor. Опрашивайте курсор, и когда
он продвинется, забирайте новые пакеты по номеру:
# 1. насколько продвинулся поток?
curl "https://runnev.dev/v1/streams/$STREAM/cursor" \
-H "Authorization: Bearer $RUNNEV_API_KEY"
# {"cursor":41825,"updated_at":"2026-08-04T11:03:01.220Z"}
# 2. заберите недостающие пакеты по номеру
curl "https://runnev.dev/v1/streams/$STREAM/batches/41824" \
-H "Authorization: Bearer $RUNNEV_API_KEY"
Потоковая передача дешевле и с меньшей задержкой, но опрос доступен всегда и требует не больше, чем обычный клиент «запрос/ответ».
Дедупликация при переподключении
Доставка — не менее одного раза, поэтому переподключение вокруг сбоя может повторно доставить
последний пакет. Поскольку каждый пакет несёт свой seq, дедупликация — это одно
сравнение:
let last = -1;
for await (const batch of runnev.subscribe(streamId, { cursor: saved })) {
if (batch.seq <= last) continue; // уже обработано
handle(batch);
last = batch.seq;
persist(last); // чтобы после перезапуска возобновиться отсюда
}
Сохраняйте last там, где вы держите надёжное состояние, и передавайте его как курсор
при старте. Это весь рецепт подписчика, переживающего перезапуски и сетевые сбои без потери или
двойной обработки данных.