.NET STREAM Server Session Public Interface¶
.NET per-language interface table of contents
1. STREAM Server Session¶
A STREAM session owns lifecycle and typed packet handlers. The framework's internal recv loop receives Core's raw STREAM parts, puts them on a managed queue, and then runs the application callback. Queue admission isn't bypassed by a transport callback.
public interface IZLinkSession
{
IZLinkSessionContext Context { get; }
void Configure() { }
ValueTask OnConnectedAsync(CancellationToken cancellationToken);
ValueTask OnDisconnectedAsync(CancellationToken cancellationToken);
ValueTask OnActorBindingReplacedAsync(
string actorId,
CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
ValueTask OnErrorAsync(
ZLinkStreamError error,
CancellationToken cancellationToken);
ValueTask OnDispatchAsync(
ZLinkSessionDispatchContext dispatch,
ZLinkMessage payload,
CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
public interface IZLinkSessionContext
{
string SessionId { get; }
RoutingId? RoutingId { get; }
string? LocalAddr { get; }
string? RemoteAddr { get; }
IZLinkSessionClient Client { get; }
IZLinkSessionActors Actors { get; }
IZLinkSessionHandlerRegistry Handlers { get; }
ValueTask CloseAsync();
}
public interface IZLinkSessionHandlerRegistry
{
void AddHandler<THandler>() where THandler : class;
void AddHandler<THandler>(string packetName) where THandler : class;
ValueTask<bool> TryHandleAsync(
ZLinkSessionDispatchContext dispatch,
ZLinkMessage payload,
CancellationToken cancellationToken = default);
}
public interface IZLinkSessionPacketHandler<in TSessionContext, TMessage>
{
ValueTask HandleAsync(
TSessionContext context,
ZLinkSessionDispatchContext dispatch,
TMessage message,
CancellationToken cancellationToken);
}
public interface IZLinkSessionClient
{
IZLinkSessionSendCall Send<TMessage>(TMessage message);
IZLinkSessionReplyCall Reply<TMessage>(TMessage message);
}
public interface IZLinkSessionSendCall
: IZLinkMetadataCall<IZLinkSessionSendCall>
{
IZLinkSessionSendCall Compress();
IZLinkSessionSendCall Timeout(TimeSpan timeout);
ValueTask Async(
CancellationToken cancellationToken = default);
}
public interface IZLinkSessionReplyCall
{
IZLinkSessionReplyCall Compress();
ValueTask Async(
CancellationToken cancellationToken = default);
}
public interface IZLinkSessionActors
{
IReadOnlyCollection<IZLinkSessionActor> Bound { get; }
ValueTask<IZLinkSessionActor> BindAsync(
ActorRef actor,
CancellationToken cancellationToken = default);
ValueTask<IZLinkSessionActor> BindOrGetAsync(
ActorRef actor,
CancellationToken cancellationToken = default);
IZLinkSessionActor? Find(string actorId);
}
public interface IZLinkSessionActor
{
string ActorId => Ref.ActorId;
ActorRef Ref { get; }
ValueTask RelayAsync(
ZLinkMessage payload,
CancellationToken cancellationToken = default);
ValueTask NotifyDisconnectedAsync(
CancellationToken cancellationToken = default);
}
public enum ZLinkStreamSessionError
{
Internal = 0,
TransportError = 1,
HandshakeFailed = 2
}
public readonly record struct ZLinkStreamError(
ZLinkStreamSessionError Error,
string? Message);
public sealed class ZLinkSessionDispatchContext
{
public ZLinkSessionDispatchContext(
string packetName,
ZLinkMessageMetadata? metadata = null,
bool canReply = false) { }
public string PacketName { get; }
public ZLinkMessageMetadata Metadata { get; }
public bool CanReply { get; }
}
IZLinkSessionReplyCall validates the current request sequence and
one-shot reply token before sending. A valid first terminator atomically
claims and consumes the token before starting transport. If two calls
created from the same token race, the one that fails the claim doesn't
attempt transport and ends with exceptional completion. A reply created
from a send packet, an already-used token, and a duplicate submit are
rejected the same way. Even if the call that consumed the token ends with
timeout, DeadlineExceeded, or cancellation, the token can't be used
again. A valid reply only uses the STREAM socket send timeout as the
admission deadline. Since the caller request timeout isn't delivered over
the wire, it isn't used as the reply
deadline, and no late reply is
sent after a timeout or cancellation.
IZLinkSessionSendCall.Timeout(...) only shortens this send's admission
wait. Omission uses the STREAM socket SendTimeout; specifying it uses the
shorter of the two, so it cannot extend the socket timeout. Only a positive
value that rounds up into 1..Int32.MaxValue milliseconds is valid. Expiry
completes terminal-once as DeadlineExceeded and does not start later
admission or replay. CancellationToken keeps the existing .NET cancellation
contract, and the reply call doesn't provide this modifier.
OnActorBindingReplacedAsync(...) is an optional callback run once on the previous session when
the same Actor is bound to a new session. The application may use Context.Client.Send(...) to
notify the client, but does not call Context.CloseAsync(). The framework closes the connection
100 ms after the callback reaches a successful or failed terminal; an empty outbound queue does not shorten this delay.
The new bind does not wait for this callback or close.
| Implementation difference | Current state |
|---|---|
| Session Actor binding replacement | The .NET runtime implements command 51 send/receive, this callback, and the non-blocking 100 ms close timer. No implementation difference remains. |
After bind, RelayAsync(...) and NotifyDisconnectedAsync(...) use the
per-Actor binding. A physical disconnect is notified by the framework to
every current binding, running the Spot callback at most once per
binding identity. NotifyDisconnectedAsync(...) is a logical
notification while the connection is kept, and waits for the callback
terminal. The binding callback runs at most once, and after terminal
the binding is committed as a tombstone and removed. The physical STREAM
connection and Actor/Spot membership are kept. No new public Unbind API is
provided. Rebind completes as soon as the new identity becomes current and
does not wait for the previous session. The previous session may notify
the client in OnActorBindingReplacedAsync(...). The framework closes the connection 100 ms
after the callback reaches a successful or failed terminal. Callback or close
failure doesn't remove the new binding or restore the old one. Relocation
keeps the same ObjectGeneration and only updates that Actor's
binding route, so it isn't a rebind and doesn't run the disconnect callback.
A different Actor binding of the same Session and
the physical STREAM connection aren't changed.
RelayAsync(...) is a one-way operation that completes normally once the
Actor relay accepts source-local admission. For a request relayed to a bound Actor,
the typed reply returned by the Actor handler completes the original STREAM
correlation once (Session–Actor binding §12).
Only a request the session callback handles itself is submitted through IZLinkSessionClient.Reply(...).
Packet and lifecycle callbacks of the same session run serially.
Handshake and node-scope errors are reported through runtime monitoring
and aren't delivered to OnErrorAsync(...).
Session binding fixes the specified incarnation of ActorRef.ActorId +
ObjectGeneration once. The MeshName/NodeRid of the Ref submitted at bind
is used as the initial control route snapshot. If there's no mapping,
NotFound; if the current generation differs, InvalidOperation; if in
pre-commit seal, Unavailable — the framework doesn't find a different
ref in the Store and hidden-retry the same bind operation. IZLinkSessionActor.Ref has type ActorRef.
Session–Actor binding §8.2 owns relocation route updates for a bound Session. An overload taking a local IZLinkActor isn't provided.
2. STREAM Transport Handle¶
public interface IZLinkStream
{
string SessionId { get; }
RoutingId? RoutingId { get; }
string? LocalAddr { get; }
string? RemoteAddr { get; }
bool Write(
ZLinkMessage payload,
SendFlags flags = SendFlags.None);
ValueTask CloseAsync();
}
public interface IZLinkMessageMetadataPolicy
{
bool CanForward(string key);
}
IZLinkStream provides transport-facing operations in the session
callback. Bound session and the typed call have a separate
responsibility from this interface.