28 KiB
Leaf Nodes — Gap Analysis
This file tracks what has and hasn't been ported from Go to .NET for the Leaf Nodes module. See stillmissing.md for the full LOC comparison across all modules.
LLM Instructions: How to Analyze This Category
Step 1: Read the Go Reference Files
Read each Go source file listed below. For every file:
- Extract all exported types (structs, interfaces, type aliases)
- Extract all exported methods on those types (receiver functions)
- Extract all exported standalone functions
- Note key constants, enums, and protocol states
- Note important unexported helpers that implement core logic (functions >20 lines)
- Pay attention to concurrency patterns (goroutines, mutexes, channels) — these map to different .NET patterns
Step 2: Read the .NET Implementation Files
Read all .cs files in the .NET directories listed below. For each Go symbol found in Step 1:
- Search for a matching type, method, or function in .NET
- If found, compare the behavior: does it handle the same edge cases? Same error paths?
- If partially implemented, note what's missing
- If not found, note it as MISSING
Step 3: Cross-Reference Tests
Compare Go test functions against .NET test methods:
- For each Go
Test*function, check if a corresponding .NET[Fact]or[Theory]exists - Note which test scenarios are covered and which are missing
- Check the parity DB (
docs/test_parity.db) for existing mappings:sqlite3 docs/test_parity.db "SELECT go_test, dotnet_test, confidence FROM test_mappings tm JOIN go_tests gt ON tm.go_test_id=gt.rowid JOIN dotnet_tests dt ON tm.dotnet_test_id=dt.rowid WHERE gt.go_file LIKE '%PATTERN%'"
Step 4: Classify Each Item
Use these status values:
| Status | Meaning |
|---|---|
| PORTED | Equivalent exists in .NET with matching behavior |
| PARTIAL | .NET implementation exists but is incomplete (missing edge cases, error handling, or features) |
| MISSING | No .NET equivalent found — needs to be ported |
| NOT_APPLICABLE | Go-specific pattern that doesn't apply to .NET (build tags, platform-specific goroutine tricks, etc.) |
| DEFERRED | Intentionally skipped for now (document why) |
Step 5: Fill In the Gap Inventory
Add rows to the Gap Inventory table below. Group by Go source file. Include the Go file and line number so a porting LLM can jump directly to the reference implementation.
Key Porting Notes for Leaf Nodes
- Leaf nodes only share subscription interest with the hub — no full mesh.
- Loop detection uses the
$LDS.subject prefix. - Leaf connections use
ClientKind = LEAF. - Leaf nodes can connect through WebSocket and support TLS.
Go Reference Files (Source)
golang/nats-server/server/leafnode.go— Hub-and-spoke topology for edge deployments (~3,470 lines). Only subscribed subjects shared with hub. Loop detection via$LDS.prefix.
Go Reference Files (Tests)
golang/nats-server/server/leafnode_test.gogolang/nats-server/server/leafnode_proxy_test.gogolang/nats-server/test/leafnode_test.go(integration)
.NET Implementation Files (Source)
src/NATS.Server/LeafNodes/(all files)
.NET Implementation Files (Tests)
tests/NATS.Server.Tests/LeafNodes/
Gap Inventory
golang/nats-server/server/leafnode.go
Constants and Types
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|---|---|---|---|---|
leafnodeTLSInsecureWarning (const) |
golang/nats-server/server/leafnode.go:47 |
PORTED | src/NATS.Server/LeafNodes/LeafNodeManager.cs |
Warning logged in DisableLeafConnect; same semantic intent, no separate constant |
leafNodeReconnectDelayAfterLoopDetected (const) |
golang/nats-server/server/leafnode.go:50 |
MISSING | — | 30s reconnect delay after loop detection. .NET loop detector (LeafLoopDetector) detects but does not enforce the delay on reconnect |
leafNodeReconnectAfterPermViolation (const) |
golang/nats-server/server/leafnode.go:54 |
MISSING | — | 30s reconnect delay after permission violation. No .NET equivalent enforced |
leafNodeReconnectDelayAfterClusterNameSame (const) |
golang/nats-server/server/leafnode.go:57 |
MISSING | — | 30s delay when same cluster name detected. No .NET equivalent |
leafNodeLoopDetectionSubjectPrefix (const "$LDS.") |
golang/nats-server/server/leafnode.go:60 |
PORTED | src/NATS.Server/LeafNodes/LeafLoopDetector.cs:5 |
LeafLoopPrefix = "$LDS." |
leafNodeWSPath (const "/leafnode") |
golang/nats-server/server/leafnode.go:64 |
PORTED | src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs |
Path constant is implicit in the WS adapter; not a named constant in .NET |
leafNodeWaitBeforeClose (const 5s) |
golang/nats-server/server/leafnode.go:68 |
MISSING | — | Minimum version wait-before-close timer. Not ported |
leaf (unexported struct) |
golang/nats-server/server/leafnode.go:71 |
PARTIAL | src/NATS.Server/LeafNodes/LeafConnection.cs |
LeafConnection covers remote, isSpoke, remoteCluster, remoteServer, remoteDomain. Missing: isolated, smap, tsub/tsubt (transient sub map), compression, gwSub |
leafNodeCfg (unexported struct) |
golang/nats-server/server/leafnode.go:107 |
PARTIAL | src/NATS.Server/Configuration/LeafNodeOptions.cs |
RemoteLeafOptions covers URLs, credentials, local account. Missing: curURL, tlsName, username/password (runtime fields), perms, connDelay, jsMigrateTimer |
leafConnectInfo (unexported struct) |
golang/nats-server/server/leafnode.go:2001 |
MISSING | — | JSON CONNECT payload for leaf solicited connections (JWT, Nkey, Sig, Hub, Cluster, Headers, JetStream, Compression, RemoteAccount, Proto). Not represented in .NET |
Methods on client (receiver functions)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|---|---|---|---|---|
(c *client) isSolicitedLeafNode() |
golang/nats-server/server/leafnode.go:121 |
MISSING | — | No client type in .NET; LeafConnection does not track solicited vs. accepted role |
(c *client) isSpokeLeafNode() |
golang/nats-server/server/leafnode.go:127 |
MISSING | — | Hub/spoke role tracking missing in .NET |
(c *client) isHubLeafNode() |
golang/nats-server/server/leafnode.go:131 |
MISSING | — | Hub role helper missing in .NET |
(c *client) isIsolatedLeafNode() |
golang/nats-server/server/leafnode.go:135 |
MISSING | — | Isolation flag not tracked in .NET |
(c *client) sendLeafConnect(clusterName, headers) |
golang/nats-server/server/leafnode.go:969 |
MISSING | — | Sends CONNECT JSON payload (JWT/NKey/creds auth) on solicited connections. .NET handshake only sends LEAF <id> line |
(c *client) leafClientHandshakeIfNeeded(remote, opts) |
golang/nats-server/server/leafnode.go:1402 |
PARTIAL | src/NATS.Server/LeafNodes/LeafConnection.cs:80 |
.NET PerformOutboundHandshakeAsync performs the handshake but without TLS negotiation or TLS-first logic |
(c *client) processLeafnodeInfo(info) |
golang/nats-server/server/leafnode.go:1426 |
MISSING | — | Complex INFO protocol processing (TLS negotiation, compression selection, URL updates, permission updates). Not ported |
(c *client) updateLeafNodeURLs(info) |
golang/nats-server/server/leafnode.go:1711 |
MISSING | — | Dynamically updates remote URL list from async INFO. Not ported |
(c *client) doUpdateLNURLs(cfg, scheme, URLs) |
golang/nats-server/server/leafnode.go:1732 |
MISSING | — | Helper for updateLeafNodeURLs. Not ported |
(c *client) remoteCluster() |
golang/nats-server/server/leafnode.go:2235 |
MISSING | — | Returns remote cluster name. Not tracked in .NET |
(c *client) updateSmap(sub, delta, isLDS) |
golang/nats-server/server/leafnode.go:2522 |
MISSING | — | Core subject-map delta updates. .NET has PropagateLocalSubscription but no per-connection smap with refcounting |
(c *client) forceAddToSmap(subj) |
golang/nats-server/server/leafnode.go:2567 |
MISSING | — | Force-inserts a subject into the smap. Not ported |
(c *client) forceRemoveFromSmap(subj) |
golang/nats-server/server/leafnode.go:2584 |
MISSING | — | Force-removes a subject from the smap. Not ported |
(c *client) sendLeafNodeSubUpdate(key, n) |
golang/nats-server/server/leafnode.go:2607 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:265 |
PropagateLocalSubscription / PropagateLocalUnsubscription send LS+/LS-. Missing: spoke permission check before sending, queue weight encoding |
(c *client) writeLeafSub(w, key, n) |
golang/nats-server/server/leafnode.go:2687 |
PARTIAL | src/NATS.Server/LeafNodes/LeafConnection.cs:108 |
SendLsPlusAsync/SendLsMinusAsync write LS+/LS-. Missing: queue weight (n) in LS+ for queue subs |
(c *client) processLeafSub(argo) |
golang/nats-server/server/leafnode.go:2720 |
PARTIAL | src/NATS.Server/LeafNodes/LeafConnection.cs:190 |
Read loop parses LS+ lines. Missing: loop detection check, permission check, subscription insertion into SubList, route/gateway propagation, queue weight delta handling |
(c *client) handleLeafNodeLoop(sendErr) |
golang/nats-server/server/leafnode.go:2860 |
PARTIAL | src/NATS.Server/LeafNodes/LeafLoopDetector.cs:13 |
IsLooped detects the condition. Missing: sending the error back to remote, closing connection, setting reconnect delay |
(c *client) processLeafUnsub(arg) |
golang/nats-server/server/leafnode.go:2875 |
PARTIAL | src/NATS.Server/LeafNodes/LeafConnection.cs:200 |
Read loop parses LS- lines. Missing: SubList removal, route/gateway propagation |
(c *client) processLeafHeaderMsgArgs(arg) |
golang/nats-server/server/leafnode.go:2917 |
MISSING | — | Parses LMSG header arguments (header size + total size for NATS headers protocol). Not ported |
(c *client) processLeafMsgArgs(arg) |
golang/nats-server/server/leafnode.go:3001 |
PARTIAL | src/NATS.Server/LeafNodes/LeafConnection.cs:213 |
.NET read loop parses LMSG lines. Missing: reply indicator (+/` |
(c *client) processInboundLeafMsg(msg) |
golang/nats-server/server/leafnode.go:3072 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:248 |
ForwardMessageAsync forwards to all connections; inbound path calls _messageSink. Missing: SubList match + fanout to local subscribers, L1 result cache, gateway forwarding |
(c *client) leafSubPermViolation(subj) |
golang/nats-server/server/leafnode.go:3148 |
MISSING | — | Handles subscription permission violation (log + close). Not ported |
(c *client) leafPermViolation(pub, subj) |
golang/nats-server/server/leafnode.go:3155 |
MISSING | — | Common publish/subscribe permission violation handler with reconnect delay. Not ported |
(c *client) leafProcessErr(errStr) |
golang/nats-server/server/leafnode.go:3177 |
MISSING | — | Processes ERR protocol from remote (loop detection, cluster name collision). Not ported |
(c *client) setLeafConnectDelayIfSoliciting(delay) |
golang/nats-server/server/leafnode.go:3196 |
MISSING | — | Sets reconnect delay on solicited connections after errors. Not ported |
(c *client) leafNodeGetTLSConfigForSolicit(remote) |
golang/nats-server/server/leafnode.go:3215 |
MISSING | — | Derives TLS config for solicited connection. .NET has no real TLS handshake for leaf nodes |
(c *client) leafNodeSolicitWSConnection(opts, rURL, remote) |
golang/nats-server/server/leafnode.go:3253 |
PARTIAL | src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs |
WebSocketStreamAdapter adapts a WebSocket to a Stream. Missing: HTTP upgrade negotiation (GET /leafnode request/response), TLS handshake, compression negotiation, no-masking header |
Methods on Server
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|---|---|---|---|---|
(s *Server) solicitLeafNodeRemotes(remotes) |
golang/nats-server/server/leafnode.go:144 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:200 |
StartAsync iterates _options.Remotes and spawns ConnectSolicitedWithRetryAsync. Missing: credentials file validation, system account delay, disabled-remote filtering, per-remote NKey/JWT auth |
(s *Server) remoteLeafNodeStillValid(remote) |
golang/nats-server/server/leafnode.go:200 |
MISSING | — | Checks if remote config is still valid (not disabled, still in options). No equivalent in .NET |
(s *Server) updateRemoteLeafNodesTLSConfig(opts) |
golang/nats-server/server/leafnode.go:432 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:157 |
UpdateTlsConfig updates cert/key paths. Missing: actual TLS config propagation to existing connections |
(s *Server) reConnectToRemoteLeafNode(remote) |
golang/nats-server/server/leafnode.go:458 |
PORTED | src/NATS.Server/LeafNodes/LeafNodeManager.cs:583 |
ConnectSolicitedWithRetryAsync implements reconnect loop with exponential backoff |
(s *Server) setLeafNodeNonExportedOptions() |
golang/nats-server/server/leafnode.go:549 |
NOT_APPLICABLE | — | Sets test-only options (dialTimeout, resolver). .NET uses DI/options; no direct equivalent needed |
(s *Server) connectToRemoteLeafNode(remote, firstConnect) |
golang/nats-server/server/leafnode.go:625 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:583 |
ConnectSolicitedWithRetryAsync covers basic TCP connect + retry. Missing: proxy tunnel support, system account delay for first connect, JetStream migrate timer, isLeafConnectDisabled check |
(s *Server) clearObserverState(remote) |
golang/nats-server/server/leafnode.go:768 |
MISSING | — | Clears JetStream RAFT observer state after reconnect. RAFT not ported |
(s *Server) checkJetStreamMigrate(remote) |
golang/nats-server/server/leafnode.go:802 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:367 |
CheckJetStreamMigrate validates domain conflicts. Missing: actual RAFT StepDown/SetObserver calls |
(s *Server) isLeafConnectDisabled() |
golang/nats-server/server/leafnode.go:844 |
PORTED | src/NATS.Server/LeafNodes/LeafNodeManager.cs:90 |
IsLeafConnectDisabled(remoteUrl) / IsGloballyDisabled |
(s *Server) startLeafNodeAcceptLoop() |
golang/nats-server/server/leafnode.go:875 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:200 |
StartAsync starts an accept loop. Missing: TLS setup, INFO JSON generation, nonce generation, async INFO propagation, setLeafNodeInfoHostPortAndIP |
(s *Server) copyLeafNodeInfo() |
golang/nats-server/server/leafnode.go:1083 |
MISSING | — | Deep-copies the leaf node INFO struct. No Info type in .NET leaf module |
(s *Server) addLeafNodeURL(urlStr) |
golang/nats-server/server/leafnode.go:1096 |
MISSING | — | Adds a leaf URL from route and regenerates INFO JSON. Not ported |
(s *Server) removeLeafNodeURL(urlStr) |
golang/nats-server/server/leafnode.go:1108 |
MISSING | — | Removes a leaf URL and regenerates INFO JSON. Not ported |
(s *Server) generateLeafNodeInfoJSON() |
golang/nats-server/server/leafnode.go:1122 |
MISSING | — | Regenerates the serialized INFO JSON bytes. Not ported |
(s *Server) sendAsyncLeafNodeInfo() |
golang/nats-server/server/leafnode.go:1131 |
MISSING | — | Sends async INFO to all connected leaf nodes (URL list updates). Not ported |
(s *Server) createLeafNode(conn, rURL, remote, ws) |
golang/nats-server/server/leafnode.go:1140 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:566 |
HandleInboundAsync / ConnectSolicitedAsync create connections. Missing: client struct setup, maxPay/maxSubs, TLS negotiation, websocket detection, auth timer, temp client registration, read/write loop spawning |
(s *Server) negotiateLeafCompression(c, didSolicit, infoCompression, co) |
golang/nats-server/server/leafnode.go:1648 |
MISSING | — | Negotiates S2 compression mode between hub and leaf. Not ported |
(s *Server) setLeafNodeInfoHostPortAndIP() |
golang/nats-server/server/leafnode.go:1763 |
MISSING | — | Sets advertise host/port for leaf INFO. Not ported |
(s *Server) addLeafNodeConnection(c, srvName, clusterName, checkForDup) |
golang/nats-server/server/leafnode.go:1811 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:693 |
ValidateRemoteLeafNode checks for duplicate + domain conflict. Missing: JetStream domain API deny-list merging, system account detection, RAFT observer mode toggling, JS mapping table setup, actual s.leafs map insertion |
(s *Server) removeLeafNodeConnection(c) |
golang/nats-server/server/leafnode.go:1975 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:671 |
WatchConnectionAsync removes connection on close. Missing: gwSub removal from gwLeafSubs, tsubt timer stop, proxyKey removal |
(s *Server) checkInternalSyncConsumers(acc) |
golang/nats-server/server/leafnode.go:2193 |
MISSING | — | Kicks JetStream source/mirror consumers after leaf connect. Requires full JetStream integration |
(s *Server) sendPermsAndAccountInfo(c) |
golang/nats-server/server/leafnode.go:2244 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:292 |
SendPermsAndAccountInfo syncs allow-lists to connection. Missing: actual INFO JSON protocol send to remote, IsSystemAccount flag, ConnectInfo flag |
(s *Server) initLeafNodeSmapAndSendSubs(c) |
golang/nats-server/server/leafnode.go:2264 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:325 |
InitLeafNodeSmapAndSendSubs sends LS+ for a list of subjects. Missing: gathering subs from SubList, loop detection subject, gateway interest, siReply prefix, tsub transient map, spoke-only local sub filtering |
(s *Server) updateInterestForAccountOnGateway(accName, sub, delta) |
golang/nats-server/server/leafnode.go:2428 |
MISSING | — | Called from gateway code to update leaf node smap. Requires gateway integration |
(s *Server) leafNodeResumeConnectProcess(c) |
golang/nats-server/server/leafnode.go:3369 |
MISSING | — | Sends CONNECT protocol and starts write loop on solicited leaf. Not ported (no CONNECT handshake) |
(s *Server) leafNodeFinishConnectProcess(c) |
golang/nats-server/server/leafnode.go:3409 |
MISSING | — | Registers leaf with account, initialises smap, sends sys connect event. Not ported |
Methods on Account
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|---|---|---|---|---|
(acc *Account) updateLeafNodesEx(sub, delta, hubOnly) |
golang/nats-server/server/leafnode.go:2442 |
MISSING | — | Propagates sub interest delta to all leaf connections for the account, with hub-only option |
(acc *Account) updateLeafNodes(sub, delta) |
golang/nats-server/server/leafnode.go:2515 |
PARTIAL | src/NATS.Server/LeafNodes/LeafNodeManager.cs:265 |
PropagateLocalSubscription / PropagateLocalUnsubscription broadcast to all connections. Missing: per-connection smap refcounting, isolated/hub-only filtering, origin-cluster filtering |
Methods on leafNodeCfg
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|---|---|---|---|---|
(cfg *leafNodeCfg) pickNextURL() |
golang/nats-server/server/leafnode.go:510 |
MISSING | — | Round-robins through URL list. .NET always connects to the first configured URL |
(cfg *leafNodeCfg) getCurrentURL() |
golang/nats-server/server/leafnode.go:525 |
MISSING | — | Returns current URL. Not tracked in .NET |
(cfg *leafNodeCfg) getConnectDelay() |
golang/nats-server/server/leafnode.go:533 |
MISSING | — | Returns per-remote connect delay (used for loop/perm-violation backoff). Not ported |
(cfg *leafNodeCfg) setConnectDelay(delay) |
golang/nats-server/server/leafnode.go:541 |
MISSING | — | Sets per-remote connect delay. Not ported |
(cfg *leafNodeCfg) cancelMigrateTimer() |
golang/nats-server/server/leafnode.go:761 |
MISSING | — | Cancels the JetStream migration timer. No timer in .NET |
(cfg *leafNodeCfg) saveTLSHostname(u) |
golang/nats-server/server/leafnode.go:858 |
MISSING | — | Saves TLS hostname from URL for SNI. Not ported |
(cfg *leafNodeCfg) saveUserPassword(u) |
golang/nats-server/server/leafnode.go:866 |
MISSING | — | Saves username/password from URL for bare-URL fallback. Not ported |
Standalone Functions
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|---|---|---|---|---|
validateLeafNode(o *Options) |
golang/nats-server/server/leafnode.go:214 |
MISSING | — | Validates all leaf node config options (accounts, operator mode, TLS, proxy, compression). Not ported |
checkLeafMinVersionConfig(mv) |
golang/nats-server/server/leafnode.go:343 |
MISSING | — | Validates minimum version string. Not ported |
validateLeafNodeAuthOptions(o) |
golang/nats-server/server/leafnode.go:357 |
MISSING | — | Validates single-user vs. multi-user leaf auth config. Not ported |
validateLeafNodeProxyOptions(remote) |
golang/nats-server/server/leafnode.go:377 |
MISSING | — | Validates HTTP proxy options for WebSocket leaf remotes. Not ported |
newLeafNodeCfg(remote) |
golang/nats-server/server/leafnode.go:470 |
PARTIAL | src/NATS.Server/Configuration/LeafNodeOptions.cs |
RemoteLeafOptions covers URLs and credentials. Missing: URL randomization, per-URL TLS hostname/password extraction, WS TLS detection |
establishHTTPProxyTunnel(proxyURL, targetHost, timeout, username, password) |
golang/nats-server/server/leafnode.go:565 |
MISSING | — | Establishes an HTTP CONNECT tunnel through an HTTP proxy for WebSocket leaf connections. Not ported |
keyFromSub(sub) |
golang/nats-server/server/leafnode.go:2638 |
MISSING | — | Builds smap key "subject" or "subject queue". Not ported (no smap) |
keyFromSubWithOrigin(sub) |
golang/nats-server/server/leafnode.go:2664 |
MISSING | — | Builds routed smap key with origin cluster prefix. Not ported |
Constants in smap key helpers
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|---|---|---|---|---|
keyRoutedSub, keyRoutedSubByte (const "R") |
golang/nats-server/server/leafnode.go:2651 |
MISSING | — | Prefix for routed plain subs. No smap in .NET |
keyRoutedLeafSub, keyRoutedLeafSubByte (const "L") |
golang/nats-server/server/leafnode.go:2653 |
MISSING | — | Prefix for routed leaf subs. No smap in .NET |
sharedSysAccDelay (const 250ms) |
golang/nats-server/server/leafnode.go:562 |
MISSING | — | System account shared delay before first connect. Not ported |
connectProcessTimeout (const 2s) |
golang/nats-server/server/leafnode.go:3365 |
MISSING | — | Timeout for the leaf connect process. Not ported |
.NET-only additions (no Go equivalent — extensions)
| .NET Symbol | .NET File:Line | Notes |
|---|---|---|
LeafConnection.SetPermissions() |
src/NATS.Server/LeafNodes/LeafConnection.cs:66 |
Allows-list sync API — exposes what Go does internally |
LeafNodeManager.DisableLeafConnect() / EnableLeafConnect() |
src/NATS.Server/LeafNodes/LeafNodeManager.cs:98 |
Per-remote disable/enable API (Go uses a simple bool flag) |
LeafNodeManager.DisableAllLeafConnections() / EnableAllLeafConnections() |
src/NATS.Server/LeafNodes/LeafNodeManager.cs:121 |
Global disable API |
LeafNodeManager.ComputeBackoff(attempt) |
src/NATS.Server/LeafNodes/LeafNodeManager.cs:541 |
Exponential backoff utility (Go uses fixed reconnectDelay + jitter) |
LeafNodeManager.RegisterLeafNodeCluster() / UnregisterLeafNodeCluster() |
src/NATS.Server/LeafNodes/LeafNodeManager.cs:446 |
Cluster topology registry (partially mirrors Go's registerLeafNodeCluster) |
LeafHubSpokeMapper.Map() |
src/NATS.Server/LeafNodes/LeafHubSpokeMapper.cs:77 |
Account mapping hub↔spoke (Go does this inline in client publish path) |
LeafHubSpokeMapper.IsSubjectAllowed() |
src/NATS.Server/LeafNodes/LeafHubSpokeMapper.cs:92 |
Allow/deny list filtering (mirrors Go's permission check in sendLeafNodeSubUpdate) |
LeafLoopDetector.Mark() / IsLooped() / TryUnmark() |
src/NATS.Server/LeafNodes/LeafLoopDetector.cs |
Loop detection helpers (Go does this inline in processLeafSub) |
WebSocketStreamAdapter |
src/NATS.Server/LeafNodes/WebSocketStreamAdapter.cs |
WebSocket→Stream adapter; Go uses its own ws read/write path in client.go |
LeafPermSyncResult, LeafTlsReloadResult, LeafValidationResult |
src/NATS.Server/LeafNodes/LeafNodeManager.cs:758 |
Result types for .NET API surface (Go returns error tuples) |
JetStreamMigrationResult, JetStreamMigrationStatus |
src/NATS.Server/LeafNodes/LeafNodeManager.cs:804 |
JetStream migration result type |
LeafClusterInfo |
src/NATS.Server/LeafNodes/LeafNodeManager.cs:829 |
Cluster topology entry (partial Go analog) |
Summary
| Status | Count |
|---|---|
| PORTED | 5 |
| PARTIAL | 18 |
| MISSING | 38 |
| NOT_APPLICABLE | 1 |
| DEFERRED | 0 |
| Total | 62 |
Key Gaps
The .NET leaf node implementation is a structural scaffold — the basic connection lifecycle (accept/connect, LS+/LS- propagation, LMSG forwarding, loop detection) is present, but significant protocol depth is missing:
- No CONNECT protocol: Go sends a full JSON CONNECT (with JWT/NKey/credentials auth, headers support, compression mode, hub/spoke role) before registering. .NET sends a simple
LEAF <id>line. - No smap (subject map): Go maintains a per-connection reference-counted map (
leaf.smap) to deduplicate LS+/LS- traffic. .NET broadcasts blindly to all connections. - No INFO protocol handling: Dynamic URL list updates, compression negotiation, and permission updates over async INFO are unimplemented.
- No compression: S2 compression negotiation between hub and leaf is entirely absent.
- No HTTP proxy tunnel:
establishHTTPProxyTunnelfor WebSocket-via-proxy leaf connections is not ported. - No SubList integration: Inbound leaf subscriptions (LS+) are not inserted into the server's SubList trie, so received leaf subscriptions do not create actual server-side subscription state for message routing.
- No route/gateway propagation: When a leaf sub arrives, Go also updates routes and gateways; .NET does not.
- No reconnect delay enforcement: After loop detection, permission violations, or cluster name collision, Go enforces a 30-second reconnect delay; .NET detects but does not enforce.
- No JetStream RAFT integration:
clearObserverState,checkJetStreamMigrate(actual RAFT step-down), andcheckInternalSyncConsumersare all absent. - WebSocket path incomplete: The
WebSocketStreamAdapteradapts the byte stream, but the HTTP upgrade handshake (GET /leafnodewithSec-WebSocket-Key) is not implemented for solicited WS leaf connections.
Keeping This File Updated
After porting work is completed:
- Update status: Change
MISSING → PORTEDorPARTIAL → PORTEDfor each item completed - Add .NET path: Fill in the ".NET Equivalent" column with the actual file:line
- Re-count LOC: Update the LOC numbers in
stillmissing.md:# Re-count .NET source LOC for this module find src/NATS.Server/LeafNodes/ -name '*.cs' -type f -exec cat {} + | wc -l # Re-count .NET test LOC for this module find tests/NATS.Server.Tests/LeafNodes/ -name '*.cs' -type f -exec cat {} + | wc -l - Add a changelog entry below with date and summary of what was ported
- Update the parity DB if new test mappings were created:
sqlite3 docs/test_parity.db "INSERT INTO test_mappings (go_test_id, dotnet_test_id, confidence, notes) VALUES (?, ?, 'manual', 'ported in YYYY-MM-DD session')"
Change Log
| Date | Change | By |
|---|---|---|
| 2026-02-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated: 62 symbols classified (5 PORTED, 18 PARTIAL, 38 MISSING, 1 NOT_APPLICABLE) | claude-sonnet-4-6 |