From 77403e3d31a7330cd80795a43fb004144b58b710 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 26 Feb 2026 15:50:51 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20port=20sessions=2014-16=20=E2=80=94=20R?= =?UTF-8?q?outes,=20Leaf=20Nodes=20&=20Gateways?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Gateway/GatewayTypes.cs | 384 ++++++++++++++++++ .../LeafNode/LeafNodeTypes.cs | 202 +++++++++ .../ZB.MOM.NatsNet.Server/NatsServerTypes.cs | 9 +- .../Routes/RouteTypes.cs | 185 +++++++++ porting.db | Bin 2473984 -> 2473984 bytes reports/current.md | 8 +- reports/report_ce45dff.md | 39 ++ 7 files changed, 816 insertions(+), 11 deletions(-) create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/LeafNode/LeafNodeTypes.cs create mode 100644 dotnet/src/ZB.MOM.NatsNet.Server/Routes/RouteTypes.cs create mode 100644 reports/report_ce45dff.md diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs new file mode 100644 index 0000000..c405948 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Gateway/GatewayTypes.cs @@ -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 +// ============================================================================ + +/// +/// Represents the interest mode for a given account on a gateway connection. +/// Mirrors Go GatewayInterestMode byte iota in gateway.go. +/// Do not change values — they are part of the wire-level gossip protocol. +/// +public enum GatewayInterestMode : byte +{ + /// + /// Default mode: the cluster sends to a gateway unless told there is no + /// interest (applies to plain subscribers only). + /// + Optimistic = 0, + + /// + /// Transitioning: the gateway has been sending too many no-interest signals + /// and is switching to mode for this account. + /// + Transitioning = 1, + + /// + /// Interest-only mode: the cluster has sent all its subscription interest; + /// the gateway only forwards messages when explicit interest is known. + /// + InterestOnly = 2, + + /// + /// Internal sentinel used after a cache flush; not part of the public wire enum. + /// + CacheFlushed = 3, +} + +/// +/// Server-level gateway state kept on the instance. +/// Replaces the stub that was in NatsServerTypes.cs. +/// Mirrors Go srvGateway struct in gateway.go. +/// +internal sealed class SrvGateway +{ + /// + /// Total number of queue subs across all remote gateways. + /// Accessed via Interlocked — must be 64-bit aligned. + /// + public long TotalQSubs; + + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + + /// + /// True if both a gateway name and port are configured (immutable after init). + /// + public bool Enabled { get; set; } + + /// Name of this server's gateway cluster. + public string Name { get; set; } = string.Empty; + + /// Outbound gateway connections keyed by remote gateway name. + public Dictionary Out { get; set; } = new(); + + /// + /// Outbound gateway connections in RTT order, used for message routing. + /// + public List Outo { get; set; } = []; + + /// Inbound gateway connections keyed by connection ID. + public Dictionary In { get; set; } = new(); + + /// Per-remote-gateway configuration, keyed by gateway name. + public Dictionary Remotes { get; set; } = new(); + + /// Reference-counted set of all gateway URLs in the cluster. + public RefCountedUrlSet Urls { get; set; } = new(); + + /// This server's own gateway URL (after random-port resolution). + public string Url { get; set; } = string.Empty; + + /// Gateway INFO protocol object. + public ServerInfo? Info { get; set; } + + /// Pre-marshalled INFO JSON bytes. + public byte[]? InfoJson { get; set; } + + /// When true, reject connections from gateways not in . + public bool RejectUnknown { get; set; } + + /// + /// Reply prefix bytes: "$GNR.<reserved>.<clusterHash>.<serverHash>." + /// + 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) + // ------------------------------------------------------------------------- + + /// + /// Per-account subject-interest tally. + /// Outer key = account name; inner key = subject (or "subject queue" pair); + /// value = tally struct. + /// Mirrors Go's anonymous pasi embedded struct in srvGateway. + /// + private readonly Lock _pasiLock = new(); + public Dictionary> Pasi { get; set; } = new(); + + public Lock PasiLock => _pasiLock; + + // ------------------------------------------------------------------------- + // Recent subscription tracking (thread-safe map) + // ------------------------------------------------------------------------- + + /// + /// Recent subscriptions for a given account (subject → expiry ticks). + /// Mirrors Go's rsubs sync.Map. + /// + public System.Collections.Concurrent.ConcurrentDictionary RSubs { get; set; } = new(); + + // ------------------------------------------------------------------------- + // Other server-level gateway fields + // ------------------------------------------------------------------------- + + /// DNS resolver used before dialling gateway connections. + public INetResolver? Resolver { get; set; } + + /// Max buffer size for sending queue-sub protocol (used in tests). + public int SqbSz { get; set; } + + /// How long to look for a subscription match for a reply message. + public TimeSpan RecSubExp { get; set; } + + /// Server ID hash (6 bytes) for routing mapped replies. + public byte[] SIdHash { get; set; } = []; + + /// + /// Map from a route server's hashed ID (6 bytes) to the route client. + /// Mirrors Go's routesIDByHash sync.Map. + /// + public System.Collections.Concurrent.ConcurrentDictionary RoutesIdByHash { get; set; } = new(); + + /// + /// Gateway URLs from this server's own entry in the Gateways config block, + /// used for monitoring reports. + /// + public List 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(); +} + +/// +/// Subject-interest tally entry. Indicates whether the key in the map is a +/// queue subscription and how many matching subscriptions exist. +/// Mirrors Go sitally struct in gateway.go. +/// +internal sealed class SitAlly +{ + /// Number of subscriptions directly matching the subject/queue key. + public int N { get; set; } + + /// True if this entry represents a queue subscription. + public bool Q { get; set; } +} + +/// +/// Runtime configuration for a single remote gateway. +/// Wraps with connection-attempt state and a lock. +/// Mirrors Go gatewayCfg struct in gateway.go. +/// +internal sealed class GatewayCfg +{ + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + + /// The raw remote-gateway options this cfg was built from. + public RemoteGatewayOpts? RemoteOpts { get; set; } + + /// 6-byte cluster hash used for reply routing. + public byte[] Hash { get; set; } = []; + + /// 4-byte old-style hash for backward compatibility. + public byte[] OldHash { get; set; } = []; + + /// Map of URL string → parsed URL for this remote gateway. + public Dictionary Urls { get; set; } = new(); + + /// Number of connection attempts made so far. + public int ConnAttempts { get; set; } + + /// TLS server name override for SNI. + public string TlsName { get; set; } = string.Empty; + + /// True if this remote was discovered via gossip (not configured). + public bool Implicit { get; set; } + + /// When true, monitoring should refresh the URL list on next varz inspection. + 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(); +} + +/// +/// Per-connection gateway state embedded in +/// when the connection kind is Gateway. +/// Mirrors Go gateway struct in gateway.go. +/// +internal sealed class Gateway +{ + /// Name of the remote gateway cluster. + public string Name { get; set; } = string.Empty; + + /// Configuration block for the remote gateway. + public GatewayCfg? Cfg { get; set; } + + /// URL used for CONNECT after receiving the remote INFO (outbound only). + public Uri? ConnectUrl { get; set; } + + /// + /// Per-account subject interest (outbound connection). + /// Maps account name → for that account. + /// Uses a thread-safe map because it is read from multiple goroutines. + /// + public System.Collections.Concurrent.ConcurrentDictionary? OutSim { get; set; } + + /// + /// Per-account no-interest subjects or interest-only mode (inbound connection). + /// + public Dictionary? InSim { get; set; } + + /// True if this is an outbound gateway connection. + public bool Outbound { get; set; } + + /// + /// Set in the read loop without locking to record that the inbound side + /// sent its CONNECT protocol. + /// + public bool Connected { get; set; } + + /// + /// True if the remote server only understands the old $GR. prefix, + /// not the newer $GNR. scheme. + /// + public bool UseOldPrefix { get; set; } + + /// + /// When true the inbound side switches accounts to interest-only mode + /// immediately, so the outbound side can disregard optimistic mode. + /// + public bool InterestOnlyMode { get; set; } + + /// Name of the remote server on this gateway connection. + public string RemoteName { get; set; } = string.Empty; +} + +/// +/// Outbound subject-interest entry for a single account on a gateway connection. +/// Mirrors Go outsie struct in gateway.go. +/// +internal sealed class OutSide +{ + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + + /// Current interest mode for this account on the outbound gateway. + public GatewayInterestMode Mode { get; set; } + + /// + /// Set of subjects for which the remote has signalled no-interest. + /// Null when the remote has sent all its subscriptions (interest-only mode). + /// + public HashSet? Ni { get; set; } + + /// + /// Subscription index: contains queue subs in optimistic mode, + /// or all subs when has been switched. + /// + public SubscriptionIndex? Sl { get; set; } + + /// Number of queue subscriptions tracked in . + 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(); +} + +/// +/// 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 insie struct in gateway.go. +/// +internal sealed class InSide +{ + /// + /// Subjects for which RS- was sent to the remote (null when in interest-only mode). + /// + public HashSet? Ni { get; set; } + + /// Current interest mode for this account on the inbound gateway. + public GatewayInterestMode Mode { get; set; } +} + +/// +/// A single gateway reply-mapping entry: the mapped subject and its expiry. +/// Mirrors Go gwReplyMap struct in gateway.go. +/// +internal sealed class GwReplyMap +{ + /// The mapped (routed) subject string. + public string Ms { get; set; } = string.Empty; + + /// Expiry expressed as (UTC). + public long Exp { get; set; } +} + +/// +/// Gateway reply routing table and a fast-path check flag. +/// Mirrors Go gwReplyMapping struct in gateway.go. +/// +internal sealed class GwReplyMapping +{ + /// + /// Non-zero when the mapping table should be consulted while processing + /// inbound messages. Accessed via Interlocked — must be 32-bit aligned. + /// + public int Check; + + /// Active reply-subject → GwReplyMap entries. + public Dictionary Mapping { get; set; } = new(); + + /// + /// Returns the routed subject for if a mapping + /// exists, otherwise returns the original subject and false. + /// Caller must hold any required lock before invoking. + /// + public (byte[] Subject, bool Found) Get(byte[] subject) + { + // TODO: session 16 — implement mapping lookup + return (subject, false); + } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/LeafNode/LeafNodeTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/LeafNode/LeafNodeTypes.cs new file mode 100644 index 0000000..2a7801f --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/LeafNode/LeafNodeTypes.cs @@ -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 +// ============================================================================ + +/// +/// Per-connection leaf-node state embedded in +/// when the connection kind is Leaf. +/// Mirrors Go leaf struct in leafnode.go. +/// +internal sealed class Leaf +{ + /// + /// Config for solicited (outbound) leaf connections; null for accepted connections. + /// + public LeafNodeCfg? Remote { get; set; } + + /// + /// True when we are the spoke side of a hub/spoke leaf pair. + /// + public bool IsSpoke { get; set; } + + /// + /// Cluster name of the remote server when we are a hub and the spoke is + /// part of a cluster. + /// + public string RemoteCluster { get; set; } = string.Empty; + + /// Remote server name or ID. + public string RemoteServer { get; set; } = string.Empty; + + /// Domain name of the remote server. + public string RemoteDomain { get; set; } = string.Empty; + + /// Account name of the remote server. + public string RemoteAccName { get; set; } = string.Empty; + + /// + /// When true, suppresses propagation of east-west interest from other leaf nodes. + /// + public bool Isolated { get; set; } + + /// + /// Subject-interest suppression map shared with the remote side. + /// Key = subject, Value = interest count (positive = subscribe, negative = unsubscribe delta). + /// + public Dictionary Smap { get; set; } = new(); + + /// + /// Short-lived set of subscriptions added during initLeafNodeSmapAndSendSubs + /// to detect and avoid double-counting races. + /// + public HashSet? Tsub { get; set; } + + /// Timer that clears after the initialization window. + public Timer? Tsubt { get; set; } + + /// + /// Selected compression mode, which may differ from the server-configured mode. + /// + public string Compression { get; set; } = string.Empty; + + /// + /// Gateway-mapped reply subscription used for GW reply routing via leaf nodes. + /// + public Subscription? GwSub { get; set; } +} + +/// +/// Runtime configuration for a remote (solicited) leaf-node connection. +/// Wraps with connection-attempt state and a +/// reader-writer lock for concurrent access. +/// Mirrors Go leafNodeCfg struct in leafnode.go. +/// Replaces the stub that was in NatsServerTypes.cs. +/// +public sealed class LeafNodeCfg +{ + private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.SupportsRecursion); + + // ------------------------------------------------------------------------- + // Embedded RemoteLeafOpts fields + // ------------------------------------------------------------------------- + + /// The raw remote options this cfg was constructed from. + public RemoteLeafOpts? RemoteOpts { get; set; } + + // ------------------------------------------------------------------------- + // Runtime connection-attempt fields + // ------------------------------------------------------------------------- + + /// Resolved URLs to attempt connections to. + public List Urls { get; set; } = []; + + /// Currently selected URL from . + public Uri? CurUrl { get; set; } + + /// TLS server name override for SNI. + public string TlsName { get; set; } = string.Empty; + + /// Username for authentication (resolved from credentials or options). + public string Username { get; set; } = string.Empty; + + /// Password for authentication (resolved from credentials or options). + public string Password { get; set; } = string.Empty; + + /// Publish/subscribe permission overrides for this connection. + public Permissions? Perms { get; set; } + + /// + /// Delay before the next connection attempt (e.g. during loop-detection back-off). + /// + public TimeSpan ConnDelay { get; set; } + + /// + /// Timer used to trigger JetStream account migration for this leaf. + /// + 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(); +} + +/// +/// CONNECT protocol payload sent by a leaf-node connection. +/// Fields map 1-to-1 with the JSON tags in Go's leafConnectInfo. +/// Mirrors Go leafConnectInfo struct in leafnode.go. +/// +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; } + + /// + /// Compression mode string. The legacy boolean field was never used; this + /// string field uses a different JSON tag to avoid conflicts. + /// + [JsonPropertyName("compress_mode")] public string Compression { get; set; } = string.Empty; + + /// + /// Used only to detect wrong-port connections (client connecting to leaf port). + /// + [JsonPropertyName("gateway")] public string Gateway { get; set; } = string.Empty; + + /// Account name the remote is binding to on the accept side. + [JsonPropertyName("remote_account")] public string RemoteAccount { get; set; } = string.Empty; + + /// + /// Protocol version sent by the soliciting side so the accepting side knows + /// which features are supported (e.g. message tracing). + /// + [JsonPropertyName("protocol")] public int Proto { get; set; } +} diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs index 29daa35..68506b3 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/NatsServerTypes.cs @@ -225,11 +225,7 @@ public sealed class JetStreamConfig public string UniqueTag { get; set; } = string.Empty; } -/// Stub for server gateway state (session 16). -internal sealed class SrvGateway -{ - public bool Enabled { get; set; } -} +// SrvGateway — full class is in Gateway/GatewayTypes.cs (session 16). /// Stub for server websocket state (session 23). internal sealed class SrvWebsocket @@ -249,8 +245,7 @@ internal interface IOcspResponseCache { } /// Stub for IP queue (session 02 — already ported as IpQueue). // IpQueue is already in session 02 internals — used here via object. -/// Stub for leaf node config (session 15). -internal sealed class LeafNodeCfg { } +// LeafNodeCfg — full class is in LeafNode/LeafNodeTypes.cs (session 15). /// /// Network resolver used by . diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Routes/RouteTypes.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Routes/RouteTypes.cs new file mode 100644 index 0000000..1449957 --- /dev/null +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Routes/RouteTypes.cs @@ -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 +// ============================================================================ + +/// +/// Designates whether a route was explicitly configured or discovered via gossip. +/// Mirrors Go RouteType iota in route.go. +/// Note: Go defines Implicit=0, Explicit=1 — we keep TombStone=2 for future use. +/// +public enum RouteType : int +{ + /// This route was learned from speaking to other routes. + Implicit = 0, + /// This route was explicitly configured. + Explicit = 1, + /// Reserved tombstone marker for removed routes. + TombStone = 2, +} + +/// +/// Gossip mode constants exchanged between route servers. +/// Mirrors the const block immediately after routeInfo in route.go. +/// Do not change values — they are part of the wire protocol. +/// +internal static class GossipMode +{ + public const byte Default = 0; + public const byte Disabled = 1; + public const byte Override = 2; +} + +/// +/// Per-connection route state embedded in when the +/// connection kind is Router. +/// Mirrors Go route struct in route.go. +/// +internal sealed class Route +{ + /// Remote server ID string. + public string RemoteId { get; set; } = string.Empty; + + /// Remote server name. + public string RemoteName { get; set; } = string.Empty; + + /// True if this server solicited the outbound connection. + public bool DidSolicit { get; set; } + + /// True if the connection should be retried on failure. + public bool Retry { get; set; } + + /// Leaf-node origin cluster flag (lnoc). + public bool Lnoc { get; set; } + + /// Leaf-node origin cluster with unsub support (lnocu). + public bool Lnocu { get; set; } + + /// Whether this is an explicit or implicit route. + public RouteType RouteType { get; set; } + + /// Remote URL used to establish the connection. + public Uri? Url { get; set; } + + /// True if the remote requires authentication. + public bool AuthRequired { get; set; } + + /// True if the remote requires TLS. + public bool TlsRequired { get; set; } + + /// True if JetStream is enabled on the remote. + public bool JetStream { get; set; } + + /// List of client connect URLs advertised by the remote. + public List ConnectUrls { get; set; } = []; + + /// List of WebSocket connect URLs advertised by the remote. + public List WsConnUrls { get; set; } = []; + + /// Gateway URL advertised by the remote. + public string GatewayUrl { get; set; } = string.Empty; + + /// Leaf-node URL advertised by the remote. + public string LeafnodeUrl { get; set; } = string.Empty; + + /// Cluster hash used for routing. + public string Hash { get; set; } = string.Empty; + + /// Server ID hash (6 bytes encoded). + public string IdHash { get; set; } = string.Empty; + + /// + /// Index of this route in the s.routes[remoteID] slice. + /// Initialized to -1 to indicate the route has not yet been registered. + /// + public int PoolIdx { get; set; } = -1; + + /// + /// When set, this route is pinned to a specific account and the account + /// name will not be included in routed protocols. + /// + public byte[]? AccName { get; set; } + + /// True if this is a connection to an old server or one with pooling disabled. + public bool NoPool { get; set; } + + /// + /// Selected compression mode, which may differ from the server-configured mode. + /// + public string Compression { get; set; } = string.Empty; + + /// + /// Transient gossip mode byte sent when initiating an implicit route. + /// + public byte GossipMode { get; set; } + + /// + /// When set in a pooling scenario, signals that the route should trigger + /// creation of the next pooled connection after receiving the first PONG. + /// + public RouteInfo? StartNewRoute { get; set; } +} + +/// +/// Minimal descriptor used to create a new route connection, including +/// the target URL, its type, and gossip mode. +/// Mirrors Go routeInfo struct (the small inner type) in route.go. +/// +internal sealed class RouteInfo +{ + public Uri? Url { get; set; } + public RouteType RouteType { get; set; } + public byte GossipMode { get; set; } +} + +/// +/// CONNECT protocol payload exchanged between cluster servers. +/// Fields map 1-to-1 with the JSON tags in Go's connectInfo. +/// Mirrors Go connectInfo struct in route.go. +/// +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; +} + +/// +/// Holds a set of subscriptions for a single account, used when fanning out +/// route subscription interest. +/// Mirrors Go asubs struct in route.go. +/// +internal sealed class ASubs +{ + public Account? Account { get; set; } + public List Subs { get; set; } = []; +} diff --git a/porting.db b/porting.db index 22ef20afedbd71fe8de49b2a846b20f5fe23a68b..0bb05ba26421b36c6fc53470a02f304b40dbef02 100644 GIT binary patch delta 15798 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 H`O*7-XZ2xO delta 12720 zcmb_?d3aOB`hU(j$w?+TCrKCDrYT8FsYpW!p_CK~wxyH;wYFtPmQwbevhS261#Db` zB6$^25Eon!3o1tt5pl&8xbD}hY>F2ZdU3gztKj#YnH0GB{riLG^m#h{%scPA^X@Zi z*MhJXzl}~I@!pIwA#u-yN4NQGpi8zE$~v8{)w;-9XMM=F-CAh7U>$7RYYW=u*hbmP zY+Y>*o8EfS`jz!9Ys(AWw-c*vgetWcC(WDFFnh|PDQd~4!Mkq~Qa8=tomTsmUCI{c z&RbNsa8dn&MN=lL`;oTqMz|>vNATZAaY$u54!#jLX#Q!_SG%9wwDw#5rl#k+I4ohr z435J{MLHb_FBx(|7oOKHJTD}z)f-!MKWK(-Vz!)d_E`h#JuVS!RHJKj_;+v z!L4Vdbm(T4?y{StHWuiyn)Gn+tmK4uevqv29EDp)8?NjW?9#C;(?;E8(W)~AO^ZxZ zO{0{x$^vDwGC~=s^j5kkF2%n29;qvg@`>qCbFZ2LCr(K{U`syaf_5DnZ-ZF3*t%fgtS7MEsd9Iq<&Hl z=_cuG=_Bc|v{!muib{7F>;_T)tNwfa7y9?~x9Gd+JLr@3vh*M6y#Anmw|=|+K7EsZ ziGI5NcAf6}4pEa(Z{ErgZK5R|%v)3^kS(eeetuBRfaCPX&Ii?8nD?Ms2+jx9VKD1< z!!TRd>jV}}u@r&%c0)Z>j=>)zw&CPO!wl`=mtlrN@QyKLz;j~^L$zBUQO$yAPJ~h0 z)Tgz1QFXllm1iXt?#F|ZO-abcFnZQC>_|I2qULJ9f_jr6njX^notI_`u=ioL0WyA) z20$VnxYm7(I!)Rjv`RaLR{aDc&VR??(rJfl3>^gRzZ=zGB}VD%muwYKwa?Z7@9(n} zYK{?x-%u%DvME}R0oLzDQOVTAv4+1M+bL{Jvg5hRnKo&-&Z_IElNO7)x^HzI!QM?y zwgvpn^AmJJsp`#l2tqcD?Iu@2_qV0YXm{BNA9jT;jap3)Iah$w#|@cKqN#S+Ss>@|1rnQAAiQs& z+?&-�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