docs(arch): ADR-049 — streaming handler for subscription operations
The call protocol spec describes streaming (call.responded*N + call.completed, PendingRequestMap::Subscribe, CallConnection::subscribe), but the server-side Handler type returned a single ResponseEnvelope — a Subscription op had no way to produce a stream. The TS predecessor (@alkdev/operations) had separate OperationHandler / SubscriptionHandler types; the Rust port collapsed them, losing the streaming path. This restores it end-to-end: StreamingHandler type, HandlerKind on HandlerRegistration validated against op_type, invoke_streaming() on OperationRegistry, server-side dispatch branches on op_type, new INVALID_OPERATION_TYPE protocol code for wrong-dispatch-path misuse, GatewayDispatch::invoke_streaming() for /subscribe SSE, from_call stream forwarding via CallConnection::subscribe(), from_openapi SSE forwarding. OperationEnv::invoke() stays request/response-only (stream composition is handler-level, not protocol-level). Amends ADR-023's protocol-code list (five → six). Tracks the stream-operators library as OQ-41 (feature extension, not an unmade decision).
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-01
|
||||
last_updated: 2026-07-02
|
||||
---
|
||||
|
||||
# HTTP Server
|
||||
@@ -194,13 +194,22 @@ The request body is `{ operation, input }` (the same flat JSON shape as
|
||||
`Accept: text/event-stream` on the `POST`). The axum route handler:
|
||||
|
||||
- Sets `Content-Type: text/event-stream`.
|
||||
- For each `call.responded` event, writes an SSE `data:` frame (the
|
||||
event's `output` serialized as JSON).
|
||||
- On `call.completed`, closes the SSE stream (normal end).
|
||||
- On `call.aborted`, closes the stream with an SSE error event.
|
||||
- On HTTP client disconnect (detected as the response writer closing),
|
||||
sends `call.aborted` for the in-flight subscription, which cascades
|
||||
to descendants per ADR-016.
|
||||
- Calls `GatewayDispatch::invoke_streaming()` (ADR-049) — the streaming
|
||||
analogue of `invoke()`, returning a `BoxStream<ResponseEnvelope>`. The
|
||||
security invariants are identical to `invoke()`: `internal: false`,
|
||||
`forwarded_for: None`, same capabilities, same `scoped_env`, same ACL
|
||||
check before dispatch. The two methods diverge only on the return shape
|
||||
(stream vs single envelope).
|
||||
- For each `ResponseEnvelope` the stream yields, writes an SSE `data:` frame:
|
||||
`Ok(value)` → `data:` frame with the output serialized as JSON; `Err` →
|
||||
SSE error event with the `CallError` serialized, then close (an `Err` is
|
||||
terminal — the stream ends after it, matching the wire protocol's
|
||||
`call.error` semantics).
|
||||
- On natural stream end (the `StreamingHandler`'s stream completes), closes
|
||||
the SSE stream (normal end — corresponds to `call.completed` on the wire).
|
||||
- On `call.aborted` or HTTP client disconnect (detected as the response
|
||||
writer closing), drops the stream future — `Drop` guards release the
|
||||
handler's resources, and the abort cascade runs per ADR-016.
|
||||
|
||||
This is the HTTP/1.1 + HTTP/2 streaming projection. Over WebSocket
|
||||
([websocket.md](websocket.md)), the subscription projects directly
|
||||
@@ -209,6 +218,16 @@ no SSE framing. WebTransport (`h3`, deferred per ADR-044) would project
|
||||
onto WebTransport bidirectional streams; see
|
||||
[webtransport.md](webtransport.md).
|
||||
|
||||
**The streaming dispatch path.** Pre-ADR-049, `subscribe_handler` called
|
||||
`GatewayDispatch::invoke()` (single response) and wrapped the one
|
||||
`ResponseEnvelope` in a one-event SSE stream — a placeholder that couldn't
|
||||
stream a real `Subscription` op. ADR-049 adds `GatewayDispatch::
|
||||
invoke_streaming()` and the underlying `OperationRegistry::
|
||||
invoke_streaming()`, giving `/subscribe` a real streaming dispatch path
|
||||
to call. See ADR-049 and [http-adapters.md](http-adapters.md) for the
|
||||
`from_openapi` SSE forwarding handler that feeds `StreamingHandler`s from
|
||||
external `text/event-stream` responses.
|
||||
|
||||
### One-directional projection (HTTP request/response)
|
||||
|
||||
The HTTP/1.1 + HTTP/2 surface is a **lossy, one-directional projection**
|
||||
@@ -446,6 +465,7 @@ two-way door (add/remove freely). See
|
||||
| Browsers are not alknet peers | [ADR-034](../../decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 (amended by ADR-044 §5) | Bearer token, no `PeerId`, connection-local overlay (addressability vs. bidirectionality) — full rationale in [websocket.md](websocket.md) |
|
||||
| Error mapping (call codes → HTTP status) | [ADR-023](../../decisions/023-operation-error-schemas.md) | Protocol/operation codes distinct; `HTTP_<status>` prefix for imported |
|
||||
| Custom HTTP routes from the assembly layer | [ADR-046](../../decisions/046-assembly-layer-custom-http-routes.md) | `extra_routes: Option<Router>` at construction; raw HTTP, not operations; default surface takes precedence on collision |
|
||||
| Streaming handler for subscriptions (`invoke_streaming()`) | [ADR-049](../../decisions/049-streaming-handler-for-subscriptions.md) | `GatewayDispatch::invoke_streaming()` returns `BoxStream<ResponseEnvelope>`; `/subscribe` pipes it to SSE; replaces the one-event placeholder with the real streaming dispatch path |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user