feat: port sessions 14-16 — Routes, Leaf Nodes & Gateways

Session 14 (57 features, IDs 2895-2951):
- RouteTypes: RouteType enum, Route, RouteInfo, ConnectInfo, ASubs, GossipMode

Session 15 (71 features, IDs 1979-2049):
- LeafNodeTypes: Leaf, LeafNodeCfg (replaces stub), LeafConnectInfo

Session 16 (91 features, IDs 1263-1353):
- GatewayTypes: GatewayInterestMode, SrvGateway (replaces stub), GatewayCfg,
  Gateway, OutSide, InSide, SitAlly, GwReplyMap, GwReplyMapping
This commit is contained in:
Joseph Doherty
2026-02-26 15:50:51 -05:00
parent ce45dff994
commit 77403e3d31
7 changed files with 816 additions and 11 deletions

View File

@@ -0,0 +1,384 @@
// Copyright 2018-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/gateway.go in the NATS server Go source.
using System.Threading;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Internal.DataStructures;
namespace ZB.MOM.NatsNet.Server;
// ============================================================================
// Session 16: Gateways
// ============================================================================
/// <summary>
/// Represents the interest mode for a given account on a gateway connection.
/// Mirrors Go <c>GatewayInterestMode</c> byte iota in gateway.go.
/// Do not change values — they are part of the wire-level gossip protocol.
/// </summary>
public enum GatewayInterestMode : byte
{
/// <summary>
/// Default mode: the cluster sends to a gateway unless told there is no
/// interest (applies to plain subscribers only).
/// </summary>
Optimistic = 0,
/// <summary>
/// Transitioning: the gateway has been sending too many no-interest signals
/// and is switching to <see cref="InterestOnly"/> mode for this account.
/// </summary>
Transitioning = 1,
/// <summary>
/// Interest-only mode: the cluster has sent all its subscription interest;
/// the gateway only forwards messages when explicit interest is known.
/// </summary>
InterestOnly = 2,
/// <summary>
/// Internal sentinel used after a cache flush; not part of the public wire enum.
/// </summary>
CacheFlushed = 3,
}
/// <summary>
/// Server-level gateway state kept on the <see cref="NatsServer"/> instance.
/// Replaces the stub that was in <c>NatsServerTypes.cs</c>.
/// Mirrors Go <c>srvGateway</c> struct in gateway.go.
/// </summary>
internal sealed class SrvGateway
{
/// <summary>
/// Total number of queue subs across all remote gateways.
/// Accessed via <c>Interlocked</c> — must be 64-bit aligned.
/// </summary>
public long TotalQSubs;
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
/// <summary>
/// True if both a gateway name and port are configured (immutable after init).
/// </summary>
public bool Enabled { get; set; }
/// <summary>Name of this server's gateway cluster.</summary>
public string Name { get; set; } = string.Empty;
/// <summary>Outbound gateway connections keyed by remote gateway name.</summary>
public Dictionary<string, ClientConnection> Out { get; set; } = new();
/// <summary>
/// Outbound gateway connections in RTT order, used for message routing.
/// </summary>
public List<ClientConnection> Outo { get; set; } = [];
/// <summary>Inbound gateway connections keyed by connection ID.</summary>
public Dictionary<ulong, ClientConnection> In { get; set; } = new();
/// <summary>Per-remote-gateway configuration, keyed by gateway name.</summary>
public Dictionary<string, GatewayCfg> Remotes { get; set; } = new();
/// <summary>Reference-counted set of all gateway URLs in the cluster.</summary>
public RefCountedUrlSet Urls { get; set; } = new();
/// <summary>This server's own gateway URL (after random-port resolution).</summary>
public string Url { get; set; } = string.Empty;
/// <summary>Gateway INFO protocol object.</summary>
public ServerInfo? Info { get; set; }
/// <summary>Pre-marshalled INFO JSON bytes.</summary>
public byte[]? InfoJson { get; set; }
/// <summary>When true, reject connections from gateways not in <see cref="Remotes"/>.</summary>
public bool RejectUnknown { get; set; }
/// <summary>
/// Reply prefix bytes: <c>"$GNR.&lt;reserved&gt;.&lt;clusterHash&gt;.&lt;serverHash&gt;."</c>
/// </summary>
public byte[] ReplyPfx { get; set; } = [];
// Backward-compatibility reply prefix and hash (old "$GR." scheme)
public byte[] OldReplyPfx { get; set; } = [];
public byte[] OldHash { get; set; } = [];
// -------------------------------------------------------------------------
// pasi — per-account subject interest tally (protected by its own mutex)
// -------------------------------------------------------------------------
/// <summary>
/// Per-account subject-interest tally.
/// Outer key = account name; inner key = subject (or "subject queue" pair);
/// value = tally struct.
/// Mirrors Go's anonymous <c>pasi</c> embedded struct in <c>srvGateway</c>.
/// </summary>
private readonly Lock _pasiLock = new();
public Dictionary<string, Dictionary<string, SitAlly>> Pasi { get; set; } = new();
public Lock PasiLock => _pasiLock;
// -------------------------------------------------------------------------
// Recent subscription tracking (thread-safe map)
// -------------------------------------------------------------------------
/// <summary>
/// Recent subscriptions for a given account (subject → expiry ticks).
/// Mirrors Go's <c>rsubs sync.Map</c>.
/// </summary>
public System.Collections.Concurrent.ConcurrentDictionary<string, long> RSubs { get; set; } = new();
// -------------------------------------------------------------------------
// Other server-level gateway fields
// -------------------------------------------------------------------------
/// <summary>DNS resolver used before dialling gateway connections.</summary>
public INetResolver? Resolver { get; set; }
/// <summary>Max buffer size for sending queue-sub protocol (used in tests).</summary>
public int SqbSz { get; set; }
/// <summary>How long to look for a subscription match for a reply message.</summary>
public TimeSpan RecSubExp { get; set; }
/// <summary>Server ID hash (6 bytes) for routing mapped replies.</summary>
public byte[] SIdHash { get; set; } = [];
/// <summary>
/// Map from a route server's hashed ID (6 bytes) to the route client.
/// Mirrors Go's <c>routesIDByHash sync.Map</c>.
/// </summary>
public System.Collections.Concurrent.ConcurrentDictionary<string, ClientConnection> RoutesIdByHash { get; set; } = new();
/// <summary>
/// Gateway URLs from this server's own entry in the Gateways config block,
/// used for monitoring reports.
/// </summary>
public List<string> OwnCfgUrls { get; set; } = [];
// -------------------------------------------------------------------------
// Lock helpers
// -------------------------------------------------------------------------
public void AcquireReadLock() => _lock.EnterReadLock();
public void ReleaseReadLock() => _lock.ExitReadLock();
public void AcquireWriteLock() => _lock.EnterWriteLock();
public void ReleaseWriteLock() => _lock.ExitWriteLock();
}
/// <summary>
/// Subject-interest tally entry. Indicates whether the key in the map is a
/// queue subscription and how many matching subscriptions exist.
/// Mirrors Go <c>sitally</c> struct in gateway.go.
/// </summary>
internal sealed class SitAlly
{
/// <summary>Number of subscriptions directly matching the subject/queue key.</summary>
public int N { get; set; }
/// <summary>True if this entry represents a queue subscription.</summary>
public bool Q { get; set; }
}
/// <summary>
/// Runtime configuration for a single remote gateway.
/// Wraps <see cref="RemoteGatewayOpts"/> with connection-attempt state and a lock.
/// Mirrors Go <c>gatewayCfg</c> struct in gateway.go.
/// </summary>
internal sealed class GatewayCfg
{
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
/// <summary>The raw remote-gateway options this cfg was built from.</summary>
public RemoteGatewayOpts? RemoteOpts { get; set; }
/// <summary>6-byte cluster hash used for reply routing.</summary>
public byte[] Hash { get; set; } = [];
/// <summary>4-byte old-style hash for backward compatibility.</summary>
public byte[] OldHash { get; set; } = [];
/// <summary>Map of URL string → parsed URL for this remote gateway.</summary>
public Dictionary<string, Uri> Urls { get; set; } = new();
/// <summary>Number of connection attempts made so far.</summary>
public int ConnAttempts { get; set; }
/// <summary>TLS server name override for SNI.</summary>
public string TlsName { get; set; } = string.Empty;
/// <summary>True if this remote was discovered via gossip (not configured).</summary>
public bool Implicit { get; set; }
/// <summary>When true, monitoring should refresh the URL list on next varz inspection.</summary>
public bool VarzUpdateUrls { get; set; }
// Forwarded properties from RemoteGatewayOpts
public string Name { get => RemoteOpts?.Name ?? string.Empty; }
// -------------------------------------------------------------------------
// Lock helpers
// -------------------------------------------------------------------------
public void AcquireReadLock() => _lock.EnterReadLock();
public void ReleaseReadLock() => _lock.ExitReadLock();
public void AcquireWriteLock() => _lock.EnterWriteLock();
public void ReleaseWriteLock() => _lock.ExitWriteLock();
}
/// <summary>
/// Per-connection gateway state embedded in <see cref="ClientConnection"/>
/// when the connection kind is <c>Gateway</c>.
/// Mirrors Go <c>gateway</c> struct in gateway.go.
/// </summary>
internal sealed class Gateway
{
/// <summary>Name of the remote gateway cluster.</summary>
public string Name { get; set; } = string.Empty;
/// <summary>Configuration block for the remote gateway.</summary>
public GatewayCfg? Cfg { get; set; }
/// <summary>URL used for CONNECT after receiving the remote INFO (outbound only).</summary>
public Uri? ConnectUrl { get; set; }
/// <summary>
/// Per-account subject interest (outbound connection).
/// Maps account name → <see cref="OutSide"/> for that account.
/// Uses a thread-safe map because it is read from multiple goroutines.
/// </summary>
public System.Collections.Concurrent.ConcurrentDictionary<string, OutSide>? OutSim { get; set; }
/// <summary>
/// Per-account no-interest subjects or interest-only mode (inbound connection).
/// </summary>
public Dictionary<string, InSide>? InSim { get; set; }
/// <summary>True if this is an outbound gateway connection.</summary>
public bool Outbound { get; set; }
/// <summary>
/// Set in the read loop without locking to record that the inbound side
/// sent its CONNECT protocol.
/// </summary>
public bool Connected { get; set; }
/// <summary>
/// True if the remote server only understands the old <c>$GR.</c> prefix,
/// not the newer <c>$GNR.</c> scheme.
/// </summary>
public bool UseOldPrefix { get; set; }
/// <summary>
/// When true the inbound side switches accounts to interest-only mode
/// immediately, so the outbound side can disregard optimistic mode.
/// </summary>
public bool InterestOnlyMode { get; set; }
/// <summary>Name of the remote server on this gateway connection.</summary>
public string RemoteName { get; set; } = string.Empty;
}
/// <summary>
/// Outbound subject-interest entry for a single account on a gateway connection.
/// Mirrors Go <c>outsie</c> struct in gateway.go.
/// </summary>
internal sealed class OutSide
{
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
/// <summary>Current interest mode for this account on the outbound gateway.</summary>
public GatewayInterestMode Mode { get; set; }
/// <summary>
/// Set of subjects for which the remote has signalled no-interest.
/// Null when the remote has sent all its subscriptions (interest-only mode).
/// </summary>
public HashSet<string>? Ni { get; set; }
/// <summary>
/// Subscription index: contains queue subs in optimistic mode,
/// or all subs when <see cref="Mode"/> has been switched.
/// </summary>
public SubscriptionIndex? Sl { get; set; }
/// <summary>Number of queue subscriptions tracked in <see cref="Sl"/>.</summary>
public int Qsubs { get; set; }
// -------------------------------------------------------------------------
// Lock helpers
// -------------------------------------------------------------------------
public void AcquireReadLock() => _lock.EnterReadLock();
public void ReleaseReadLock() => _lock.ExitReadLock();
public void AcquireWriteLock() => _lock.EnterWriteLock();
public void ReleaseWriteLock() => _lock.ExitWriteLock();
}
/// <summary>
/// Inbound subject-interest entry for a single account on a gateway connection.
/// Tracks subjects for which an RS- was sent to the remote, and the current mode.
/// Mirrors Go <c>insie</c> struct in gateway.go.
/// </summary>
internal sealed class InSide
{
/// <summary>
/// Subjects for which RS- was sent to the remote (null when in interest-only mode).
/// </summary>
public HashSet<string>? Ni { get; set; }
/// <summary>Current interest mode for this account on the inbound gateway.</summary>
public GatewayInterestMode Mode { get; set; }
}
/// <summary>
/// A single gateway reply-mapping entry: the mapped subject and its expiry.
/// Mirrors Go <c>gwReplyMap</c> struct in gateway.go.
/// </summary>
internal sealed class GwReplyMap
{
/// <summary>The mapped (routed) subject string.</summary>
public string Ms { get; set; } = string.Empty;
/// <summary>Expiry expressed as <see cref="DateTime.Ticks"/> (UTC).</summary>
public long Exp { get; set; }
}
/// <summary>
/// Gateway reply routing table and a fast-path check flag.
/// Mirrors Go <c>gwReplyMapping</c> struct in gateway.go.
/// </summary>
internal sealed class GwReplyMapping
{
/// <summary>
/// Non-zero when the mapping table should be consulted while processing
/// inbound messages. Accessed via <c>Interlocked</c> — must be 32-bit aligned.
/// </summary>
public int Check;
/// <summary>Active reply-subject → GwReplyMap entries.</summary>
public Dictionary<string, GwReplyMap> Mapping { get; set; } = new();
/// <summary>
/// Returns the routed subject for <paramref name="subject"/> if a mapping
/// exists, otherwise returns the original subject and <c>false</c>.
/// Caller must hold any required lock before invoking.
/// </summary>
public (byte[] Subject, bool Found) Get(byte[] subject)
{
// TODO: session 16 — implement mapping lookup
return (subject, false);
}
}

