text/event-stream; charset=utf-8 with Cache-Control: no-cache,
and each frame carries three lines:
id— the event’s cursor. It is what you send back to resume.event— the event type, so a client can dispatch without parsing the body.data— the whole event object, the same JSON the paged mode returns.
:hb, to keep the
connection from being reaped by something in the middle. Ignore it; SSE clients
already do.
Or read it as pages
Ask for JSON — or send noAccept at all — and the same journal comes back a
page at a time:
limit runs from 1 to 500 and defaults to 100. next_cursor is the last row’s
cursor even when has_more is false, so a client that has caught up can keep the
cursor and resume later instead of starting over.
Asking for something neither mode serves is a 406:
The event types
sequence counts from 1 and is monotonic per run, so it also tells you whether
you missed anything.
hook.called is the one worth watching while you build: it names the file that
answered each point of the loop and what it decided.
Reconnecting
Delivery is at-least-once, so a client that reconnects may see an event it already has. Deduplicate by eventid and you can reconnect as often as you
like.
To resume, hand back the last cursor you saw — either as the standard SSE header
or as a query parameter:
EventSource sends that header for you. The stream continues strictly
after the named event.
Cursors are opaque and bound to the endpoint and run that issued them. Three ways
to get it wrong, three distinct answers:
How a stream ends
The stream closes after therun.status_changed event whose to is succeeded,
failed or cancelled. That event is the last frame you will see:
In the SDKs
The reconnect, the deduplication and the terminal close are already written:Last-Event-ID, drop duplicates by event id, and end the
iteration after the terminal event — leaving you the same “do a final paged read”
choice the raw protocol does.
