Read first: Message Based Communication. A plain
hub.Observe(request, …)waits for exactly ONE reply. This page is the other shape: a reply that arrives in parts.
Why
The model relay (MeshWeaver.Plugins Hosting/ModelRelay) runs a language model on a person's own
machine. The machine answers over its participant connection, and a chat completion is a stream of
tokens that may take minutes. One reply at the end would make the reader wait for the whole answer,
and a total-duration bound would cut a long answer that is still arriving. The mesh's request/response
contract had one terminal reply per request (an AsyncSubject), with RequestProgress as the only
non-terminal message — and progress carries no payload.
The contract
// Caller — cold: the request is posted on Subscribe.
hub.ObserveStreamed(new MyRequest(…), o => o.WithTarget(address))
.Subscribe(delivery =>
{
if (delivery.IsResponsePart()) Append((MyPart)delivery.Message);
else Finish((MyReply)delivery.Message); // the terminal reply, emitted last
}, error => …); // DeliveryFailureException / TimeoutException
// Handler
.WithHandler<MyRequest>((hub, request) =>
{
if (!request.AcceptsResponseParts()) // a single-reply caller: answer ONCE
{
hub.Post(WholeReply(), o => o.ResponseFor(request));
return request.Processed();
}
var n = 0L;
produce.Subscribe(
part => hub.PostResponsePart(request, part, n++),
error => hub.Post(new DeliveryFailure(…), o => o.ResponseFor(request)),
() => hub.PostFinalResponse(request, new MyReply(…), n));
return request.Processed();
});
| Piece | What it does |
|---|---|
PostOptions.AcceptsResponseParts |
Stamped by ObserveStreamed on the request. A handler sends parts only to a request that carries it, so a caller that predates the feature (an older image during a roll) is never sent a part it would take for its reply. |
PostOptions.ResponsePart |
The part's zero-based number. A part is correlated like the reply (RequestId), never consumes the callback, and restarts the request's silence deadline. |
PostOptions.ResponsePartCount |
On the terminal reply: how many parts came before it. The stream completes only once that many parts were emitted, so a part that arrives after the reply is still delivered, in order. |
The rules it keeps
- Order is by number, not by arrival. The SignalR participant forwards each delivery through the
Http
IIoPool, so two posts can overtake each other on the wire. The caller holds early parts until the gap closes and drops a duplicate (a number below the next one expected). - Silence, not duration. The bound is
RequestTimeout(set per hub withWithRequestTimeout) of SILENCE between elements. A long answer that keeps arriving is never cut; one that stops ends in aTimeoutExceptionnaming the request. It is not a raised bound: a stream that goes quiet is cut exactly as early as a single reply that never comes. - A broken part is a failure. A part whose number cannot be read, or a reply whose part count cannot be read, faults the stream at once — it is never placed by guess or read as "no parts".
- Batch the parts. The storm breaker counts parts per REQUEST, so many streams at once between two hubs are fine; one stream above its threshold (2,000 within a second) is a storm and is cut. A producer sends parts of a useful size (the model relay's device sends up to 16 KB of the response per part), never one per byte.
- A failure is a reply. A
DeliveryFailureanswer faults the stream withDeliveryFailureException, after the parts that arrived before it. - The callback lives as long as the stream. A streamed wait is not consumed by its reply (a part may still be in flight behind it); the entry is removed when the caller's stream completes, faults or is disposed.
Not built
- Cancellation towards the handler. Disposing the caller's stream removes its callback, and every later part is consumed with nothing to deliver to; the handler is not told. A handler that produces for a long time bounds itself (the model relay's device does, with a declared maximum duration).
- Back-pressure. Parts are pushed as they are produced; a slow caller buffers them in memory for the life of one stream.
Pinned by StreamedResponseTest (MeshWeaver.Messaging.Hub.Test): parts in order then the reply,
parts posted in reverse re-ordered, a single-reply caller answered once and never sent a part, and a
stream that goes quiet before its reply timed out after the parts that did arrive.