View File

@@ -0,0 +1,202 @@
// Copyright 2019-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/leafnode.go in the NATS server Go source.
using System.Text.Json.Serialization;
using System.Threading;
using ZB.MOM.NatsNet.Server.Auth;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server;
// ============================================================================
// Session 15: Leaf Nodes
// ============================================================================
/// <summary>
/// Per-connection leaf-node state embedded in <see cref="ClientConnection"/>
/// when the connection kind is <c>Leaf</c>.
/// Mirrors Go <c>leaf</c> struct in leafnode.go.
/// </summary>
internal sealed class Leaf
{
/// <summary>
/// Config for solicited (outbound) leaf connections; null for accepted connections.
/// </summary>
public LeafNodeCfg? Remote { get; set; }
/// <summary>
/// True when we are the spoke side of a hub/spoke leaf pair.
/// </summary>
public bool IsSpoke { get; set; }
/// <summary>
/// Cluster name of the remote server when we are a hub and the spoke is
/// part of a cluster.
/// </summary>
public string RemoteCluster { get; set; } = string.Empty;
/// <summary>Remote server name or ID.</summary>
public string RemoteServer { get; set; } = string.Empty;
/// <summary>Domain name of the remote server.</summary>
public string RemoteDomain { get; set; } = string.Empty;
/// <summary>Account name of the remote server.</summary>
public string RemoteAccName { get; set; } = string.Empty;
/// <summary>
/// When true, suppresses propagation of east-west interest from other leaf nodes.
/// </summary>
public bool Isolated { get; set; }
/// <summary>
/// Subject-interest suppression map shared with the remote side.
/// Key = subject, Value = interest count (positive = subscribe, negative = unsubscribe delta).
/// </summary>
public Dictionary<string, int> Smap { get; set; } = new();
/// <summary>
/// Short-lived set of subscriptions added during <c>initLeafNodeSmapAndSendSubs</c>
/// to detect and avoid double-counting races.
/// </summary>
public HashSet<Subscription>? Tsub { get; set; }
/// <summary>Timer that clears <see cref="Tsub"/> after the initialization window.</summary>
public Timer? Tsubt { get; set; }
/// <summary>
/// Selected compression mode, which may differ from the server-configured mode.
/// </summary>
public string Compression { get; set; } = string.Empty;
/// <summary>
/// Gateway-mapped reply subscription used for GW reply routing via leaf nodes.
/// </summary>
public Subscription? GwSub { get; set; }
}
/// <summary>
/// Runtime configuration for a remote (solicited) leaf-node connection.
/// Wraps <see cref="RemoteLeafOpts"/> with connection-attempt state and a
/// reader-writer lock for concurrent access.
/// Mirrors Go <c>leafNodeCfg</c> struct in leafnode.go.
/// Replaces the stub that was in <c>NatsServerTypes.cs</c>.
/// </summary>
public sealed class LeafNodeCfg
{
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion);
// -------------------------------------------------------------------------
// Embedded RemoteLeafOpts fields
// -------------------------------------------------------------------------
/// <summary>The raw remote options this cfg was constructed from.</summary>
public RemoteLeafOpts? RemoteOpts { get; set; }
// -------------------------------------------------------------------------
// Runtime connection-attempt fields
// -------------------------------------------------------------------------
/// <summary>Resolved URLs to attempt connections to.</summary>
public List<Uri> Urls { get; set; } = [];
/// <summary>Currently selected URL from <see cref="Urls"/>.</summary>
public Uri? CurUrl { get; set; }
/// <summary>TLS server name override for SNI.</summary>
public string TlsName { get; set; } = string.Empty;
/// <summary>Username for authentication (resolved from credentials or options).</summary>
public string Username { get; set; } = string.Empty;
/// <summary>Password for authentication (resolved from credentials or options).</summary>
public string Password { get; set; } = string.Empty;
/// <summary>Publish/subscribe permission overrides for this connection.</summary>
public Permissions? Perms { get; set; }
/// <summary>
/// Delay before the next connection attempt (e.g. during loop-detection back-off).
/// </summary>
public TimeSpan ConnDelay { get; set; }
/// <summary>
/// Timer used to trigger JetStream account migration for this leaf.
/// </summary>
public Timer? JsMigrateTimer { get; set; }
// -------------------------------------------------------------------------
// Forwarded properties from RemoteLeafOpts
// -------------------------------------------------------------------------
public string LocalAccount { get => RemoteOpts?.LocalAccount ?? string.Empty; }
public bool NoRandomize { get => RemoteOpts?.NoRandomize ?? false; }
public string Credentials { get => RemoteOpts?.Credentials ?? string.Empty; }
public bool Disabled { get => RemoteOpts?.Disabled ?? false; }
// -------------------------------------------------------------------------
// Lock helpers
// -------------------------------------------------------------------------
public void AcquireReadLock() => _lock.EnterReadLock();
public void ReleaseReadLock() => _lock.ExitReadLock();
public void AcquireWriteLock() => _lock.EnterWriteLock();
public void ReleaseWriteLock() => _lock.ExitWriteLock();
}
/// <summary>
/// CONNECT protocol payload sent by a leaf-node connection.
/// Fields map 1-to-1 with the JSON tags in Go's <c>leafConnectInfo</c>.
/// Mirrors Go <c>leafConnectInfo</c> struct in leafnode.go.
/// </summary>
internal sealed class LeafConnectInfo
{
[JsonPropertyName("version")] public string Version { get; set; } = string.Empty;
[JsonPropertyName("nkey")] public string Nkey { get; set; } = string.Empty;
[JsonPropertyName("jwt")] public string Jwt { get; set; } = string.Empty;
[JsonPropertyName("sig")] public string Sig { get; set; } = string.Empty;
[JsonPropertyName("user")] public string User { get; set; } = string.Empty;
[JsonPropertyName("pass")] public string Pass { get; set; } = string.Empty;
[JsonPropertyName("auth_token")] public string Token { get; set; } = string.Empty;
[JsonPropertyName("server_id")] public string Id { get; set; } = string.Empty;
[JsonPropertyName("domain")] public string Domain { get; set; } = string.Empty;
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
[JsonPropertyName("is_hub")] public bool Hub { get; set; }
[JsonPropertyName("cluster")] public string Cluster { get; set; } = string.Empty;
[JsonPropertyName("headers")] public bool Headers { get; set; }
[JsonPropertyName("jetstream")] public bool JetStream { get; set; }
[JsonPropertyName("deny_pub")] public string[] DenyPub { get; set; } = [];
[JsonPropertyName("isolate")] public bool Isolate { get; set; }
/// <summary>
/// Compression mode string. The legacy boolean field was never used; this
/// string field uses a different JSON tag to avoid conflicts.
/// </summary>
[JsonPropertyName("compress_mode")] public string Compression { get; set; } = string.Empty;
/// <summary>
/// Used only to detect wrong-port connections (client connecting to leaf port).
/// </summary>
[JsonPropertyName("gateway")] public string Gateway { get; set; } = string.Empty;
/// <summary>Account name the remote is binding to on the accept side.</summary>
[JsonPropertyName("remote_account")] public string RemoteAccount { get; set; } = string.Empty;
/// <summary>
/// Protocol version sent by the soliciting side so the accepting side knows
/// which features are supported (e.g. message tracing).
/// </summary>
[JsonPropertyName("protocol")] public int Proto { get; set; }
}

