# Routes — Gap Analysis > This file tracks what has and hasn't been ported from Go to .NET for the **Routes** module. > See [stillmissing.md](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: 1. Extract all **exported types** (structs, interfaces, type aliases) 2. Extract all **exported methods** on those types (receiver functions) 3. Extract all **exported standalone functions** 4. Note **key constants, enums, and protocol states** 5. Note **important unexported helpers** that implement core logic (functions >20 lines) 6. 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: 1. Search for a matching type, method, or function in .NET 2. If found, compare the behavior: does it handle the same edge cases? Same error paths? 3. If partially implemented, note what's missing 4. If not found, note it as MISSING ### Step 3: Cross-Reference Tests Compare Go test functions against .NET test methods: 1. For each Go `Test*` function, check if a corresponding .NET `[Fact]` or `[Theory]` exists 2. Note which test scenarios are covered and which are missing 3. Check the parity DB (`docs/test_parity.db`) for existing mappings: ```bash 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 Routes - Route connections use the same `client.go` read/write loop but with `ClientKind = ROUTER`. - Route pooling sends different accounts over different connections to avoid head-of-line blocking. - `RS+`/`RS-` are subscription interest propagation messages between clustered servers. - `RMSG` is the routed message format (differs from client `MSG`). --- ## Go Reference Files (Source) - `golang/nats-server/server/route.go` — Full-mesh cluster routes (~3,300 lines). Route pooling (default 3 connections per peer). Account-specific dedicated routes. Protocol: `RS+`/`RS-` for subscribe propagation, `RMSG` for routed messages. ## Go Reference Files (Tests) - `golang/nats-server/server/routes_test.go` - `golang/nats-server/test/routes_test.go` (integration) - `golang/nats-server/test/new_routes_test.go` (integration) ## .NET Implementation Files (Source) - `src/NATS.Server/Routes/` (all files) ## .NET Implementation Files (Tests) - `tests/NATS.Server.Tests/Routes/` --- ## Gap Inventory ### `golang/nats-server/server/route.go` | Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | |-----------|:-------------|--------|:----------------|-------| | `RouteType` (type alias + consts `Implicit`/`Explicit`) | route.go:36–44 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs` — implicit/explicit distinction tracked in `RouteConnection` handshake and `RouteManager.ConnectToRouteWithRetryAsync` | No explicit enum; Implicit/Explicit distinction is encoded in how routes are established (solicited vs inbound) | | `route` struct (unexported) | route.go:56–94 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:8` | Fields `remoteID`, `poolIdx`, `accName`, `noPool`, `compression`, `gossipMode` are present. Fields for `lnoc`, `lnocu`, `jetstream`, `connectURLs`, `wsConnURLs`, `gatewayURL`, `leafnodeURL`, `hash`, `idHash`, `startNewRoute`, `retry` are MISSING — not modelled in .NET | | `routeInfo` struct (unexported) | route.go:97–101 | MISSING | — | Used internally for deferred pool-connection creation after first PONG; no .NET equivalent | | `gossipDefault`/`gossipDisabled`/`gossipOverride` consts | route.go:104–108 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:17–19` | Gossip mode constants are defined as byte constants (`GossipDefault`, `GossipDisabled`, `GossipOverride`) | | `connectInfo` struct | route.go:110–124 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:378` (`BuildConnectInfoJson`) | Connect payload now includes parity fields: `echo`, `verbose`, `pedantic`, `tls_required`, `headers`, `cluster`, `dynamic`, `lnoc`, `lnocu` | | `ConProto`/`InfoProto` protocol format strings | route.go:127–130 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:11–12,83–95` | CONNECT/INFO format constants added, but active wire handshake remains simplified `ROUTE ` rather than full CONNECT/INFO exchange | | `clusterTLSInsecureWarning` const | route.go:134 | NOT_APPLICABLE | — | TLS not yet implemented in .NET port; warning string has no counterpart | | `defaultRouteMaxPingInterval` const | route.go:140 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:20` | `DefaultRouteMaxPingInterval` constant added | | `routeConnectDelay`/`routeConnectMaxDelay`/`routeMaxPingInterval` vars | route.go:145–148 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:14–15,20,751–757` | Route reconnect delay now uses bounded exponential backoff (`ComputeRetryDelay`) with dedicated delay/max constants; still not runtime-configurable from route config | | `(c *client) removeReplySub` | route.go:151 | MISSING | — | Reply-sub cleanup for remote reply subs not implemented | | `(c *client) processAccountSub` | route.go:167 | NOT_APPLICABLE | — | Gateway-only path; gateway sub interest not in routes module | | `(c *client) processAccountUnsub` | route.go:174 | NOT_APPLICABLE | — | Gateway-only path | | `(c *client) processRoutedOriginClusterMsgArgs` | route.go:182 | MISSING | — | LMSG (origin-cluster routed msg) arg parsing not implemented; .NET only handles RMSG | | `(c *client) processRoutedHeaderMsgArgs` | route.go:281 | MISSING | — | HMSG (header msg from route) arg parsing not implemented | | `(c *client) processRoutedMsgArgs` | route.go:378 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:187–222` (`ReadFramesAsync`) | .NET parses basic RMSG account/subject/reply/size. Missing: queue-group routing indicators (`+`/`|`), header size field, multi-field arg parsing | | `(c *client) processInboundRoutedMsg` | route.go:460 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:219–221` | .NET fires `RoutedMessageReceived` callback; missing: stats update, gateway reply handling (`handleGatewayReply`), account lookup and fanout via `processMsgResults` | | `(c *client) sendRouteConnect` | route.go:503 | MISSING | — | Outbound CONNECT protocol (with cluster auth, TLS flags, etc.) not sent; .NET uses a simpler `ROUTE ` handshake | | `computeRoutePoolIdx` | route.go:538 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:149` (`ComputeRoutePoolIdx`) | FNV-1a 32-bit hash, identical algorithm | | `(c *client) processRouteInfo` | route.go:549 | MISSING | — | Full INFO processing (cluster name negotiation, compression negotiation, duplicate detection, account route setup) not implemented; .NET handshake is a simple ID exchange | | `(s *Server) negotiateRouteCompression` | route.go:897 | PARTIAL | `src/NATS.Server/Routes/RouteCompressionCodec.cs:82` (`NegotiateCompression`) | .NET has the negotiation logic; but integration into handshake (INFO exchange, switching compression writer/reader mid-stream) is MISSING | | `(s *Server) updateRemoteRoutePerms` | route.go:953 | MISSING | — | Route permission update on INFO reload not implemented | | `(s *Server) sendAsyncInfoToClients` | route.go:1015 | PORTED | `src/NATS.Server/NatsServer.cs:181` | Added `UpdateServerINFOAndSendINFOToClients()` INFO refresh/broadcast path to connected clients; validated with targeted socket-level parity test. | | `(s *Server) processImplicitRoute` | route.go:1043 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:115` (`ProcessImplicitRoute`) | Now guards discovered URLs with `HasThisRouteConfigured` and normalized duplicate checks; pinned-account re-solicitation and duplicate-ID handling remain missing | | `(s *Server) hasThisRouteConfigured` | route.go:1104 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:164` | Implemented with normalized URL matching against explicit configured routes and known route URLs | | `(s *Server) forwardNewRouteInfoToKnownServers` | route.go:1139 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:127` (`ForwardNewRouteInfoToKnownServers`) | .NET raises an event with the new peer URL; missing: gossip mode logic (`gossipDefault`/`gossipDisabled`/`gossipOverride`), pinned-account route filtering, serialized INFO JSON sending | | `(c *client) canImport` | route.go:1226 | MISSING | — | Route import permission check not implemented | | `(c *client) canExport` | route.go:1235 | MISSING | — | Route export permission check not implemented | | `(c *client) setRoutePermissions` | route.go:1244 | MISSING | — | Route permission mapping (Import→Publish, Export→Subscribe) not implemented | | `asubs` struct | route.go:1263 | NOT_APPLICABLE | — | Internal Go helper to group subscriptions by account during cleanup; .NET uses LINQ equivalents | | `getAccNameFromRoutedSubKey` | route.go:1273 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:176` (`GetAccNameFromRoutedSubKey`) | Routed-sub key parsing helper added and validated with parity tests | | `(c *client) getRoutedSubKeyInfo` | route.go:1290 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:179` (`GetRoutedSubKeyInfo`) | Routed-sub key decomposition helper added (route/account/subject/queue) and covered by tests | | `(c *client) removeRemoteSubs` | route.go:1299 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:195` (`RemoveRemoteSubs`), `src/NATS.Server/Routes/RouteManager.cs:593` (`WatchRouteAsync`), `src/NATS.Server/NatsServer.cs:960` (`RemoveRemoteSubscriptionsForRoute`) | Route close/removal now triggers remote-sub cleanup from account SubLists when the last connection for a remote server is gone | | `(c *client) removeRemoteSubsForAcc` | route.go:1352 | PARTIAL | `src/NATS.Server/Subscriptions/SubList.cs:229` (`RemoveRemoteSubsForAccount`), `src/NATS.Server/Routes/RouteManager.cs:286` (`UnregisterAccountRoute`), `src/NATS.Server/NatsServer.cs:966` (`RemoveRemoteSubscriptionsForRouteAccount`) | Per-account cleanup path is now wired on dedicated-route unregistration; full dedicated-route transition parity remains incomplete | | `(c *client) parseUnsubProto` | route.go:1366 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:357` (`TryParseRemoteUnsub`) | Dedicated RS-/LS- parser now handles account/subject/optional-queue extraction and is used by frame processing | | `(c *client) processRemoteUnsub` | route.go:1404 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:177–185` | .NET fires `RemoteSubscriptionReceived` with `IsRemoval=true`; missing: sub key lookup and removal from SubList, gateway/leafnode interest updates | | `(c *client) processRemoteSub` | route.go:1489 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:167–175` | .NET fires `RemoteSubscriptionReceived`; missing: key construction with type byte prefix, account lookup/creation, permission check (`canExport`), SubList insertion, gateway/leafnode updates, queue-weight delta tracking | | `(c *client) addRouteSubOrUnsubProtoToBuf` | route.go:1729 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:179` (`SendRouteSubOrUnSubProtosAsync`) | Added low-level buffered route-proto sender that batches RS+/RS-/LS+/LS- control lines into a single write/flush. | | `(s *Server) sendSubsToRoute` | route.go:1781 | MISSING | — | Bulk send of local subscription interest to newly connected route not implemented; .NET only propagates incremental sub/unsub | | `(c *client) sendRouteSubProtos` | route.go:1881 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:146` (`SendRouteSubProtosAsync`) | Added batched RS+ emission from remote-subscription models with queue/weight support. | | `(c *client) sendRouteUnSubProtos` | route.go:1890 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:165` (`SendRouteUnSubProtosAsync`) | Added batched RS- emission from remote-subscription models. | | `(c *client) sendRouteSubOrUnSubProtos` | route.go:1898 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:179` (`SendRouteSubOrUnSubProtosAsync`) | Added low-level batch sender for arbitrary route sub/unsub protocol lines. | | `(s *Server) createRoute` | route.go:1935 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:447,462` (`HandleInboundRouteAsync`/`ConnectToRouteWithRetryAsync`) | .NET creates a RouteConnection and performs handshake; missing: TLS setup, auth timeout timer, CONNECT protocol sending, INFO JSON sending, compression negotiation, ping timer | | `routeShouldDelayInfo` | route.go:2082 | MISSING | — | Logic to delay initial INFO until pool connection auth is confirmed not implemented | | `(s *Server) generateRouteInitialInfoJSON` | route.go:2090 | MISSING | — | Route INFO JSON generation (with nonce, pool index, gossip mode, compression) not implemented | | `(s *Server) addRoute` | route.go:2113 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:496` (`Register`) | .NET registers route in dictionary; missing: pool index management, duplicate detection with `handleDuplicateRoute`, per-account route registration in `accRoutes`, `sendSubsToRoute` call, gateway/leafnode URL propagation, `forwardNewRouteInfoToKnownServers` | | `hasSolicitedRoute` | route.go:2438 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:721` | Implemented helper to query whether a given remote server currently has a solicited route | | `upgradeRouteToSolicited` | route.go:2458 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:730` | Implemented route upgrade helper to flip an existing route into solicited mode | | `handleDuplicateRoute` | route.go:2473 | MISSING | — | Duplicate route resolution (close extra connection, preserve retry flag) not implemented | | `(c *client) importFilter` | route.go:2510 | MISSING | — | Permission-based subscription filter for sending to routes not implemented | | `(s *Server) updateRouteSubscriptionMap` | route.go:2519 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:381,392` (`PropagateLocalSubscription`/`PropagateLocalUnsubscription`) | .NET broadcasts RS+/RS- to all routes; missing: account routePoolIdx-based routing, queue-weight dedup (`sqmu`/`lqws`), no-pool route handling, gateway/leafnode interest updates | | `(s *Server) startRouteAcceptLoop` | route.go:2696 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:333` (`StartAsync`) | .NET binds and starts accept loop, solicits configured routes; missing: cluster name logging, TLS config on accept, routeInfo construction, advertise/NoAdvertise, LeafNode/Gateway URL propagation | | `(s *Server) setRouteInfoHostPortAndIP` | route.go:2829 | MISSING | — | Route INFO host/port/IP with Cluster.Advertise support not implemented | | `(s *Server) StartRouting` | route.go:2849 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:333` (`StartAsync`) | Functionally equivalent: starts accept loop and solicits routes | | `(s *Server) reConnectToRoute` | route.go:2861 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:512` (`ConnectToRouteWithRetryAsync`) | Retry loop now uses bounded exponential backoff and route validity guard; jitter, explicit/implicit-specific delay behavior, and quit-channel parity remain missing | | `(s *Server) routeStillValid` | route.go:2881 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:180` | Implemented reconnect guard that validates configured and discovered route URLs using normalized comparisons | | `(s *Server) connectToRoute` | route.go:2890 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:512` (`ConnectToRouteWithRetryAsync`) | Exponential backoff and route-validity checks are implemented; ConnectRetries/ConnectBackoff config parity, routes-to-self exclusion, and DNS randomization are still missing | | `(c *client) isSolicitedRoute` | route.go:2976 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs:35,370` | `IsSolicited` state is tracked on route connections and exposed via `IsSolicitedRoute()` helper | | `(s *Server) saveRouteTLSName` | route.go:2985 | NOT_APPLICABLE | — | TLS not yet implemented in .NET port | | `(s *Server) solicitRoutes` | route.go:2996 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:347–354` | .NET solicits configured routes with pool connections; missing: per-account (pinned) route solicitation, `saveRouteTLSName` | | `(c *client) processRouteConnect` | route.go:3011 | MISSING | — | Parsing and validation of inbound CONNECT from route (cluster name check, wrong-port detection, LNOC/LNOCU flags) not implemented; .NET uses a simpler handshake | | `(s *Server) removeAllRoutesExcept` | route.go:3085 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:602` (`RemoveAllRoutesExcept`) | Equivalent behavior: remove all routes not in the keep-set | | `(s *Server) removeRoute` | route.go:3113 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:632` (`RemoveRoute`) | Remove path now also cleans hash index and per-account route mappings tied to removed connections; gateway/leafnode URL withdrawal, noPool counters, and reconnect-after-noPool logic remain missing | | `(s *Server) isDuplicateServerName` | route.go:3233 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:748` | Duplicate server-name detection helper implemented against current connected-server ID set | | `(s *Server) forEachNonPerAccountRoute` | route.go:3263 | NOT_APPLICABLE | — | Internal Go iterator over route slice; .NET uses `_routes.Values` LINQ directly | | `(s *Server) forEachRoute` | route.go:3277 | NOT_APPLICABLE | — | Internal Go iterator; .NET enumerates `_routes` and `_accountRoutes` directly | | `(s *Server) forEachRouteIdx` | route.go:3292 | NOT_APPLICABLE | — | Internal Go pool-index iterator; .NET `ComputeRoutePoolIdx` achieves equivalent selection | | `(s *Server) forEachRemote` | route.go:3305 | NOT_APPLICABLE | — | Internal Go iterator (first non-nil per remote); .NET has no equivalent but uses LINQ | --- ## Keeping This File Updated After porting work is completed: 1. **Update status**: Change `MISSING → PORTED` or `PARTIAL → PORTED` for each item completed 2. **Add .NET path**: Fill in the ".NET Equivalent" column with the actual file:line 3. **Re-count LOC**: Update the LOC numbers in `stillmissing.md`: ```bash # Re-count .NET source LOC for this module find src/NATS.Server/Routes/ -name '*.cs' -type f -exec cat {} + | wc -l # Re-count .NET test LOC for this module find tests/NATS.Server.Tests/Routes/ -name '*.cs' -type f -exec cat {} + | wc -l ``` 4. **Add a changelog entry** below with date and summary of what was ported 5. **Update the parity DB** if new test mappings were created: ```bash 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-26 | Ported async INFO broadcast parity slice: wired/validated `UpdateServerINFOAndSendINFOToClients()` as the `sendAsyncInfoToClients` equivalent and added targeted socket broadcast test (`RouteInfoBroadcastParityBatch4Tests`). | codex | | 2026-02-25 | File created with LLM analysis instructions | auto | | 2026-02-25 | Full gap inventory populated: 57 Go symbols classified across route.go (3,314 lines). Counts: PORTED 4, PARTIAL 21, MISSING 23, NOT_APPLICABLE 9, DEFERRED 0 | auto | | 2026-02-25 | Ported route parity helper batch: gossip/default constants, connect-info parity fields, configured-route/reconnect guard helpers, solicited-route helpers, duplicate-server-name detection, RS-/LS- parser, and LS+/LS- + queue-weight wire helpers; updated row statuses and notes | codex | | 2026-02-25 | Ported routed-sub key helpers and remote-sub cleanup parity batch: added `getAccNameFromRoutedSubKey`/`getRoutedSubKeyInfo` equivalents plus route-close and per-account cleanup plumbing with targeted tests | codex | | 2026-02-25 | Ported route batch-proto parity batch: added buffered batch sender APIs (`SendRouteSubProtosAsync`, `SendRouteUnSubProtosAsync`, `SendRouteSubOrUnSubProtosAsync`) for RS+/RS-/LS+/LS- protocol frames with targeted tests (`RouteBatchProtoParityBatch3Tests`) | codex |