Публикация

Публикация дописывает пакет в поток под номером, который вы выбираете. Именно выбор номера даёт вам идемпотентность бесплатно. Эта страница описывает основной путь записи, путь для удобства, режим сырых бинарных данных, лимиты и группировка в пакеты.

Основной путь: явный номер

Поместите номер в URL. Он должен быть текущим cursor + 1, чтобы продлить поток, либо любым значением на уровне курсора или ниже, чтобы повторить более раннюю запись.

POST /v1/streams/{id}/{seq}
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}
      ]}'
202 Accepted
{"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. В любом случае вы получаете ровно одну копию, и вам никогда не пришлось спрашивать: «а это прошло?».

200 OK — повтор уже сохранённого пакета
{"stream_id":"cBczepiZSW8GJaCae0xIj7","seq":41823,"accepted":0,"cursor":41830,"duplicate":true}

Это рекомендуемый паттерн для любого издателя, который не должен писать дважды: выводите номер из своего монотонного источника (версия строки БД, смещение, счётчик) и делайте повторы безопасными по построению.

Окно номеров

Нельзя оставлять дыры. Номер более чем на 1024 впереди текущего курсора отклоняется, чтобы баг не мог загнать поток в недостижимую голову:

422 Unprocessable Entity
{
  "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 позволяет серверу назначить следующий номер.

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 или собственный формат кадров.

публикация байтов, закодированных protobuf
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 МиБ. Большие тела отклоняются:

413 Payload Too Large
{
  "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 реализуют ровно это.