feat: web, websocket 어댑터 추가 구현
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# Web streaming: the client contract
|
||||
|
||||
What a client must implement to consume SSE, NDJSON or `application/json-seq` from this platform.
|
||||
|
||||
## Three outcomes, not two
|
||||
|
||||
After the first byte the HTTP status is 200 and cannot change. So the status tells you nothing about
|
||||
whether the stream succeeded, and a closed connection is ambiguous. There are three endings:
|
||||
|
||||
1. **A `Complete` envelope.** The stream finished. `lastSequence` is the final position.
|
||||
2. **A `Failure` envelope.** Something failed after commit. Carries a `ProblemCode` and a
|
||||
client-safe message. The status is still 200.
|
||||
3. **Neither.** The connection closed mid-stream. The server records this as `ABRUPT_CLOSE`; from
|
||||
the client's side it is indistinguishable from a completed stream *unless the client is looking
|
||||
for the terminal envelope*.
|
||||
|
||||
**A client that treats a closed connection as completion will silently truncate data.** No server
|
||||
change can fix that for it. Looking for the terminal envelope is the contract.
|
||||
|
||||
## Positions
|
||||
|
||||
Every `Item` carries a `sequence`, counting from 1. Positions are strictly increasing, and the
|
||||
server enforces it — a repeat or a regression is a server-side error, not something a client has to
|
||||
tolerate.
|
||||
|
||||
Zero is never a valid position. A stream whose first item claimed 0 and one whose sequence was never
|
||||
set would look identical.
|
||||
|
||||
## Resuming
|
||||
|
||||
Where a route supports it, send the last position you applied as `Last-Event-ID`.
|
||||
|
||||
- If the source still holds it, delivery continues from the next position.
|
||||
- If it does not, the response is a **resnapshot-required** error, not a partial stream. Resuming
|
||||
from the oldest retained position would give you contiguous positions with a hole in the middle,
|
||||
and nothing in the data would say so.
|
||||
|
||||
A resnapshot means re-reading the resource from its normal endpoint and starting a fresh stream.
|
||||
Clients that cannot do that cheaply should not use the resume path.
|
||||
|
||||
## Framing
|
||||
|
||||
| Format | Media type | Framing | Truncation behaviour |
|
||||
| --- | --- | --- | --- |
|
||||
| SSE | `text/event-stream` | `data:` lines, blank-line separated | Partial event at the end |
|
||||
| NDJSON | `application/x-ndjson` | One JSON value, then `\n` | The truncated line has no newline, and the delimiter is what was lost |
|
||||
| JSON-seq | `application/json-seq` | `0x1E`, value, `\n` | The next `0x1E` unambiguously starts the next record |
|
||||
|
||||
Prefer JSON-seq where truncation matters. Its separator comes *first*, so a parser resynchronises at
|
||||
the next record rather than trying to parse the truncation joined to what follows. For a long-lived
|
||||
stream, truncation is the normal way it ends.
|
||||
|
||||
Records never contain a raw newline. The server refuses to write one, because for NDJSON the
|
||||
consumer's line split is the only record boundary there is.
|
||||
|
||||
## Heartbeats and timeouts
|
||||
|
||||
The server writes a keepalive every `heartbeatInterval`. A client that sees nothing for longer than
|
||||
that should assume the connection is dead — a silent connection and a disconnected one are the same
|
||||
thing at the socket, and the heartbeat is what separates them.
|
||||
|
||||
Streams are closed at `idleTimeout` (nothing produced) and at `maxStreamAge` (regardless of
|
||||
activity). Both are normal endings, and both send a terminal envelope where the connection permits.
|
||||
|
||||
## Slow consumers
|
||||
|
||||
The server buffers at most `maxBufferedItems` for a consumer that is behind. Past that it closes the
|
||||
connection rather than growing the buffer. A client that cannot keep up should reconnect with a
|
||||
resume cursor, not expect the server to hold its backlog.
|
||||
|
||||
## Shutdown
|
||||
|
||||
During a rolling deploy, open streams receive a reconnect request before the node stops accepting.
|
||||
Reconnect promptly — the node force-closes anything still open at its drain deadline, and that
|
||||
arrives as an abrupt close.
|
||||
|
||||
Do not reconnect immediately on an abrupt close without backoff and jitter. If a node's streams are
|
||||
all cut at once, every client reconnecting at once is what keeps the replacement down.
|
||||
Reference in New Issue
Block a user