Skip to content

.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.