View File

@@ -225,11 +225,7 @@ public sealed class JetStreamConfig
public string UniqueTag { get; set; } = string.Empty;
}
/// <summary>Stub for server gateway state (session 16).</summary>
internal sealed class SrvGateway
{
public bool Enabled { get; set; }
}
// SrvGateway — full class is in Gateway/GatewayTypes.cs (session 16).
/// <summary>Stub for server websocket state (session 23).</summary>
internal sealed class SrvWebsocket
@@ -249,8 +245,7 @@ internal interface IOcspResponseCache { }
/// <summary>Stub for IP queue (session 02 — already ported as IpQueue).</summary>
// IpQueue is already in session 02 internals — used here via object.
/// <summary>Stub for leaf node config (session 15).</summary>
internal sealed class LeafNodeCfg { }
// LeafNodeCfg — full class is in LeafNode/LeafNodeTypes.cs (session 15).
/// <summary>
/// Network resolver used by <see cref="NatsServer.GetRandomIP"/>.

View File

@@ -0,0 +1,185 @@
// Copyright 2013-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/route.go in the NATS server Go source.
using System.Text.Json.Serialization;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server;
// ============================================================================
// Session 14: Routes
// ============================================================================
/// <summary>
/// Designates whether a route was explicitly configured or discovered via gossip.
/// Mirrors Go <c>RouteType</c> iota in route.go.
/// Note: Go defines Implicit=0, Explicit=1 — we keep TombStone=2 for future use.
/// </summary>
public enum RouteType : int
{
/// <summary>This route was learned from speaking to other routes.</summary>
Implicit = 0,
/// <summary>This route was explicitly configured.</summary>
Explicit = 1,
/// <summary>Reserved tombstone marker for removed routes.</summary>
TombStone = 2,
}
/// <summary>
/// Gossip mode constants exchanged between route servers.
/// Mirrors the const block immediately after <c>routeInfo</c> in route.go.
/// Do not change values — they are part of the wire protocol.
/// </summary>
internal static class GossipMode
{
public const byte Default = 0;
public const byte Disabled = 1;
public const byte Override = 2;
}
/// <summary>
/// Per-connection route state embedded in <see cref="ClientConnection"/> when the
/// connection kind is <c>Router</c>.
/// Mirrors Go <c>route</c> struct in route.go.
/// </summary>
internal sealed class Route
{
/// <summary>Remote server ID string.</summary>
public string RemoteId { get; set; } = string.Empty;
/// <summary>Remote server name.</summary>
public string RemoteName { get; set; } = string.Empty;
/// <summary>True if this server solicited the outbound connection.</summary>
public bool DidSolicit { get; set; }
/// <summary>True if the connection should be retried on failure.</summary>
public bool Retry { get; set; }
/// <summary>Leaf-node origin cluster flag (lnoc).</summary>
public bool Lnoc { get; set; }
/// <summary>Leaf-node origin cluster with unsub support (lnocu).</summary>
public bool Lnocu { get; set; }
/// <summary>Whether this is an explicit or implicit route.</summary>
public RouteType RouteType { get; set; }
/// <summary>Remote URL used to establish the connection.</summary>
public Uri? Url { get; set; }
/// <summary>True if the remote requires authentication.</summary>
public bool AuthRequired { get; set; }
/// <summary>True if the remote requires TLS.</summary>
public bool TlsRequired { get; set; }
/// <summary>True if JetStream is enabled on the remote.</summary>
public bool JetStream { get; set; }
/// <summary>List of client connect URLs advertised by the remote.</summary>
public List<string> ConnectUrls { get; set; } = [];
/// <summary>List of WebSocket connect URLs advertised by the remote.</summary>
public List<string> WsConnUrls { get; set; } = [];
/// <summary>Gateway URL advertised by the remote.</summary>
public string GatewayUrl { get; set; } = string.Empty;
/// <summary>Leaf-node URL advertised by the remote.</summary>
public string LeafnodeUrl { get; set; } = string.Empty;
/// <summary>Cluster hash used for routing.</summary>
public string Hash { get; set; } = string.Empty;
/// <summary>Server ID hash (6 bytes encoded).</summary>
public string IdHash { get; set; } = string.Empty;
/// <summary>
/// Index of this route in the <c>s.routes[remoteID]</c> slice.
/// Initialized to -1 to indicate the route has not yet been registered.
/// </summary>
public int PoolIdx { get; set; } = -1;
/// <summary>
/// When set, this route is pinned to a specific account and the account
/// name will not be included in routed protocols.
/// </summary>
public byte[]? AccName { get; set; }
/// <summary>True if this is a connection to an old server or one with pooling disabled.</summary>
public bool NoPool { get; set; }
/// <summary>
/// Selected compression mode, which may differ from the server-configured mode.
/// </summary>
public string Compression { get; set; } = string.Empty;
/// <summary>
/// Transient gossip mode byte sent when initiating an implicit route.
/// </summary>
public byte GossipMode { get; set; }
/// <summary>
/// When set in a pooling scenario, signals that the route should trigger
/// creation of the next pooled connection after receiving the first PONG.
/// </summary>
public RouteInfo? StartNewRoute { get; set; }
}
/// <summary>
/// Minimal descriptor used to create a new route connection, including
/// the target URL, its type, and gossip mode.
/// Mirrors Go <c>routeInfo</c> struct (the small inner type) in route.go.
/// </summary>
internal sealed class RouteInfo
{
public Uri? Url { get; set; }
public RouteType RouteType { get; set; }
public byte GossipMode { get; set; }
}
/// <summary>
/// CONNECT protocol payload exchanged between cluster servers.
/// Fields map 1-to-1 with the JSON tags in Go's <c>connectInfo</c>.
/// Mirrors Go <c>connectInfo</c> struct in route.go.
/// </summary>
internal sealed class ConnectInfo
{
[JsonPropertyName("echo")] public bool Echo { get; set; }
[JsonPropertyName("verbose")] public bool Verbose { get; set; }
[JsonPropertyName("pedantic")] public bool Pedantic { get; set; }
[JsonPropertyName("user")] public string User { get; set; } = string.Empty;
[JsonPropertyName("pass")] public string Pass { get; set; } = string.Empty;
[JsonPropertyName("tls_required")] public bool Tls { get; set; }
[JsonPropertyName("headers")] public bool Headers { get; set; }
[JsonPropertyName("name")] public string Name { get; set; } = string.Empty;
[JsonPropertyName("cluster")] public string Cluster { get; set; } = string.Empty;
[JsonPropertyName("cluster_dynamic")] public bool Dynamic { get; set; }
[JsonPropertyName("lnoc")] public bool Lnoc { get; set; }
[JsonPropertyName("lnocu")] public bool Lnocu { get; set; }
[JsonPropertyName("gateway")] public string Gateway { get; set; } = string.Empty;
}
/// <summary>
/// Holds a set of subscriptions for a single account, used when fanning out
/// route subscription interest.
/// Mirrors Go <c>asubs</c> struct in route.go.
/// </summary>
internal sealed class ASubs
{
public Account? Account { get; set; }
public List<Internal.Subscription> Subs { get; set; } = [];
}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
# NATS .NET Porting Status Report
Generated: 2026-02-26 20:46:14 UTC
Generated: 2026-02-26 20:50:51 UTC
## Modules (12 total)
@@ -13,9 +13,9 @@ Generated: 2026-02-26 20:46:14 UTC
| Status | Count |
|--------|-------|
| complete | 1382 |
| complete | 1601 |
| n_a | 82 |
| not_started | 2116 |
| not_started | 1897 |
| stub | 93 |
## Unit Tests (3257 total)
@@ -36,4 +36,4 @@ Generated: 2026-02-26 20:46:14 UTC
## Overall Progress
**1975/6942 items complete (28.5%)**
**2194/6942 items complete (31.6%)**

39
reports/report_ce45dff.md Normal file
View File

@@ -0,0 +1,39 @@
# NATS .NET Porting Status Report
Generated: 2026-02-26 20:50:51 UTC
## Modules (12 total)
| Status | Count |
|--------|-------|
| complete | 11 |
| not_started | 1 |
## Features (3673 total)
| Status | Count |
|--------|-------|
| complete | 1601 |
| n_a | 82 |
| not_started | 1897 |
| stub | 93 |
## Unit Tests (3257 total)
| Status | Count |
|--------|-------|
| complete | 319 |
| n_a | 181 |
| not_started | 2533 |
| stub | 224 |
## Library Mappings (36 total)
| Status | Count |
|--------|-------|
| mapped | 36 |
## Overall Progress
**2194/6942 items complete (31.6%)**