Публикация
Публикация дописывает пакет в поток под номером, который вы выбираете. Именно выбор номера даёт вам идемпотентность бесплатно. Эта страница описывает основной путь записи, путь для удобства, режим сырых бинарных данных, лимиты и группировка в пакеты.
Основной путь: явный номер
Поместите номер в URL. Он должен быть текущим cursor + 1, чтобы продлить поток, либо
любым значением на уровне курсора или ниже, чтобы повторить более раннюю запись.
curl https://runnev.dev/v1/streams/$STREAM/41823 \
-H "Authorization: Bearer $RUNNEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"events":[
{"type":"order.paid","id":"o_5521","amount":1999},
{"type":"order.paid","id":"o_5522","amount":4500}
]}'
{"stream_id":"cBczepiZSW8GJaCae0xIj7","seq":41823,"accepted":2,"cursor":41823,"duplicate":false}
Ответ также несёт курсор в заголовке, так что вы можете продвигать своё состояние, не разбирая тело:
x-runnev-cursor: 41823
x-request-id: req_0Kj2wq8ULn4mAe1s
Почему явные номера дают идемпотентность
Запись привязана к ключу (stream_id, seq). Если публикация завершилась таймаутом и вы не
знаете, дошла ли она, отправьте ровно тот же запрос заново. Если первый уже прошёл, повтор вернёт
200 с "duplicate": true и ничего нового не сохранит. Если нет — повтор
сохранит его как обычно и вернёт 202. В любом случае вы получаете ровно одну копию, и
вам никогда не пришлось спрашивать: «а это прошло?».
{"stream_id":"cBczepiZSW8GJaCae0xIj7","seq":41823,"accepted":0,"cursor":41830,"duplicate":true}
Это рекомендуемый паттерн для любого издателя, который не должен писать дважды: выводите номер из своего монотонного источника (версия строки БД, смещение, счётчик) и делайте повторы безопасными по построению.
Окно номеров
Нельзя оставлять дыры. Номер более чем на 1024 впереди текущего курсора отклоняется, чтобы баг не мог загнать поток в недостижимую голову:
{
"error": {
"type": "invalid_request_error",
"code": "sequence_too_far_ahead",
"message": "seq 45000 is more than 1024 ahead of the current cursor 41823.",
"request_id": "req_1Ab2cd3EFgh4Ij5k",
"doc_url": "https://runnev.dev/docs/errors#sequence_too_far_ahead"
}
}
Номер на уровне курсора или ниже трактуется как возможный повтор: идентичные байты — это
no-op-дубликат, а иные байты для существующего номера отклоняются с invalid_sequence,
а не молча перезаписывают историю.
Путь для удобства: серверный номер
Когда у вас нет естественного источника номеров и публикует один писатель,
POST /v1/streams/{id}/publish позволяет серверу назначить следующий номер.
curl https://runnev.dev/v1/streams/$STREAM/publish \
-H "Authorization: Bearer $RUNNEV_API_KEY" \
-d '{"events":[{"type":"tick"}]}'
# 202 {"stream_id":"...","seq":41824,"accepted":1,"cursor":41824,"duplicate":false}
Поскольку номер выбирает сервер, повторный publish после неопределённого таймаута
может создать второй пакет. Отправьте заголовок Idempotency-Key, чтобы сделать
повтор безопасным, либо используйте путь с явным номером, когда у вас есть естественный источник
номеров.
Сырые бинарные пакеты
Для потока, созданного с "mode":"raw", публикуйте тело
application/octet-stream. Runnev хранит байты дословно и никогда их не разбирает;
приносите protobuf, msgpack, Avro, CBOR или собственный формат кадров.
curl https://runnev.dev/v1/streams/$STREAM/9001 \
-H "Authorization: Bearer $RUNNEV_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @order.pb
# 202 {"stream_id":"...","seq":9001,"accepted":1,"cursor":9001,"duplicate":false}
На стороне подписки сырые пакеты приходят с байтами, закодированными в base64, в поле
data SSE; прямой GET пакета возвращает сырые байты с исходным
content-type. SDK отдают декодированные байты напрямую.
Лимит размера
Тело одной публикации ограничено 8 МиБ. Большие тела отклоняются:
{
"error": {
"type": "invalid_request_error",
"code": "payload_too_large",
"message": "Publish body is 10.4 MiB; the limit is 8 MiB. Split it across sequences.",
"request_id": "req_5Mn6op7QRst8Uv9w",
"doc_url": "https://runnev.dev/docs/errors#payload_too_large"
}
}
Группировка пакетов ради пропускной способности
Пакет — это единица и доставки, и учёта частоты, поэтому то, как вы группируете события, имеет значение. Одно событие на публикацию просто, но ограничивает вас лимитом частоты запросов; несколько сотен событий на публикацию перегоняют куда больше данных под тем же лимитом.
| Стратегия | Событий/с при 1000 запр./мин | Когда |
|---|---|---|
| 1 событие на публикацию | ~16 | Низкий объём, минимальная задержка на событие |
| 100 событий на публикацию | ~1 600 | Универсальный случай |
| 1000 событий на публикацию | ~16 000 | Высокообъёмный firehose |
Хорошее значение по умолчанию — сбрасывать пакет по достижении либо 100 событий, либо 50 мс буферизации, смотря что раньше. Это удерживает задержку в границах, амортизируя стоимость на запрос. Следите за лимитом тела 8 МиБ по мере роста среднего размера события. Лимиты частоты и тройка заголовков описаны в разделе Ограничения частоты.
Прямая совместимость
Runnev игнорирует не распознанные им параметры запроса. Запрос
POST /v1/streams/{id}/{seq}?trace=abc123&shard=7 обрабатывается ровно так, как
если бы лишних параметров не было. Это намеренное, постоянное обещание: оно позволяет добавлять
собственные параметры трассировки, шардирования или обхода кэша к любому URL Runnev, не опасаясь,
что будущая версия API начнёт их отклонять. Новое необязательное поведение всегда включается через
параметры, которые определяем мы; неизвестные никогда не являются ошибкой.
Повтор и backoff
Повторяйте при 429 и при 5xx. Соблюдайте заголовок
Retry-After при 429. Иначе отступайте экспоненциально с полным
джиттером: sleep = random(0, min(cap, base * 2^attempt)), где
base = 200 мс и cap = 20 с. Поскольку путь с явным номером
идемпотентен, повторять его всегда безопасно. SDK реализуют ровно это.