Add Go-to-.NET gap inventory docs to track porting parity

This commit is contained in:
Joseph Doherty
2026-02-25 15:12:52 -05:00
parent 339c60bac6
commit 79c1ee8776
21 changed files with 7997 additions and 0 deletions

387
gaps/auth-and-accounts.md Normal file
View File

@@ -0,0 +1,387 @@
# Auth & Accounts — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Auth & Accounts** 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 Auth & Accounts
- `accounts.go` is the **second-largest functional file** (~4,100 lines). It manages multi-tenant isolation with per-account Sublists, service latency tracking, and subject import/export mappings.
- The Go implementation supports 6+ auth mechanisms. Check which are implemented in .NET's `Auth/` directory.
- JWT handling in Go includes account claims, user claims, activation tokens, and revocation lists.
- Service imports/exports allow cross-account message delivery — check `src/NATS.Server/Imports/`.
---
## Go Reference Files (Source)
- `golang/nats-server/server/auth.go` — Auth mechanisms: username/password, token, NKeys (Ed25519), JWT, external auth callout, LDAP
- `golang/nats-server/server/auth_callout.go` — External auth callout service
- `golang/nats-server/server/nkey.go` — NKey (Ed25519) validation
- `golang/nats-server/server/jwt.go` — JWT claims parsing, account/user claims
- `golang/nats-server/server/accounts.go` — Multi-tenant account isolation (~4,100 lines). Each account has its own Sublist, client set, subject namespace. Supports exports/imports.
## Go Reference Files (Tests)
- `golang/nats-server/server/auth_test.go`
- `golang/nats-server/server/auth_callout_test.go`
- `golang/nats-server/server/nkey_test.go`
- `golang/nats-server/server/jwt_test.go`
- `golang/nats-server/server/accounts_test.go`
- `golang/nats-server/server/trust_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Auth/` (all files including `Jwt/` subdirectory)
- `src/NATS.Server/Imports/` (account import/export)
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Auth/`
- `tests/NATS.Server.Tests/Accounts/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### auth.go (1,697 lines)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Authentication interface | golang/nats-server/server/auth.go:40 | PORTED | src/NATS.Server/Auth/IAuthenticator.cs:7 | Renamed to IAuthenticator; Check(ClientAuthentication) -> Authenticate(ClientAuthContext) |
| ClientAuthentication interface | golang/nats-server/server/auth.go:46 | PORTED | src/NATS.Server/Auth/IAuthenticator.cs:12 | Mapped to ClientAuthContext record |
| NkeyUser struct | golang/nats-server/server/auth.go:62 | PARTIAL | src/NATS.Server/Auth/NKeyUser.cs:3 | Missing: Issued field, AllowedConnectionTypes, ProxyRequired |
| User struct | golang/nats-server/server/auth.go:73 | PARTIAL | src/NATS.Server/Auth/User.cs:3 | Missing: AllowedConnectionTypes, ProxyRequired |
| User.clone() | golang/nats-server/server/auth.go:85 | NOT_APPLICABLE | — | .NET uses immutable init-only records; deep clone not needed |
| NkeyUser.clone() | golang/nats-server/server/auth.go:106 | NOT_APPLICABLE | — | .NET uses immutable init-only records; deep clone not needed |
| SubjectPermission struct | golang/nats-server/server/auth.go:127 | PORTED | src/NATS.Server/Auth/Permissions.cs:10 | |
| ResponsePermission struct | golang/nats-server/server/auth.go:134 | PORTED | src/NATS.Server/Auth/Permissions.cs:16 | |
| Permissions struct | golang/nats-server/server/auth.go:141 | PORTED | src/NATS.Server/Auth/Permissions.cs:3 | |
| RoutePermissions struct | golang/nats-server/server/auth.go:150 | MISSING | — | Route-level permissions for cluster; needed for clustering |
| SubjectPermission.clone() | golang/nats-server/server/auth.go:156 | NOT_APPLICABLE | — | .NET uses immutable records |
| Permissions.clone() | golang/nats-server/server/auth.go:174 | NOT_APPLICABLE | — | .NET uses immutable records |
| checkAuthforWarnings() | golang/nats-server/server/auth.go:196 | MISSING | — | Warns about plaintext passwords at startup |
| assignGlobalAccountToOrphanUsers() | golang/nats-server/server/auth.go:226 | MISSING | — | Auto-assigns global account to users without one |
| validateResponsePermissions() | golang/nats-server/server/auth.go:243 | MISSING | — | Sets defaults for ResponsePermission (MaxMsgs, Expires) |
| configureAuthorization() | golang/nats-server/server/auth.go:266 | PARTIAL | src/NATS.Server/Auth/AuthService.cs:30 | AuthService.Build covers the priority chain; missing websocket/mqtt auth config, auth callout account validation |
| buildNkeysAndUsersFromOptions() | golang/nats-server/server/auth.go:325 | PARTIAL | src/NATS.Server/Auth/AuthService.cs:30 | User/NKey map building is in AuthService.Build; missing clone + account resolution + response permission validation |
| checkAuthentication() | golang/nats-server/server/auth.go:365 | PARTIAL | src/NATS.Server/Auth/AuthService.cs:97 | Only CLIENT kind is implemented; ROUTER, GATEWAY, LEAF auth missing |
| isClientAuthorized() | golang/nats-server/server/auth.go:382 | PORTED | src/NATS.Server/Auth/AuthService.cs:97 | Core flow matches; missing accountConnectEvent |
| matchesPinnedCert() | golang/nats-server/server/auth.go:405 | MISSING | — | TLS pinned certificate validation |
| processUserPermissionsTemplate() | golang/nats-server/server/auth.go:427 | PORTED | src/NATS.Server/Auth/Jwt/PermissionTemplates.cs:36 | Full template expansion with cartesian product |
| processClientOrLeafAuthentication() | golang/nats-server/server/auth.go:588 | PARTIAL | src/NATS.Server/Auth/AuthService.cs:97 | Core client auth flow ported; missing leaf node auth, proxy check integration, auth callout defer, JWT src/time validation |
| proxyCheck() | golang/nats-server/server/auth.go:1153 | PARTIAL | src/NATS.Server/Auth/ProxyAuthenticator.cs:3 | Basic proxy prefix auth exists; full NKey signature-based proxy verification missing |
| getTLSAuthDCs() | golang/nats-server/server/auth.go:1198 | MISSING | — | Extract DC (Domain Component) from TLS cert RDN |
| tlsMapAuthFn type | golang/nats-server/server/auth.go:1218 | NOT_APPLICABLE | — | Go function type; .NET uses delegate/lambda |
| checkClientTLSCertSubject() | golang/nats-server/server/auth.go:1220 | PARTIAL | src/NATS.Server/Auth/TlsMapAuthenticator.cs:9 | Basic DN/CN matching ported; missing email, URI, DNS SAN matching, LDAP DN parsing |
| dnsAltNameLabels() | golang/nats-server/server/auth.go:1316 | MISSING | — | DNS alt name label splitting for TLS |
| dnsAltNameMatches() | golang/nats-server/server/auth.go:1321 | MISSING | — | DNS alt name matching against URLs |
| isRouterAuthorized() | golang/nats-server/server/auth.go:1349 | MISSING | — | Cluster route authentication |
| isGatewayAuthorized() | golang/nats-server/server/auth.go:1390 | MISSING | — | Gateway authentication |
| registerLeafWithAccount() | golang/nats-server/server/auth.go:1425 | MISSING | — | Leaf node account registration |
| isLeafNodeAuthorized() | golang/nats-server/server/auth.go:1443 | MISSING | — | Leaf node authentication |
| isBcrypt() | golang/nats-server/server/auth.go:1569 | PORTED | src/NATS.Server/Auth/UserPasswordAuthenticator.cs:65 | Simplified to StartsWith("$2") check |
| comparePasswords() | golang/nats-server/server/auth.go:1577 | PORTED | src/NATS.Server/Auth/UserPasswordAuthenticator.cs:46 | bcrypt + constant-time comparison |
| validateAuth() | golang/nats-server/server/auth.go:1595 | MISSING | — | Top-level auth config validation |
| validateAllowedConnectionTypes() | golang/nats-server/server/auth.go:1612 | PARTIAL | src/NATS.Server/Auth/Jwt/JwtConnectionTypes.cs:18 | Validation logic exists in JWT context; not used for config-level validation |
| validateNoAuthUser() | golang/nats-server/server/auth.go:1631 | MISSING | — | Validates no_auth_user exists in Users/Nkeys |
| validateProxies() | golang/nats-server/server/auth.go:1657 | MISSING | — | Validates proxy trusted keys are valid NKeys |
| processProxiesTrustedKeys() | golang/nats-server/server/auth.go:1672 | MISSING | — | Builds NKey keypairs from proxy trusted keys |
| getAuthErrClosedState() | golang/nats-server/server/auth.go:1688 | MISSING | — | Maps auth errors to connection close states |
### auth_callout.go (497 lines)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| AuthCalloutSubject const | golang/nats-server/server/auth_callout.go:30 | MISSING | — | "$SYS.REQ.USER.AUTH" subject |
| AuthRequestSubject const | golang/nats-server/server/auth_callout.go:31 | MISSING | — | "nats-authorization-request" |
| AuthRequestXKeyHeader const | golang/nats-server/server/auth_callout.go:32 | MISSING | — | "Nats-Server-Xkey" header |
| processClientOrLeafCallout() | golang/nats-server/server/auth_callout.go:36 | PARTIAL | src/NATS.Server/Auth/ExternalAuthCalloutAuthenticator.cs:3 | .NET has a simplified external auth callout via IExternalAuthClient interface; Go implementation uses internal NATS messaging ($SYS subjects), JWT encoding/decoding, XKey encryption, replay prevention — all missing from .NET |
| fillClientInfo() | golang/nats-server/server/auth_callout.go:456 | MISSING | — | Fills jwt.ClientInformation for auth callout requests |
| fillConnectOpts() | golang/nats-server/server/auth_callout.go:477 | MISSING | — | Fills jwt.ConnectOptions for auth callout requests |
### nkey.go (47 lines)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| nonceRawLen const | golang/nats-server/server/nkey.go:23 | PORTED | src/NATS.Server/Auth/AuthService.cs:147 | Uses 11-byte raw nonce |
| nonceLen const | golang/nats-server/server/nkey.go:24 | NOT_APPLICABLE | — | Derived constant (base64 length) |
| NonceRequired() | golang/nats-server/server/nkey.go:28 | PORTED | src/NATS.Server/Auth/AuthService.cs:18 | Property on AuthService |
| nonceRequired() | golang/nats-server/server/nkey.go:36 | PORTED | src/NATS.Server/Auth/AuthService.cs:34 | Checked during Build based on NKeys/TrustedKeys |
| generateNonce() | golang/nats-server/server/nkey.go:42 | PORTED | src/NATS.Server/Auth/AuthService.cs:146 | GenerateNonce + EncodeNonce use RandomNumberGenerator.Fill |
### jwt.go (245 lines)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| jwtPrefix const | golang/nats-server/server/jwt.go:29 | PORTED | src/NATS.Server/Auth/Jwt/NatsJwt.cs:17 | "eyJ" constant |
| ReadOperatorJWT() | golang/nats-server/server/jwt.go:32 | MISSING | — | Reads operator JWT from file/inline string |
| readOperatorJWT() | golang/nats-server/server/jwt.go:37 | MISSING | — | Internal helper for operator JWT parsing |
| wipeSlice() | golang/nats-server/server/jwt.go:61 | NOT_APPLICABLE | — | Go-specific memory wiping; .NET has CryptographicOperations.ZeroMemory |
| validateTrustedOperators() | golang/nats-server/server/jwt.go:70 | MISSING | — | Validates operator JWT config (keys, resolver, system account, version) |
| validateSrc() | golang/nats-server/server/jwt.go:182 | MISSING | — | Validates user JWT source CIDR restrictions |
| validateTimes() | golang/nats-server/server/jwt.go:204 | MISSING | — | Validates user JWT time-of-day restrictions |
### accounts.go (4,774 lines)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| globalAccountName const | golang/nats-server/server/accounts.go:44 | PORTED | src/NATS.Server/Auth/Account.cs:9 | Account.GlobalAccountName = "$G" |
| Account struct | golang/nats-server/server/accounts.go:52 | PARTIAL | src/NATS.Server/Auth/Account.cs:7 | Core fields ported (Name, SubList, limits, clients, revocations, exports, imports). Missing: stats (gw/rt/ln), gwReplyMapping, claimJWT, mappings, hasMapped, lleafs, leafClusters, js/jsLimits, nrgAccount, extAuth, defaultPerms, nameTag, tags, traceDest |
| limits struct | golang/nats-server/server/accounts.go:127 | PARTIAL | src/NATS.Server/Auth/Account.cs:15-19 | MaxConnections, MaxSubscriptions ported; missing MaxPayload, MaxLeafNodeConnections, MaxRemoteConnections |
| sconns struct | golang/nats-server/server/accounts.go:136 | MISSING | — | Remote server connection count tracking |
| streamImport struct | golang/nats-server/server/accounts.go:142 | PORTED | src/NATS.Server/Imports/StreamImport.cs:7 | |
| ClientInfoHdr const | golang/nats-server/server/accounts.go:157 | MISSING | — | "Nats-Request-Info" header for service imports |
| serviceImport struct | golang/nats-server/server/accounts.go:160 | PORTED | src/NATS.Server/Imports/ServiceImport.cs:6 | |
| serviceRespEntry struct | golang/nats-server/server/accounts.go:187 | MISSING | — | TTL-tracked response entries |
| ServiceRespType enum | golang/nats-server/server/accounts.go:193 | PORTED | src/NATS.Server/Imports/ServiceResponseType.cs:3 | Singleton, Streamed, Chunked |
| ServiceRespType.String() | golang/nats-server/server/accounts.go:203 | NOT_APPLICABLE | — | Go Stringer interface; .NET enum has ToString() |
| exportAuth struct | golang/nats-server/server/accounts.go:217 | PORTED | src/NATS.Server/Imports/ExportAuth.cs:5 | TokenRequired, AccountPosition, ApprovedAccounts, RevokedAccounts |
| streamExport struct | golang/nats-server/server/accounts.go:225 | PORTED | src/NATS.Server/Imports/StreamExport.cs:3 | |
| serviceExport struct | golang/nats-server/server/accounts.go:230 | PORTED | src/NATS.Server/Imports/ServiceExport.cs:5 | |
| serviceLatency struct | golang/nats-server/server/accounts.go:245 | PORTED | src/NATS.Server/Imports/ServiceLatency.cs:3 | |
| exportMap struct | golang/nats-server/server/accounts.go:251 | PORTED | src/NATS.Server/Imports/ExportMap.cs:3 | |
| importMap struct | golang/nats-server/server/accounts.go:259 | PORTED | src/NATS.Server/Imports/ImportMap.cs:5 | |
| NewAccount() | golang/nats-server/server/accounts.go:266 | PORTED | src/NATS.Server/Auth/Account.cs:91 | Account constructor |
| Account.String() | golang/nats-server/server/accounts.go:275 | NOT_APPLICABLE | — | Trivial Go Stringer |
| Account.setTraceDest() | golang/nats-server/server/accounts.go:279 | MISSING | — | Message tracing destination |
| Account.getTraceDestAndSampling() | golang/nats-server/server/accounts.go:285 | MISSING | — | Message tracing config |
| Account.shallowCopy() | golang/nats-server/server/accounts.go:298 | MISSING | — | Shallow copy for account reload |
| Account.nextEventID() | golang/nats-server/server/accounts.go:354 | MISSING | — | NUID-based event ID generation |
| Account.getClientsLocked() | golang/nats-server/server/accounts.go:363 | MISSING | — | Returns client slice under lock |
| Account.getClients() | golang/nats-server/server/accounts.go:375 | MISSING | — | Returns client slice with lock acquisition |
| Account.getExternalClientsLocked() | golang/nats-server/server/accounts.go:384 | MISSING | — | Returns only external clients |
| Account.updateRemoteServer() | golang/nats-server/server/accounts.go:399 | MISSING | — | Cluster remote server tracking |
| Account.removeRemoteServer() | golang/nats-server/server/accounts.go:446 | MISSING | — | Cluster remote server removal |
| Account.expectedRemoteResponses() | golang/nats-server/server/accounts.go:460 | MISSING | — | Expected responses from remote servers |
| Account.clearEventing() | golang/nats-server/server/accounts.go:472 | MISSING | — | Clears event timers |
| Account.GetName() | golang/nats-server/server/accounts.go:484 | PORTED | src/NATS.Server/Auth/Account.cs:12 | Name property |
| Account.NumConnections() | golang/nats-server/server/accounts.go:519 | PORTED | src/NATS.Server/Auth/Account.cs:96 | ClientCount property |
| Account.NumRemoteConnections() | golang/nats-server/server/accounts.go:528 | MISSING | — | Remote connection count (clustering) |
| Account.NumLocalConnections() | golang/nats-server/server/accounts.go:537 | PORTED | src/NATS.Server/Auth/Account.cs:96 | ClientCount is local only |
| Account.MaxTotalConnectionsReached() | golang/nats-server/server/accounts.go:563 | PORTED | src/NATS.Server/Auth/Account.cs:103 | AddClient checks MaxConnections |
| Account.MaxActiveConnections() | golang/nats-server/server/accounts.go:575 | PORTED | src/NATS.Server/Auth/Account.cs:15 | MaxConnections property |
| Account.MaxTotalLeafNodesReached() | golang/nats-server/server/accounts.go:583 | MISSING | — | Leaf node connection limits |
| Account.NumLeafNodes() | golang/nats-server/server/accounts.go:599 | MISSING | — | Leaf node count |
| Account.NumRemoteLeafNodes() | golang/nats-server/server/accounts.go:608 | MISSING | — | Remote leaf count |
| Account.MaxActiveLeafNodes() | golang/nats-server/server/accounts.go:618 | MISSING | — | Leaf node limit |
| Account.RoutedSubs() | golang/nats-server/server/accounts.go:627 | MISSING | — | Routed subscription count |
| Account.TotalSubs() | golang/nats-server/server/accounts.go:634 | PORTED | src/NATS.Server/Auth/Account.cs:97 | SubscriptionCount |
| Account.shouldLogMaxSubErr() | golang/nats-server/server/accounts.go:643 | MISSING | — | Rate-limited logging for max sub errors |
| MapDest struct | golang/nats-server/server/accounts.go:660 | MISSING | — | Subject mapping destination with weight |
| NewMapDest() | golang/nats-server/server/accounts.go:666 | MISSING | — | MapDest constructor |
| destination struct | golang/nats-server/server/accounts.go:671 | MISSING | — | Internal mapping destination |
| mapping struct | golang/nats-server/server/accounts.go:677 | MISSING | — | Subject mapping with weighted destinations |
| Account.AddMapping() | golang/nats-server/server/accounts.go:686 | MISSING | — | Subject remapping (1:1) |
| Account.AddWeightedMappings() | golang/nats-server/server/accounts.go:691 | MISSING | — | Weighted subject mappings |
| Account.RemoveMapping() | golang/nats-server/server/accounts.go:810 | MISSING | — | Remove a subject mapping |
| Account.hasMappings() | golang/nats-server/server/accounts.go:839 | MISSING | — | Check for subject mappings |
| Account.selectMappedSubject() | golang/nats-server/server/accounts.go:848 | MISSING | — | Select destination via weighted mapping |
| Account.SubscriptionInterest() | golang/nats-server/server/accounts.go:929 | PARTIAL | src/NATS.Server/Auth/Account.cs:787 | ServiceImportShadowed checks SubList.Match |
| Account.Interest() | golang/nats-server/server/accounts.go:934 | MISSING | — | Returns count of matching subscriptions |
| Account.addClient() | golang/nats-server/server/accounts.go:947 | PORTED | src/NATS.Server/Auth/Account.cs:103 | AddClient checks MaxConnections |
| Account.registerLeafNodeCluster() | golang/nats-server/server/accounts.go:986 | MISSING | — | Leaf node cluster registration |
| Account.hasLeafNodeCluster() | golang/nats-server/server/accounts.go:996 | MISSING | — | Check for leaf node cluster |
| Account.isLeafNodeClusterIsolated() | golang/nats-server/server/accounts.go:1004 | MISSING | — | Leaf cluster isolation check |
| Account.removeLeafNode() | golang/nats-server/server/accounts.go:1018 | MISSING | — | Remove leaf node from account |
| Account.removeClient() | golang/nats-server/server/accounts.go:1038 | PORTED | src/NATS.Server/Auth/Account.cs:111 | RemoveClient |
| setExportAuth() | golang/nats-server/server/accounts.go:1075 | PARTIAL | src/NATS.Server/Imports/ExportAuth.cs:5 | ExportAuth struct exists; setExportAuth helper logic partially in Account.AddServiceExport |
| Account.AddServiceExport() | golang/nats-server/server/accounts.go:1101 | PORTED | src/NATS.Server/Auth/Account.cs:309 | AddServiceExport |
| Account.AddServiceExportWithResponse() | golang/nats-server/server/accounts.go:1111 | PORTED | src/NATS.Server/Auth/Account.cs:309 | Included in AddServiceExport with responseType param |
| Account.TrackServiceExport() | golang/nats-server/server/accounts.go:1163 | PARTIAL | src/NATS.Server/Auth/ServiceLatencyTracker.cs:7 | Tracker exists but wiring to export subjects via NATS internal pub missing |
| Account.TrackServiceExportWithSampling() | golang/nats-server/server/accounts.go:1169 | PARTIAL | src/NATS.Server/Imports/ServiceLatency.cs:5 | SamplingPercentage field exists; full integration missing |
| Account.UnTrackServiceExport() | golang/nats-server/server/accounts.go:1238 | MISSING | — | Remove latency tracking from export |
| Account.IsExportService() | golang/nats-server/server/accounts.go:1281 | PORTED | src/NATS.Server/Auth/Account.cs:298 | HasServiceExport |
| Account.IsExportServiceTracking() | golang/nats-server/server/accounts.go:1298 | MISSING | — | Check if latency tracking is enabled for export |
| ServiceLatency struct | golang/nats-server/server/accounts.go:1326 | PORTED | src/NATS.Server/Imports/LatencyTracker.cs:5 | ServiceLatencyMsg |
| ServiceLatencyType const | golang/nats-server/server/accounts.go:1340 | PORTED | src/NATS.Server/Imports/LatencyTracker.cs:8 | Type field default |
| ServiceLatency.NATSTotalTime() | golang/nats-server/server/accounts.go:1343 | MISSING | — | Total time computation |
| ServiceLatency.merge() | golang/nats-server/server/accounts.go:1354 | MISSING | — | Merge latency metrics |
| sanitizeLatencyMetric() | golang/nats-server/server/accounts.go:1370 | MISSING | — | Sanitize negative latencies |
| remoteLatency struct | golang/nats-server/server/accounts.go:1380 | MISSING | — | Remote latency for cluster |
| Account.sendLatencyResult() | golang/nats-server/server/accounts.go:1388 | MISSING | — | Publish latency result to tracking subject |
| Account.sendBadRequestTrackingLatency() | golang/nats-server/server/accounts.go:1401 | MISSING | — | Track bad request latency |
| Account.sendReplyInterestLostTrackLatency() | golang/nats-server/server/accounts.go:1414 | MISSING | — | Track lost reply interest |
| Account.sendBackendErrorTrackingLatency() | golang/nats-server/server/accounts.go:1430 | MISSING | — | Track backend error latency |
| Account.sendTrackingLatency() | golang/nats-server/server/accounts.go:1458 | MISSING | — | Core latency tracking send |
| updateAllClientsServiceExportResponseTime() | golang/nats-server/server/accounts.go:1527 | MISSING | — | Update client response times |
| Account.lowestServiceExportResponseTime() | golang/nats-server/server/accounts.go:1542 | MISSING | — | Find lowest response time |
| Account.AddServiceImportWithClaim() | golang/nats-server/server/accounts.go:1554 | PARTIAL | src/NATS.Server/Auth/Account.cs:338 | AddServiceImport exists; JWT claim-based import missing |
| addServiceImportWithClaim() | golang/nats-server/server/accounts.go:1560 | PARTIAL | src/NATS.Server/Auth/Account.cs:338 | Core logic ported; missing claim validation, activation token handling |
| MaxAccountCycleSearchDepth const | golang/nats-server/server/accounts.go:1593 | MISSING | — | Depth limit for cycle detection |
| Account.serviceImportFormsCycle() | golang/nats-server/server/accounts.go:1595 | PORTED | src/NATS.Server/Auth/AccountImportExport.cs:18 | DetectCycle |
| Account.checkServiceImportsForCycles() | golang/nats-server/server/accounts.go:1599 | PORTED | src/NATS.Server/Auth/AccountImportExport.cs:18 | DFS cycle detection |
| Account.streamImportFormsCycle() | golang/nats-server/server/accounts.go:1627 | PORTED | src/NATS.Server/Auth/Account.cs:405 | StreamImportFormsCycle |
| Account.hasServiceExportMatching() | golang/nats-server/server/accounts.go:1632 | PORTED | src/NATS.Server/Auth/Account.cs:298 | HasServiceExport |
| Account.hasStreamExportMatching() | golang/nats-server/server/accounts.go:1642 | MISSING | — | Check if stream export matches subject |
| Account.checkStreamImportsForCycles() | golang/nats-server/server/accounts.go:1651 | PORTED | src/NATS.Server/Auth/Account.cs:412 | DetectStreamImportCycle (private) |
| Account.SetServiceImportSharing() | golang/nats-server/server/accounts.go:1686 | MISSING | — | Enable/disable service import sharing |
| Account.AddServiceImport() | golang/nats-server/server/accounts.go:1715 | PORTED | src/NATS.Server/Auth/Account.cs:338 | |
| Account.NumPendingReverseResponses() | golang/nats-server/server/accounts.go:1721 | PORTED | src/NATS.Server/Auth/Account.cs:772 | ReverseResponseMapCount |
| Account.NumPendingAllResponses() | golang/nats-server/server/accounts.go:1728 | MISSING | — | Total pending across all service imports |
| Account.NumPendingResponses() | golang/nats-server/server/accounts.go:1736 | MISSING | — | Filtered pending response count |
| Account.NumServiceImports() | golang/nats-server/server/accounts.go:1756 | MISSING | — | Count of service imports |
| rsiReason enum | golang/nats-server/server/accounts.go:1763 | MISSING | — | Response service import removal reason |
| Account.removeRespServiceImport() | golang/nats-server/server/accounts.go:1772 | PARTIAL | src/NATS.Server/Imports/ResponseRouter.cs:60 | CleanupResponse exists; missing reason-based tracking/latency |
| Account.getServiceImportForAccountLocked() | golang/nats-server/server/accounts.go:1795 | MISSING | — | Find service import by dest account + subject |
| Account.removeServiceImport() | golang/nats-server/server/accounts.go:1812 | PORTED | src/NATS.Server/Auth/Account.cs:366 | RemoveServiceImport |
| Account.addReverseRespMapEntry() | golang/nats-server/server/accounts.go:1858 | PORTED | src/NATS.Server/Auth/Account.cs:752 | AddReverseRespMapEntry |
| Account.checkForReverseEntries() | golang/nats-server/server/accounts.go:1872 | PARTIAL | src/NATS.Server/Auth/Account.cs:761 | Lookup exists; missing recursive check + interest validation |
| Account.serviceImportShadowed() | golang/nats-server/server/accounts.go:2015 | PORTED | src/NATS.Server/Auth/Account.cs:786 | ServiceImportShadowed |
| Account.serviceImportExists() | golang/nats-server/server/accounts.go:2031 | MISSING | — | Check if service import exists by dest account + from |
| Account.addServiceImport() | golang/nats-server/server/accounts.go:2041 | PARTIAL | src/NATS.Server/Auth/Account.cs:338 | Core add logic; missing claim/activation token handling |
| Account.unsubscribeInternal() | golang/nats-server/server/accounts.go:2130 | MISSING | — | Internal subscription cleanup |
| Account.subscribeServiceImportResponse() | golang/nats-server/server/accounts.go:2137 | MISSING | — | Response subscription for service import |
| Account.subscribeInternalEx() | golang/nats-server/server/accounts.go:2141 | MISSING | — | Internal subscription with routing flag |
| Account.addServiceImportSub() | golang/nats-server/server/accounts.go:2156 | MISSING | — | Wire up service import subscription |
| Account.removeAllServiceImportSubs() | golang/nats-server/server/accounts.go:2190 | MISSING | — | Remove all service import subscriptions |
| Account.addAllServiceImportSubs() | golang/nats-server/server/accounts.go:2215 | MISSING | — | Add all service import subscriptions |
| shouldSample() | golang/nats-server/server/accounts.go:2279 | PORTED | src/NATS.Server/Imports/LatencyTracker.cs:28 | LatencyTracker.ShouldSample |
| Account.processServiceImportResponse() | golang/nats-server/server/accounts.go:2358 | MISSING | — | Handle incoming response for service import |
| Account.createRespWildcard() | golang/nats-server/server/accounts.go:2380 | MISSING | — | Create wildcard reply prefix |
| isTrackedReply() | golang/nats-server/server/accounts.go:2391 | MISSING | — | Check if reply is for a tracked service |
| Account.newServiceReply() | golang/nats-server/server/accounts.go:2398 | PARTIAL | src/NATS.Server/Imports/ResponseRouter.cs:19 | GenerateReplyPrefix exists |
| serviceExport.setResponseThresholdTimer() | golang/nats-server/server/accounts.go:2446 | MISSING | — | Timer for response threshold |
| serviceExport.clearResponseThresholdTimer() | golang/nats-server/server/accounts.go:2454 | MISSING | — | Clear response threshold timer |
| serviceExport.checkExpiredResponses() | golang/nats-server/server/accounts.go:2465 | MISSING | — | Clean up expired responses |
| Account.ServiceExportResponseThreshold() | golang/nats-server/server/accounts.go:2510 | PORTED | src/NATS.Server/Auth/Account.cs:468 | GetServiceResponseThreshold |
| Account.SetServiceExportResponseThreshold() | golang/nats-server/server/accounts.go:2522 | PORTED | src/NATS.Server/Auth/Account.cs:461 | SetServiceResponseThreshold |
| Account.SetServiceExportAllowTrace() | golang/nats-server/server/accounts.go:2549 | MISSING | — | Enable tracing on service export |
| Account.addRespServiceImport() | golang/nats-server/server/accounts.go:2562 | PORTED | src/NATS.Server/Imports/ResponseRouter.cs:33 | CreateResponseImport |
| Account.AddStreamImportWithClaim() | golang/nats-server/server/accounts.go:2598 | PARTIAL | src/NATS.Server/Auth/Account.cs:371 | AddStreamImport exists; claim validation missing |
| Account.AddMappedStreamImport() | golang/nats-server/server/accounts.go:2629 | PORTED | src/NATS.Server/Auth/Account.cs:371 | AddStreamImport with 'to' parameter |
| Account.isStreamImportDuplicate() | golang/nats-server/server/accounts.go:2694 | MISSING | — | Duplicate stream import detection |
| Account.AddStreamImport() | golang/nats-server/server/accounts.go:2704 | PORTED | src/NATS.Server/Auth/Account.cs:371 | |
| IsPublicExport var | golang/nats-server/server/accounts.go:2709 | MISSING | — | Sentinel for public export |
| Account.AddStreamExport() | golang/nats-server/server/accounts.go:2713 | PORTED | src/NATS.Server/Auth/Account.cs:323 | AddStreamExport |
| Account.checkStreamImportAuthorized() | golang/nats-server/server/accounts.go:2746 | PARTIAL | src/NATS.Server/Imports/ExportAuth.cs:12 | IsAuthorized checks approved accounts; missing token/claim validation |
| Account.checkAuth() | golang/nats-server/server/accounts.go:2761 | PARTIAL | src/NATS.Server/Imports/ExportAuth.cs:12 | Basic approval; missing token validation and wildcard matching |
| Account.checkStreamExportApproved() | golang/nats-server/server/accounts.go:2782 | PARTIAL | src/NATS.Server/Imports/ExportAuth.cs:12 | Via IsAuthorized; missing wildcard subject matching |
| Account.checkServiceExportApproved() | golang/nats-server/server/accounts.go:2809 | PARTIAL | src/NATS.Server/Imports/ExportAuth.cs:12 | Via IsAuthorized; missing wildcard subject matching |
| Account.getServiceExport() | golang/nats-server/server/accounts.go:2837 | PORTED | src/NATS.Server/Auth/Account.cs:267 | GetExactServiceExport |
| Account.getWildcardServiceExport() | golang/nats-server/server/accounts.go:2849 | PORTED | src/NATS.Server/Auth/Account.cs:279 | GetWildcardServiceExport |
| Account.streamActivationExpired() | golang/nats-server/server/accounts.go:2860 | PARTIAL | src/NATS.Server/Auth/Account.cs:613 | IsActivationExpired exists; missing auto-removal and re-check logic |
| Account.serviceActivationExpired() | golang/nats-server/server/accounts.go:2895 | PARTIAL | src/NATS.Server/Auth/Account.cs:613 | IsActivationExpired exists; missing auto-removal |
| Account.activationExpired() | golang/nats-server/server/accounts.go:2920 | PARTIAL | src/NATS.Server/Auth/Account.cs:620 | GetExpiredActivations + RemoveExpiredActivations |
| isRevoked() | golang/nats-server/server/accounts.go:2929 | PORTED | src/NATS.Server/Auth/Account.cs:52 | IsUserRevoked |
| Account.checkActivation() | golang/nats-server/server/accounts.go:2943 | PARTIAL | src/NATS.Server/Auth/Account.cs:598 | CheckActivationExpiry exists; missing issuer trust chain validation |
| Account.isIssuerClaimTrusted() | golang/nats-server/server/accounts.go:2988 | MISSING | — | Validates issuer against account signing keys |
| Account.checkStreamImportsEqual() | golang/nats-server/server/accounts.go:3011 | MISSING | — | Equality check for reload |
| Account.checkStreamExportsEqual() | golang/nats-server/server/accounts.go:3036 | MISSING | — | Equality check for reload |
| isStreamExportEqual() | golang/nats-server/server/accounts.go:3054 | MISSING | — | Stream export comparison |
| Account.checkServiceExportsEqual() | golang/nats-server/server/accounts.go:3067 | MISSING | — | Equality check for reload |
| isServiceExportEqual() | golang/nats-server/server/accounts.go:3085 | MISSING | — | Service export comparison |
| isExportAuthEqual() | golang/nats-server/server/accounts.go:3121 | MISSING | — | Export auth comparison |
| Account.checkServiceImportAuthorized() | golang/nats-server/server/accounts.go:3148 | PARTIAL | src/NATS.Server/Imports/ExportAuth.cs:12 | Via IsAuthorized; missing token validation |
| Account.IsExpired() | golang/nats-server/server/accounts.go:3165 | PORTED | src/NATS.Server/Auth/Account.cs:523 | IsExpired property |
| Account.expiredTimeout() | golang/nats-server/server/accounts.go:3170 | MISSING | — | Timer callback for expired accounts |
| Account.setExpirationTimer() | golang/nats-server/server/accounts.go:3184 | PARTIAL | src/NATS.Server/Auth/Account.cs:553 | SetExpiration exists; actual Timer not created |
| Account.clearExpirationTimer() | golang/nats-server/server/accounts.go:3189 | PARTIAL | src/NATS.Server/Auth/Account.cs:557 | ClearExpiration exists; no Timer to clear |
| Account.checkUserRevoked() | golang/nats-server/server/accounts.go:3199 | PORTED | src/NATS.Server/Auth/Account.cs:52 | IsUserRevoked |
| Account.failBearer() | golang/nats-server/server/accounts.go:3206 | MISSING | — | Check if account disallows bearer tokens |
| Account.checkExpiration() | golang/nats-server/server/accounts.go:3213 | PARTIAL | src/NATS.Server/Auth/Account.cs:523 | IsExpired check exists; missing timer setup |
| Account.hasIssuer() | golang/nats-server/server/accounts.go:3235 | MISSING | — | Check signing keys with scope |
| Account.getLDSubject() | golang/nats-server/server/accounts.go:3249 | MISSING | — | Leaf node deny subject |
| SetAccountResolver() | golang/nats-server/server/accounts.go:3267 | MISSING | — | Server-level resolver assignment |
| AccountResolver() | golang/nats-server/server/accounts.go:3274 | MISSING | — | Server-level resolver getter |
| Account.isClaimAccount() | golang/nats-server/server/accounts.go:3283 | MISSING | — | Check if account backed by JWT |
| UpdateAccountClaims() | golang/nats-server/server/accounts.go:3290 | PARTIAL | src/NATS.Server/Auth/Account.cs:684 | UpdateAccountClaims exists; missing full claim application (exports/imports rebuild, signing keys, external auth, mappings) |
| updateAccountClaimsWithRefresh() | golang/nats-server/server/accounts.go:3374 | PARTIAL | src/NATS.Server/Auth/Account.cs:684 | Diff-based update exists; missing export/import refresh, dependent account refresh |
| buildInternalAccount() | golang/nats-server/server/accounts.go:3957 | MISSING | — | Build account from JWT claims |
| buildPermissionsFromJwt() | golang/nats-server/server/accounts.go:3979 | PORTED | src/NATS.Server/Auth/JwtAuthenticator.cs:117 | Inline in JwtAuthenticator.Authenticate |
| buildInternalNkeyUser() | golang/nats-server/server/accounts.go:4012 | MISSING | — | Build NkeyUser from JWT user claims |
| fetchAccount() | golang/nats-server/server/accounts.go:4027 | MISSING | — | Fetch account JWT with NKey validation |
| AccountResolver interface | golang/nats-server/server/accounts.go:4035 | PARTIAL | src/NATS.Server/Auth/Jwt/AccountResolver.cs:14 | IAccountResolver has Fetch/Store/IsReadOnly; missing Start, IsTrackingUpdate, Reload, Close |
| MemAccResolver struct | golang/nats-server/server/accounts.go:4073 | PORTED | src/NATS.Server/Auth/Jwt/AccountResolver.cs:44 | MemAccountResolver |
| MemAccResolver.Fetch() | golang/nats-server/server/accounts.go:4079 | PORTED | src/NATS.Server/Auth/Jwt/AccountResolver.cs:53 | FetchAsync |
| MemAccResolver.Store() | golang/nats-server/server/accounts.go:4087 | PORTED | src/NATS.Server/Auth/Jwt/AccountResolver.cs:60 | StoreAsync |
| URLAccResolver struct | golang/nats-server/server/accounts.go:4097 | MISSING | — | HTTP-based account resolver |
| NewURLAccResolver() | golang/nats-server/server/accounts.go:4104 | MISSING | — | URL resolver constructor |
| URLAccResolver.Fetch() | golang/nats-server/server/accounts.go:4123 | MISSING | — | HTTP GET for account JWT |
| DirAccResolver struct | golang/nats-server/server/accounts.go:4143 | MISSING | — | Directory-based resolver with NATS sync |
| DirAccResolver.Start() | golang/nats-server/server/accounts.go:4352 | MISSING | — | Start directory resolver with NATS subscriptions |
| DirAccResolver.Fetch() | golang/nats-server/server/accounts.go:4533 | MISSING | — | Fetch from directory store |
| DirAccResolver.Store() | golang/nats-server/server/accounts.go:4548 | MISSING | — | Store to directory |
| NewDirAccResolver() | golang/nats-server/server/accounts.go:4574 | MISSING | — | Dir resolver constructor |
| CacheDirAccResolver struct | golang/nats-server/server/accounts.go:4594 | MISSING | — | Caching directory resolver |
| Server.fetch() | golang/nats-server/server/accounts.go:4599 | MISSING | — | Server-level account fetch via NATS |
| NewCacheDirAccResolver() | golang/nats-server/server/accounts.go:4664 | MISSING | — | Cache dir resolver constructor |
| CacheDirAccResolver.Start() | golang/nats-server/server/accounts.go:4679 | MISSING | — | Start cache resolver with NATS sync |
---
## 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/Auth/ src/NATS.Server/Imports/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Auth/ tests/NATS.Server.Tests/Accounts/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Gap inventory populated: 5 Go files analyzed (7,260 LOC), 239 symbols classified across auth.go, auth_callout.go, nkey.go, jwt.go, accounts.go. Summary: 64 PORTED, 38 PARTIAL, 128 MISSING, 9 NOT_APPLICABLE, 0 DEFERRED | auto |

286
gaps/configuration.md Normal file
View File

@@ -0,0 +1,286 @@
# Configuration — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Configuration** 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 Configuration
- .NET LOC **exceeds** Go LOC (4,559 vs 1,871) because .NET implements a full lexer/parser/token pipeline.
- The Go config format is a custom format (not JSON, not YAML). It supports includes, variables, and block structures.
- `opts.go` (in Core Server category) also participates in config — it maps CLI flags + config file to the options struct.
- Hot reload (`reload.go` in Core Server) depends on config diffing — ensure the .NET config model supports this.
---
## Go Reference Files (Source)
- `golang/nats-server/conf/lex.go` — Lexer for NATS config file format
- `golang/nats-server/conf/parse.go` — Parser for config files (supports includes, variables)
- `golang/nats-server/conf/token.go` — Token types for config lexer
## Go Reference Files (Tests)
- `golang/nats-server/conf/lex_test.go`
- `golang/nats-server/conf/parse_test.go`
- `golang/nats-server/server/config_check_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Configuration/NatsConfLexer.cs`
- `src/NATS.Server/Configuration/NatsConfParser.cs`
- `src/NATS.Server/Configuration/NatsConfToken.cs`
- `src/NATS.Server/Configuration/ConfigProcessor.cs`
- `src/NATS.Server/Configuration/ConfigReloader.cs`
- All other files in `src/NATS.Server/Configuration/`
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Configuration/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### golang/nats-server/conf/lex.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `itemType` (type alias `int`) | `conf/lex.go:37` | PORTED | `src/NATS.Server/Configuration/NatsConfToken.cs:5` | Ported as `TokenType` enum |
| `itemError` constant | `conf/lex.go:40` | PORTED | `NatsConfToken.cs:7` | `TokenType.Error` |
| `itemNIL` constant | `conf/lex.go:41` | NOT_APPLICABLE | — | Go-only sentinel used in parser internal state; .NET uses `Token` default struct instead |
| `itemEOF` constant | `conf/lex.go:42` | PORTED | `NatsConfToken.cs:8` | `TokenType.Eof` |
| `itemKey` constant | `conf/lex.go:43` | PORTED | `NatsConfToken.cs:9` | `TokenType.Key` |
| `itemText` constant | `conf/lex.go:44` | PARTIAL | — | Go emits `itemText` for comment body; .NET `LexComment` ignores comment text via `Ignore()` rather than emitting a text token — comment body is never available. Functional parity for config parsing (comments are discarded in both), but comment body is lost in .NET. |
| `itemString` constant | `conf/lex.go:45` | PORTED | `NatsConfToken.cs:10` | `TokenType.String` |
| `itemBool` constant | `conf/lex.go:46` | PORTED | `NatsConfToken.cs:11` | `TokenType.Bool` |
| `itemInteger` constant | `conf/lex.go:47` | PORTED | `NatsConfToken.cs:12` | `TokenType.Integer` |
| `itemFloat` constant | `conf/lex.go:48` | PORTED | `NatsConfToken.cs:13` | `TokenType.Float` |
| `itemDatetime` constant | `conf/lex.go:49` | PORTED | `NatsConfToken.cs:14` | `TokenType.DateTime` |
| `itemArrayStart` constant | `conf/lex.go:50` | PORTED | `NatsConfToken.cs:15` | `TokenType.ArrayStart` |
| `itemArrayEnd` constant | `conf/lex.go:51` | PORTED | `NatsConfToken.cs:16` | `TokenType.ArrayEnd` |
| `itemMapStart` constant | `conf/lex.go:52` | PORTED | `NatsConfToken.cs:17` | `TokenType.MapStart` |
| `itemMapEnd` constant | `conf/lex.go:53` | PORTED | `NatsConfToken.cs:18` | `TokenType.MapEnd` |
| `itemCommentStart` constant | `conf/lex.go:54` | PARTIAL | `NatsConfToken.cs:21` | Go emits `itemCommentStart` then `itemText`; .NET `LexCommentStart` emits `TokenType.Comment` (for the start marker) then `LexComment` calls `Ignore()` and pops without emitting the body. Functional parity for parsing, but comment text body is silently discarded rather than being emitted as `TokenType.Text`. |
| `itemVariable` constant | `conf/lex.go:55` | PORTED | `NatsConfToken.cs:19` | `TokenType.Variable` |
| `itemInclude` constant | `conf/lex.go:56` | PORTED | `NatsConfToken.cs:20` | `TokenType.Include` |
| `stateFn` type | `conf/lex.go:84` | PORTED | `NatsConfLexer.cs:30` | Ported as `delegate LexState? LexState(NatsConfLexer lx)` — identical functional model |
| `lexer` struct | `conf/lex.go:86` | PORTED | `NatsConfLexer.cs:6` | All fields: `_input`, `_start`, `_pos`, `_width`, `_line`, `_stack`, `_stringParts`, `_stringStateFn`, `_lstart`, `_ilstart` |
| `item` struct | `conf/lex.go:113` | PORTED | `NatsConfToken.cs:24` | Ported as `readonly record struct Token(TokenType Type, string Value, int Line, int Position)` |
| `(lx *lexer) nextItem()` | `conf/lex.go:120` | PORTED | `NatsConfLexer.cs:62` | Go uses a goroutine channel; .NET runs state machine eagerly in `Tokenize()` collecting all tokens into a `List<Token>` — semantically equivalent |
| `lex(input string)` | `conf/lex.go:131` | PORTED | `NatsConfLexer.cs:48` | Ported as private constructor + `Tokenize()` factory |
| `(lx *lexer) push()` | `conf/lex.go:143` | PORTED | `NatsConfLexer.cs:75` | `Push()` using `Stack<LexState>` |
| `(lx *lexer) pop()` | `conf/lex.go:147` | PORTED | `NatsConfLexer.cs:77` | `Pop()` |
| `(lx *lexer) emit()` | `conf/lex.go:157` | PORTED | `NatsConfLexer.cs:87` | `Emit()` — identical logic for computing position and clearing stringParts |
| `(lx *lexer) emitString()` | `conf/lex.go:166` | PORTED | `NatsConfLexer.cs:106` | `EmitString()` |
| `(lx *lexer) addCurrentStringPart()` | `conf/lex.go:181` | PORTED | `NatsConfLexer.cs:125` | `AddCurrentStringPart()` |
| `(lx *lexer) addStringPart()` | `conf/lex.go:186` | PORTED | `NatsConfLexer.cs:131` | `AddStringPart()` |
| `(lx *lexer) hasEscapedParts()` | `conf/lex.go:192` | PORTED | `NatsConfLexer.cs:138` | `HasEscapedParts()` |
| `(lx *lexer) next()` | `conf/lex.go:196` | PARTIAL | `NatsConfLexer.cs:140` | Go uses `utf8.DecodeRuneInString` for multi-byte rune support; .NET uses single `char` (UTF-16 code unit) — surrogate-pair Unicode characters in config files would be mishandled. Extremely unlikely in practice for NATS config files. |
| `(lx *lexer) ignore()` | `conf/lex.go:215` | PORTED | `NatsConfLexer.cs:160` | `Ignore()` |
| `(lx *lexer) backup()` | `conf/lex.go:221` | PORTED | `NatsConfLexer.cs:166` | `Backup()` |
| `(lx *lexer) peek()` | `conf/lex.go:229` | PORTED | `NatsConfLexer.cs:175` | `Peek()` |
| `(lx *lexer) errorf()` | `conf/lex.go:238` | PARTIAL | `NatsConfLexer.cs:182` | Go version escapes rune arguments; .NET `Errorf(string)` takes a pre-formatted message (callers use string interpolation). Character escaping is done inline at call sites via `EscapeSpecial()`. Functionally equivalent. |
| `lexTop` | `conf/lex.go:257` | PORTED | `NatsConfLexer.cs:202` | `LexTop` static method |
| `lexTopValueEnd` | `conf/lex.go:296` | PORTED | `NatsConfLexer.cs:247` | `LexTopValueEnd` static method |
| `lexBlockStart` | `conf/lex.go:321` | PORTED | `NatsConfLexer.cs:291` | `LexBlockStart` static method |
| `lexBlockValueEnd` | `conf/lex.go:363` | PORTED | `NatsConfLexer.cs:338` | `LexBlockValueEnd` static method |
| `lexBlockEnd` | `conf/lex.go:392` | PORTED | `NatsConfLexer.cs:388` | `LexBlockEnd` static method |
| `lexKeyStart` | `conf/lex.go:422` | PORTED | `NatsConfLexer.cs:438` | `LexKeyStart` static method |
| `lexDubQuotedKey` | `conf/lex.go:443` | PORTED | `NatsConfLexer.cs:469` | `LexDubQuotedKey` static method |
| `lexQuotedKey` | `conf/lex.go:460` | PORTED | `NatsConfLexer.cs:494` | `LexQuotedKey` static method |
| `(lx *lexer) keyCheckKeyword()` | `conf/lex.go:480` | PORTED | `NatsConfLexer.cs:519` | `KeyCheckKeyword()` — instance method |
| `lexIncludeStart` | `conf/lex.go:495` | PORTED | `NatsConfLexer.cs:537` | `LexIncludeStart` static method |
| `lexIncludeQuotedString` | `conf/lex.go:507` | PORTED | `NatsConfLexer.cs:549` | `LexIncludeQuotedString` static method |
| `lexIncludeDubQuotedString` | `conf/lex.go:525` | PORTED | `NatsConfLexer.cs:569` | `LexIncludeDubQuotedString` static method |
| `lexIncludeString` | `conf/lex.go:541` | PORTED | `NatsConfLexer.cs:589` | `LexIncludeString` static method |
| `lexInclude` | `conf/lex.go:559` | PORTED | `NatsConfLexer.cs:611` | `LexInclude` static method |
| `lexKey` | `conf/lex.go:587` | PORTED | `NatsConfLexer.cs:646` | `LexKey` static method |
| `lexKeyEnd` | `conf/lex.go:601` | PORTED | `NatsConfLexer.cs:671` | `LexKeyEnd` static method |
| `lexValue` | `conf/lex.go:621` | PORTED | `NatsConfLexer.cs:695` | `LexValue` static method |
| `lexArrayValue` | `conf/lex.go:665` | PORTED | `NatsConfLexer.cs:745` | `LexArrayValue` static method |
| `lexArrayValueEnd` | `conf/lex.go:696` | PORTED | `NatsConfLexer.cs:788` | `LexArrayValueEnd` static method |
| `lexArrayEnd` | `conf/lex.go:723` | PORTED | `NatsConfLexer.cs:832` | `LexArrayEnd` static method |
| `lexMapKeyStart` | `conf/lex.go:732` | PORTED | `NatsConfLexer.cs:839` | `LexMapKeyStart` static method |
| `lexMapQuotedKey` | `conf/lex.go:772` | PORTED | `NatsConfLexer.cs:906` | `LexMapQuotedKey` static method |
| `lexMapDubQuotedKey` | `conf/lex.go:784` | PORTED | `NatsConfLexer.cs:925` | `LexMapDubQuotedKey` static method |
| `lexMapKey` | `conf/lex.go:799` | PORTED | `NatsConfLexer.cs:944` | `LexMapKey` static method |
| `lexMapKeyEnd` | `conf/lex.go:815` | PORTED | `NatsConfLexer.cs:972` | `LexMapKeyEnd` static method |
| `lexMapValue` | `conf/lex.go:833` | PORTED | `NatsConfLexer.cs:990` | `LexMapValue` static method |
| `lexMapValueEnd` | `conf/lex.go:850` | PORTED | `NatsConfLexer.cs:1013` | `LexMapValueEnd` static method |
| `lexMapEnd` | `conf/lex.go:875` | PORTED | `NatsConfLexer.cs:1059` | `LexMapEnd` static method |
| `(lx *lexer) isBool()` | `conf/lex.go:884` | PORTED | `NatsConfLexer.cs:1066` | `IsBool()` instance method |
| `(lx *lexer) isVariable()` | `conf/lex.go:892` | PORTED | `NatsConfLexer.cs:1072` | `IsVariable()` instance method |
| `lexQuotedString` | `conf/lex.go:906` | PORTED | `NatsConfLexer.cs:1088` | `LexQuotedString` static method |
| `lexDubQuotedString` | `conf/lex.go:928` | PORTED | `NatsConfLexer.cs:1114` | `LexDubQuotedString` static method |
| `lexString` | `conf/lex.go:951` | PORTED | `NatsConfLexer.cs:1146` | `LexString` static method |
| `lexBlock` | `conf/lex.go:986` | PORTED | `NatsConfLexer.cs:1193` | `LexBlock` static method |
| `lexStringEscape` | `conf/lex.go:1021` | PORTED | `NatsConfLexer.cs:1235` | `LexStringEscape` static method |
| `lexStringBinary` | `conf/lex.go:1043` | PORTED | `NatsConfLexer.cs:1250` | `LexStringBinary` static method — uses `Convert.FromHexString` + `Latin1.GetString` |
| `lexNumberOrDateOrStringOrIPStart` | `conf/lex.go:1065` | PORTED | `NatsConfLexer.cs:1276` | `LexNumberOrDateOrStringOrIPStart` static method |
| `lexNumberOrDateOrStringOrIP` | `conf/lex.go:1079` | PORTED | `NatsConfLexer.cs:1292` | `LexNumberOrDateOrStringOrIP` static method |
| `lexConvenientNumber` | `conf/lex.go:1106` | PORTED | `NatsConfLexer.cs:1333` | `LexConvenientNumber` static method |
| `lexDateAfterYear` | `conf/lex.go:1124` | PORTED | `NatsConfLexer.cs:1354` | `LexDateAfterYear` static method |
| `lexNegNumberStart` | `conf/lex.go:1152` | PORTED | `NatsConfLexer.cs:1385` | `LexNegNumberStart` static method |
| `lexNegNumber` | `conf/lex.go:1165` | PORTED | `NatsConfLexer.cs:1401` | `LexNegNumber` static method |
| `lexFloatStart` | `conf/lex.go:1182` | PORTED | `NatsConfLexer.cs:1424` | `LexFloatStart` static method |
| `lexFloat` | `conf/lex.go:1193` | PORTED | `NatsConfLexer.cs:1435` | `LexFloat` static method |
| `lexIPAddr` | `conf/lex.go:1210` | PORTED | `NatsConfLexer.cs:1454` | `LexIPAddr` static method |
| `lexCommentStart` | `conf/lex.go:1222` | PARTIAL | `NatsConfLexer.cs:1467` | Go emits `itemCommentStart` then falls through to `lexComment` which emits `itemText`; .NET emits `TokenType.Comment` then `LexComment` calls `Ignore()` silently discarding the body. Comment text body is unavailable in .NET. |
| `lexComment` | `conf/lex.go:1231` | PARTIAL | `NatsConfLexer.cs:1474` | Go emits `itemText` with comment body; .NET calls `Ignore()` — comment body is silently discarded. Same functional effect for config parsing. |
| `lexSkip` | `conf/lex.go:1242` | PORTED | `NatsConfLexer.cs:1489` | `LexSkip` static method — identical logic |
| `isNumberSuffix()` | `conf/lex.go:1250` | PORTED | `NatsConfLexer.cs:197` | `IsNumberSuffix()` static method |
| `isKeySeparator()` | `conf/lex.go:1255` | PORTED | `NatsConfLexer.cs:195` | `IsKeySeparator()` static method |
| `isWhitespace()` | `conf/lex.go:1261` | PORTED | `NatsConfLexer.cs:191` | `IsWhitespace()` static method |
| `isNL()` | `conf/lex.go:1265` | PORTED | `NatsConfLexer.cs:193` | `IsNL()` static method |
| `(itemType) String()` | `conf/lex.go:1269` | PORTED | — | Go's stringer is used for debugging; .NET `TokenType` is an enum (has `.ToString()` built in) — functionally equivalent |
| `(item) String()` | `conf/lex.go:1309` | PORTED | — | Go debug helper; .NET `Token` is a record struct with auto-generated `ToString()` |
| `escapeSpecial()` | `conf/lex.go:1313` | PORTED | `NatsConfLexer.cs:1495` | `EscapeSpecial()` static method — .NET version also handles `\r`, `\t`, and EOF |
---
### golang/nats-server/conf/parse.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `_EMPTY_` constant | `conf/parse.go:40` | NOT_APPLICABLE | — | Go-only string constant alias for `""` |
| `parser` struct | `conf/parse.go:42` | PORTED | `NatsConfParser.cs:88` | Ported as private `ParserState` class inside `NatsConfParser`. All fields present: `mapping`, `lx`/`_tokens`, `ctx`, `ctxs`, `keys`, `fp`/`_baseDir`. Missing: `ikeys` (pedantic item keys) and `pedantic` flag — see below. |
| `Parse()` | `conf/parse.go:71` | PORTED | `NatsConfParser.cs:29` | `NatsConfParser.Parse(string)` — identical signature and semantics |
| `ParseWithChecks()` | `conf/parse.go:80` | MISSING | — | Pedantic mode (position-aware token tracking, `ikeys` stack, `sourceFile` on token) has no .NET equivalent. Config validation tools using pedantic mode (e.g., `nats-server --config-check`) cannot be supported without this. |
| `ParseFile()` | `conf/parse.go:89` | PORTED | `NatsConfParser.cs:40` | `NatsConfParser.ParseFile(string)` |
| `ParseFileWithChecks()` | `conf/parse.go:103` | MISSING | — | Pedantic mode file variant — not ported. Same gap as `ParseWithChecks`. |
| `cleanupUsedEnvVars()` | `conf/parse.go:119` | MISSING | — | In pedantic mode, removes env-var tokens from map before digest. .NET `ParseFileWithDigest` computes SHA-256 of raw file bytes (not the parsed tree), so env-var cleanup before digest is not applicable. Functionally different approach but both produce a stable digest. |
| `ParseFileWithChecksDigest()` | `conf/parse.go:135` | PARTIAL | `NatsConfParser.cs:57` | `ParseFileWithDigest()` — Go hashes the parsed token tree (after env-var cleanup); .NET hashes the raw file bytes. Both produce a `"sha256:<hex>"` digest stable across re-reads. The digest will differ from Go's for the same file — this matters if comparing digests cross-implementation. |
| `token` struct | `conf/parse.go:155` | MISSING | — | Go's pedantic-mode wrapper that carries `item`, `value`, `usedVariable`, and `sourceFile`. .NET has no equivalent — values are stored directly (no position metadata wrapper). |
| `(t *token) MarshalJSON()` | `conf/parse.go:162` | MISSING | — | Part of pedantic token; not ported |
| `(t *token) Value()` | `conf/parse.go:166` | MISSING | — | Part of pedantic token; not ported |
| `(t *token) Line()` | `conf/parse.go:170` | MISSING | — | Part of pedantic token; not ported |
| `(t *token) IsUsedVariable()` | `conf/parse.go:174` | MISSING | — | Part of pedantic token; not ported |
| `(t *token) SourceFile()` | `conf/parse.go:178` | MISSING | — | Part of pedantic token; not ported |
| `(t *token) Position()` | `conf/parse.go:182` | MISSING | — | Part of pedantic token; not ported |
| `newParser()` | `conf/parse.go:186` | PORTED | `NatsConfParser.cs:105` | `ParserState` constructors |
| `parse()` | `conf/parse.go:199` | PORTED | `NatsConfParser.cs:118` | `ParserState.Run()` — identical loop structure |
| `parseEnv()` | `conf/parse.go:207` | PORTED | `NatsConfParser.cs:75` | `ParseEnvValue()` static method — same synthetic `pk=<value>` trick |
| `(p *parser) parse()` | `conf/parse.go:216` | PORTED | `NatsConfParser.cs:118` | `ParserState.Run()` |
| `(p *parser) next()` | `conf/parse.go:238` | PORTED | `NatsConfParser.cs:142` | `ParserState.Next()` |
| `(p *parser) pushContext()` | `conf/parse.go:242` | PORTED | `NatsConfParser.cs:152` | `PushContext()` |
| `(p *parser) popContext()` | `conf/parse.go:247` | PORTED | `NatsConfParser.cs:158` | `PopContext()` |
| `(p *parser) pushKey()` | `conf/parse.go:258` | PORTED | `NatsConfParser.cs:171` | `PushKey()` |
| `(p *parser) popKey()` | `conf/parse.go:262` | PORTED | `NatsConfParser.cs:173` | `PopKey()` |
| `(p *parser) pushItemKey()` | `conf/parse.go:272` | MISSING | — | Pedantic-mode only; no .NET equivalent |
| `(p *parser) popItemKey()` | `conf/parse.go:276` | MISSING | — | Pedantic-mode only; no .NET equivalent |
| `(p *parser) processItem()` | `conf/parse.go:286` | PORTED | `NatsConfParser.cs:205` | `ProcessItem()` — handles all token types including variable, include, map, array |
| `(p *parser) lookupVariable()` | `conf/parse.go:462` | PORTED | `NatsConfParser.cs:344` | `ResolveVariable()` — block scoping, env var lookup, cycle detection, bcrypt prefix all ported |
| `(p *parser) setValue()` | `conf/parse.go:500` | PORTED | `NatsConfParser.cs:185` | `SetValue()` — array and map context handling |
| `pkey` constant | `conf/parse.go:452` | PORTED | `NatsConfParser.cs:77` | Used in `ParseEnvValue` synthetic input (`"pk={value}"`) |
| `bcryptPrefix` constant | `conf/parse.go:455` | PARTIAL | `NatsConfParser.cs:20` | Go checks prefix `"2a$"`; .NET checks both `"2a$"` and `"2b$"` — .NET is a superset (handles both bcrypt variants) |
---
### golang/nats-server/conf/token.go (does not exist)
The `token.go` file listed in the gap analysis instructions does not exist in the Go source tree. The `item` and `itemType` declarations live in `lex.go`. The `token` struct (pedantic wrapper) lives in `parse.go`. The .NET `NatsConfToken.cs` consolidates both the token type enum (`TokenType`) and the token value record (`Token`).
---
### .NET-only Configuration Files (no direct Go counterpart in conf/)
These .NET files port functionality from `golang/nats-server/server/opts.go` and `golang/nats-server/server/reload.go` which are tracked under the Core Server category, but the implementations live in the Configuration folder:
| .NET File | Go Reference | Status | Notes |
|-----------|:-------------|--------|-------|
| `src/NATS.Server/Configuration/ConfigProcessor.cs` | `server/opts.go` (processConfigFileLine, ~line 1050) | PORTED | Maps parsed dict to `NatsOptions`. Covers: listen, port, host, logging, limits, monitoring, TLS, auth, cluster, gateway, leafnode, JetStream, MQTT config keys |
| `src/NATS.Server/Configuration/ConfigReloader.cs` | `server/reload.go` | PORTED | `Diff()`, `Validate()`, `MergeCliOverrides()`, `ApplyDiff()`, `ReloadAsync()`, `ReloadFromOptionsAsync()`, `ApplyClusterConfigChanges()`, `ApplyLoggingChanges()`, `PropagateAuthChanges()`, `ReloadTlsCertificates()`, `ReloadTlsCertificate()`, `ApplyJetStreamConfigChanges()` |
| `src/NATS.Server/Configuration/IConfigChange.cs` | `server/reload.go:4274` | PORTED | `IConfigChange` interface + `ConfigChange` implementation |
| `src/NATS.Server/Configuration/ClusterOptions.cs` | `server/opts.go` (ClusterOpts) | PORTED | Cluster options struct |
| `src/NATS.Server/Configuration/GatewayOptions.cs` | `server/opts.go` (GatewayOpts, RemoteGatewayOpts) | PORTED | Gateway options + `RemoteGatewayOptions` |
| `src/NATS.Server/Configuration/LeafNodeOptions.cs` | `server/opts.go` (LeafNodeOpts, RemoteLeafOpts) | PORTED | LeafNode options + `RemoteLeafOptions` |
| `src/NATS.Server/Configuration/JetStreamOptions.cs` | `server/opts.go` (JetStreamConfig) | PORTED | JetStream options struct |
| `src/NATS.Server/Configuration/RouteCompression.cs` | `server/opts.go` (Compression enum) | PORTED | `RouteCompression` enum (None, S2) |
| `src/NATS.Server/Configuration/SignalHandler.cs` | `server/signal_unix.go` | PORTED | SIGHUP handler via `PosixSignalRegistration` |
---
### conf/fuzz.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `Fuzz()` | `conf/fuzz.go:18` | NOT_APPLICABLE | — | Go fuzz test entry point (`//go:build gofuzz` build tag). .NET has its own fuzzing infrastructure (SharpFuzz / libFuzzer) but no equivalent fuzz target exists. Low priority. |
---
## 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/Configuration/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Configuration/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory completed: read all Go source files (lex.go, parse.go) and all .NET Configuration files; classified 100+ symbols | claude-sonnet-4-6 |

386
gaps/core-server.md Normal file
View File

@@ -0,0 +1,386 @@
# Core Server — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Core Server** 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 Core Server
- Go's `client.go` is the **largest single file** (~6,700 lines). It contains the readLoop/writeLoop goroutines, dynamic buffer sizing (512→65536), slow consumer detection, and per-client subscription tracking. The .NET equivalent should use `async/await` with `System.IO.Pipelines`.
- Go's `server.go` manages listeners, accept loops, and shutdown coordination. Map goroutines to `Task`-based async patterns.
- Go's `opts.go` handles both CLI flags and config file parsing (CLI overrides config). The .NET version separates this into `Configuration/`.
- `reload.go` implements hot-reload by diffing old vs new config and applying deltas without restart.
- Platform-specific files (`service_windows.go`, `signal_windows.go`, `signal_wasm.go`) may be NOT_APPLICABLE.
---
## Go Reference Files (Source)
- `golang/nats-server/server/server.go` — Server struct, startup lifecycle, listener management
- `golang/nats-server/server/client.go` — Connection handling, readLoop/writeLoop, per-client state (~6,700 lines)
- `golang/nats-server/server/opts.go` — Server options struct, CLI flag parsing, config file loading
- `golang/nats-server/server/reload.go` — Hot reload on signal, config diffing
- `golang/nats-server/server/service.go` — OS service/daemon management
- `golang/nats-server/server/signal.go` — Signal handling (SIGHUP, SIGTERM, etc.)
- `golang/nats-server/main.go` — Entry point
## Go Reference Files (Tests)
- `golang/nats-server/server/server_test.go`
- `golang/nats-server/server/client_test.go`
- `golang/nats-server/server/opts_test.go`
- `golang/nats-server/server/reload_test.go`
- `golang/nats-server/server/signal_test.go`
- `golang/nats-server/server/test_test.go` (test helpers)
## .NET Implementation Files (Source)
- `src/NATS.Server/NatsServer.cs` — Server lifecycle
- `src/NATS.Server/NatsClient.cs` — Per-connection client
- `src/NATS.Server/NatsOptions.cs` — Server options
- `src/NATS.Server/ClientFlags.cs`
- `src/NATS.Server/ClientKind.cs`
- `src/NATS.Server/ClientClosedReason.cs`
- `src/NATS.Server/ClientTraceInfo.cs`
- `src/NATS.Server/ClosedState.cs`
- `src/NATS.Server/INatsClient.cs`
- `src/NATS.Server/InternalClient.cs`
- `src/NATS.Server/ServerStats.cs`
- `src/NATS.Server/SlowConsumerTracker.cs`
- `src/NATS.Server/MqttOptions.cs`
- `src/NATS.Server.Host/Program.cs` — Entry point
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/` (root-level test files)
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| **golang/nats-server/main.go** | | | | |
| main() | main.go:97 | PORTED | src/NATS.Server.Host/Program.cs:1 | CLI arg parsing, config load, server create, start, wait |
| usage() | main.go:92 | PARTIAL | src/NATS.Server.Host/Program.cs | No dedicated --help usage string; CLI flags are handled inline |
| **golang/nats-server/server/server.go — Types** | | | | |
| Info struct | server.go:109 | PORTED | src/NATS.Server/Protocol/NatsProtocol.cs:39 (ServerInfo) | Core fields ported; route/gateway/leafnode-specific fields are partial |
| Server struct | server.go:169 | PARTIAL | src/NATS.Server/NatsServer.cs:31 | Core fields ported (clients, accounts, opts, listener, shutdown). Missing: route pool tracking, gateway internals, OCSP monitors, proxied conns, rate limiting maps |
| stats struct | server.go:402 | PORTED | src/NATS.Server/ServerStats.cs:6 | All counters present |
| scStats struct | server.go:413 | PORTED | src/NATS.Server/ServerStats.cs:18-25 | Per-kind slow consumer counters present |
| staleStats struct | server.go:421 | PORTED | src/NATS.Server/ServerStats.cs:26 | Per-kind stale connection counters present |
| nodeInfo struct | server.go:387 | NOT_APPLICABLE | — | JetStream cluster-specific; tracked in JetStream module |
| Ports struct | server.go:4236 | MISSING | — | Not implemented; no /ports output support |
| Compression constants | server.go:437-446 | MISSING | — | S2 compression mode constants not defined in core server |
| CompressionOpts struct | server.go:97 (opts.go) | MISSING | — | No compression options type in .NET |
| **golang/nats-server/server/server.go — Exported Server Methods** | | | | |
| NewServer() | server.go:716 | PORTED | src/NATS.Server/NatsServer.cs constructor | Options validation, NKey identity, info setup |
| New() (deprecated) | server.go:698 | NOT_APPLICABLE | — | Deprecated wrapper |
| NewServerFromConfig() | server.go:703 | PORTED | src/NATS.Server.Host/Program.cs:20-22 | Config file processing then new server |
| (s) Start() | server.go:2263 | PORTED | src/NATS.Server/NatsServer.cs:502 (StartAsync) | Listener, accept loop, subsystem startup. Async in .NET |
| (s) Shutdown() | server.go:2583 | PORTED | src/NATS.Server/NatsServer.cs:163 (ShutdownAsync) | Client disconnect, listener close, subsystem teardown |
| (s) WaitForShutdown() | server.go:2775 | PORTED | src/NATS.Server/NatsServer.cs:150 | Blocks until shutdown complete |
| (s) AcceptLoop() | server.go:2780 | PORTED | src/NATS.Server/NatsServer.cs:502 (inline in StartAsync) | Accept loop is part of StartAsync |
| (s) ReadyForConnections() | server.go:4023 | PORTED | src/NATS.Server/NatsServer.cs:148 (WaitForReadyAsync) | Async TaskCompletionSource-based |
| (s) Running() | server.go:1695 | PORTED | src/NATS.Server/NatsServer.cs:108 (IsShuttingDown inverted) | Derived from shutdown flag |
| (s) ID() | server.go:4036 | PORTED | src/NATS.Server/NatsServer.cs:101 (ServerId) | — |
| (s) Name() | server.go:4046 | PORTED | src/NATS.Server/NatsServer.cs:102 (ServerName) | — |
| (s) NodeName() | server.go:4041 | PORTED | src/NATS.Server/NatsServer.cs:102 | Same as ServerName |
| (s) ClusterName() | server.go:1017 | PARTIAL | src/NATS.Server/NatsServer.cs:110 (ClusterListen) | Only listen endpoint; no cluster name getter |
| (s) ClientURL() | server.go:1086 | MISSING | — | No dedicated method to return client connect URL |
| (s) WebsocketURL() | server.go:1100 | MISSING | — | No dedicated websocket URL getter |
| (s) NumClients() | server.go:3810 | PORTED | src/NATS.Server/NatsServer.cs:103 (ClientCount) | — |
| (s) NumRoutes() | server.go:3773 | PARTIAL | src/NATS.Server/ServerStats.cs:14 (Routes field) | Stats counter exists; no lock-safe method like Go |
| (s) NumRemotes() | server.go:3790 | MISSING | — | — |
| (s) NumLeafNodes() | server.go:3803 | PARTIAL | src/NATS.Server/ServerStats.cs:16 (Leafs field) | Stats counter; no lock-safe count method |
| (s) NumSubscriptions() | server.go:3836 | MISSING | — | No aggregated subscription count method |
| (s) NumSlowConsumers() | server.go:3855 | PORTED | src/NATS.Server/ServerStats.cs:12 | Direct field access |
| (s) NumSlowConsumersClients() | server.go:3865 | PORTED | src/NATS.Server/ServerStats.cs:18 | — |
| (s) NumSlowConsumersRoutes() | server.go:3870 | PORTED | src/NATS.Server/ServerStats.cs:19 | — |
| (s) NumSlowConsumersGateways() | server.go:3875 | PORTED | src/NATS.Server/ServerStats.cs:21 | — |
| (s) NumSlowConsumersLeafs() | server.go:3880 | PORTED | src/NATS.Server/ServerStats.cs:20 | — |
| (s) NumStalledClients() | server.go:3860 | PORTED | src/NATS.Server/ServerStats.cs:17 (Stalls) | — |
| (s) NumStaleConnections() | server.go:3885 | PORTED | src/NATS.Server/ServerStats.cs:13 | — |
| (s) NumStaleConnectionsClients() | server.go:3890 | PORTED | src/NATS.Server/ServerStats.cs:22 | — |
| (s) NumStaleConnectionsRoutes() | server.go:3895 | PORTED | src/NATS.Server/ServerStats.cs:23 | — |
| (s) NumStaleConnectionsGateways() | server.go:3900 | PORTED | src/NATS.Server/ServerStats.cs:24 | — |
| (s) NumStaleConnectionsLeafs() | server.go:3905 | PORTED | src/NATS.Server/ServerStats.cs:25 | — |
| (s) GetClient() | server.go:3817 | PORTED | src/NATS.Server/NatsServer.cs:119 (GetClients enumerable) | Enumerable, not by-ID lookup |
| (s) GetLeafNode() | server.go:3829 | MISSING | — | No leaf node by-CID lookup |
| (s) ConfigTime() | server.go:3910 | MISSING | — | No config time tracking exposed |
| (s) Addr() | server.go:3917 | PARTIAL | src/NATS.Server/NatsServer.cs:104 (Port) | Port exposed but not full net.Addr |
| (s) MonitorAddr() | server.go:3927 | MISSING | — | Monitoring managed by MonitorServer separately |
| (s) ClusterAddr() | server.go:3937 | PARTIAL | src/NATS.Server/NatsServer.cs:110 (ClusterListen string) | String, not TCPAddr |
| (s) ProfilerAddr() | server.go:3947 | MISSING | — | No profiler address getter |
| (s) ActivePeers() | server.go:1577 | MISSING | — | Cluster peer enumeration not in core |
| (s) NumActiveAccounts() | server.go:1716 | MISSING | — | No active account count method |
| (s) NumLoadedAccounts() | server.go:1744 | PARTIAL | src/NATS.Server/NatsServer.cs:123 (GetAccounts) | Enumerable, no count method |
| (s) LookupOrRegisterAccount() | server.go:1749 | PORTED | src/NATS.Server/NatsServer.cs:1260 (GetOrCreateAccount) | — |
| (s) RegisterAccount() | server.go:1762 | PORTED | src/NATS.Server/NatsServer.cs:1260 | Via GetOrCreateAccount |
| (s) SetSystemAccount() | server.go:1775 | PORTED | src/NATS.Server/NatsServer.cs constructor | Set during construction |
| (s) SystemAccount() | server.go:1798 | PORTED | src/NATS.Server/NatsServer.cs:105 | — |
| (s) GlobalAccount() | server.go:1804 | PORTED | src/NATS.Server/NatsServer.cs:50 (_globalAccount) | — |
| (s) LookupAccount() | server.go:2106 | PORTED | src/NATS.Server/NatsServer.cs:1260 | Via GetOrCreateAccount |
| (s) StartProfiler() | server.go:2941 | MISSING | — | No built-in profiler; .NET uses dotnet-trace/counters |
| (s) StartMonitoring() | server.go:3014 | PORTED | src/NATS.Server/Monitoring/MonitorServer.cs | Separate monitoring server class |
| (s) StartHTTPMonitoring() | server.go:3003 | PORTED | src/NATS.Server/Monitoring/MonitorServer.cs:140 | — |
| (s) StartHTTPSMonitoring() | server.go:3009 | PORTED | src/NATS.Server/Monitoring/MonitorServer.cs | HTTPS variant via options |
| (s) HTTPHandler() | server.go:3207 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs | ASP.NET Kestrel handles routing, not an http.Handler |
| (s) InProcessConn() | server.go:2876 | MISSING | — | No in-process connection support |
| (s) LameDuckShutdown() | server.go:4421 | PORTED | src/NATS.Server/NatsServer.cs:239 (LameDuckShutdownAsync) | Full LDM with grace period and duration |
| (s) DisconnectClientByID() | server.go:4742 | MISSING | — | No per-client disconnect by ID |
| (s) LDMClientByID() | server.go:4757 | MISSING | — | No per-client lame duck by ID |
| (s) PortsInfo() | server.go:4247 | MISSING | — | No Ports struct output |
| (s) String() | server.go:4050 | MISSING | — | No server string representation |
| PrintAndDie() | server.go:1664 | NOT_APPLICABLE | — | .NET uses exceptions/logging |
| PrintServerAndExit() | server.go:1670 | NOT_APPLICABLE | — | .NET uses --version flag differently |
| ProcessCommandLineArgs() | server.go:1678 | PORTED | src/NATS.Server.Host/Program.cs:25-137 | Inline switch-based CLI parsing |
| **golang/nats-server/server/server.go — Key Unexported Helpers** | | | | |
| (s) createClient() | server.go:3245 | PORTED | src/NATS.Server/NatsServer.cs:666 (AcceptClientAsync) | Creates NatsClient, runs it |
| (s) createInternalClient() | server.go:1925 | PORTED | src/NATS.Server/InternalClient.cs:29 | InternalClient class |
| (s) createInternalSystemClient() | server.go:1910 | PORTED | src/NATS.Server/NatsServer.cs:377 | System internal client creation |
| (s) acceptConnections() | server.go:2889 | PORTED | src/NATS.Server/NatsServer.cs:502 (StartAsync accept loop) | Inline in StartAsync |
| (s) removeClient() | server.go:3708 | PORTED | src/NATS.Server/NatsServer.cs RemoveClient() via IMessageRouter | — |
| (s) saveClosedClient() | server.go:3561 | PORTED | src/NATS.Server/Monitoring/ClosedConnectionRingBuffer.cs | ClosedConnectionRingBuffer |
| (s) configureAccounts() | server.go:1230 | PARTIAL | src/NATS.Server/NatsServer.cs constructor | Basic account setup; no multi-account import/export wiring |
| (s) setInfoHostPort() | server.go:2921 | PORTED | src/NATS.Server/NatsServer.cs:496 (BuildCachedInfo) | — |
| (s) lameDuckMode() | server.go:4428 | PORTED | src/NATS.Server/NatsServer.cs:239 (LameDuckShutdownAsync) | Async version |
| (s) handleSignals() | server.go (signal.go:37) | PORTED | src/NATS.Server/NatsServer.cs:320 (HandleSignals) | Uses PosixSignalRegistration on .NET |
| (s) logPorts() | server.go:4332 | PARTIAL | src/NATS.Server/NatsServer.cs | Logs port at startup; no ports file |
| (s) startGoRoutine() | server.go:4070 | NOT_APPLICABLE | — | .NET uses Task.Run; no goroutine tracking needed |
| (s) readyForConnections() | server.go:3956 | PORTED | src/NATS.Server/NatsServer.cs:148 (WaitForReadyAsync) | — |
| (s) getOpts() | server.go:1206 | PORTED | src/NATS.Server/NatsServer.cs:33 (_options field) | Direct field access |
| (s) isRunning() | server.go:1700 | PORTED | src/NATS.Server/NatsServer.cs:108 | Inverted IsShuttingDown |
| (s) isShuttingDown() | server.go:2577 | PORTED | src/NATS.Server/NatsServer.cs:108 (IsShuttingDown) | — |
| (s) updateServerINFOAndSendINFOToClients() | server.go:3622 | MISSING | — | No dynamic INFO update broadcast to existing clients |
| (s) getConnectURLs() | server.go:4120 | MISSING | — | No connect URL resolution for clustering |
| (s) getNonLocalIPsIfHostIsIPAny() | server.go:4159 | MISSING | — | IP enumeration for advertise not implemented |
| (s) portFile() | server.go:4307 | MISSING | — | No ports file creation |
| (s) logPid() | server.go:1704 | PARTIAL | src/NATS.Server/NatsServer.cs | PID file support present in options but write logic minimal |
| validateAndNormalizeCompressionOption() | server.go:466 | MISSING | — | No compression option validation |
| selectCompressionMode() | server.go:559 | MISSING | — | No compression mode negotiation |
| selectS2AutoModeBasedOnRTT() | server.go:618 | MISSING | — | No RTT-based auto compression |
| needsCompression() | server.go:549 | MISSING | — | — |
| s2WriterOptions() | server.go:678 | MISSING | — | — |
| **golang/nats-server/server/client.go — Types** | | | | |
| client struct | client.go:259 | PORTED | src/NATS.Server/NatsClient.cs:35 | Core fields: cid, kind, subs, opts, flags, stats. Missing: route/gw/leaf/mqtt sub-structs, perms cache, nonce, pubKey |
| ClosedState enum | client.go:188 | PORTED | src/NATS.Server/ClosedState.cs:14 | Full enum parity with all values |
| ClientOpts struct | client.go:661 | PORTED | src/NATS.Server/Protocol/ClientOptions.cs | Echo, Verbose, Pedantic, TLS, JWT, NKey, Headers, NoResponders |
| clientFlag type | client.go:132 | PORTED | src/NATS.Server/ClientFlags.cs:9 | Core flags present; missing some (infoReceived, noReconnect, expectConnect, compressionNegotiated, didTLSFirst) |
| WriteTimeoutPolicy | client.go:239 | PORTED | src/NATS.Server/NatsClient.cs:975 | Close and TcpFlush policies |
| subscription struct | client.go:631 | PORTED | src/NATS.Server/Subscriptions/Subscription.cs | Subject, queue, sid, client, max, messageCount |
| outbound struct | client.go:349 | PARTIAL | src/NATS.Server/NatsClient.cs:47-48 | Channel-based outbound; missing: net.Buffers, sync.Cond, S2 writer, stall channel |
| readCache struct | client.go:485 | MISSING | — | No per-client subject lookup cache |
| perAccountCache struct | client.go:541 | MISSING | — | No per-account L1 cache for routes/gateways |
| pinfo struct | client.go:343 | PORTED | src/NATS.Server/NatsClient.cs:99 (_pingsOut) | Simplified: no timer struct, uses PeriodicTimer |
| permissions struct | client.go:430 | PORTED | src/NATS.Server/Auth/ClientPermissions.cs | Pub/sub allow/deny; missing: pcache (permission cache) |
| routeTarget struct | client.go:458 | MISSING | — | Route message targeting structure |
| msgDeny struct | client.go:451 | MISSING | — | Deny-based message filtering cache |
| resp struct | client.go:442 | MISSING | — | Dynamic response permission tracking |
| CLIENT/ROUTER/GATEWAY/SYSTEM/LEAF/JETSTREAM/ACCOUNT constants | client.go:44-60 | PORTED | src/NATS.Server/ClientKind.cs:8 | All client kinds present |
| isInternalClient() | client.go:63 | PORTED | src/NATS.Server/ClientKind.cs:20 (IsInternal extension) | — |
| NON_CLIENT/NATS/MQTT/WS constants | client.go:70-79 | PARTIAL | src/NATS.Server/NatsClient.cs:107 (IsWebSocket) | WebSocket bool exists; no explicit MQTT/NATS/NON_CLIENT sub-type enum |
| ClientProtoZero/ClientProtoInfo | client.go:82-88 | MISSING | — | Client protocol version constants not defined |
| **golang/nats-server/server/client.go — Exported Methods** | | | | |
| (c) String() | client.go:547 | MISSING | — | No formatted string representation |
| (c) GetNonce() | client.go:557 | PARTIAL | src/NATS.Server/NatsClient.cs:43 (_nonce field) | Field exists but no public getter |
| (c) GetName() | client.go:565 | PARTIAL | src/NATS.Server/NatsClient.cs:58 (ClientOpts?.Name) | Via ClientOpts property |
| (c) GetOpts() | client.go:573 | PORTED | src/NATS.Server/NatsClient.cs:58 (ClientOpts) | — |
| (c) GetTLSConnectionState() | client.go:579 | PORTED | src/NATS.Server/NatsClient.cs:110 (TlsState) | TlsConnectionState type |
| (c) RemoteAddress() | client.go:822 | PORTED | src/NATS.Server/NatsClient.cs:85-86 (RemoteIp, RemotePort) | Separate IP and port properties |
| (c) Kind() | client.go:844 | PORTED | src/NATS.Server/NatsClient.cs:57 (Kind property) | — |
| (c) RegisterUser() | client.go:981 | MISSING | — | User registration on client not implemented |
| (c) RegisterNkeyUser() | client.go:1018 | MISSING | — | NKey user registration on client not implemented |
| (c) Account() | client.go:6204 | PORTED | src/NATS.Server/NatsClient.cs:61 (Account property) | — |
| (c) Error/Errorf/Debugf/Noticef/Tracef/Warnf | client.go:6586-6610 | PORTED | src/NATS.Server/NatsClient.cs:52 (_logger) | Uses ILogger<T> structured logging |
| (c) RateLimitErrorf/Warnf/Debugf | client.go:6610-6650 | MISSING | — | No rate-limited logging per client |
| **golang/nats-server/server/client.go — Key Unexported Methods** | | | | |
| (c) initClient() | client.go:704 | PORTED | src/NATS.Server/NatsClient.cs:116 (constructor) | CID assignment, outbound setup, subs map, trace level |
| (c) readLoop() | client.go:1358 | PORTED | src/NATS.Server/NatsClient.cs:296 (FillPipeAsync + ProcessCommandsAsync) | System.IO.Pipelines instead of goroutine + byte buffer |
| (c) writeLoop() | client.go:1274 | PORTED | src/NATS.Server/NatsClient.cs:785 (RunWriteLoopAsync) | Channel-based async write loop |
| (c) processConnect() | client.go:2167 | PORTED | src/NATS.Server/NatsClient.cs:450 (ProcessConnectAsync) | JSON deserialization, auth, account binding |
| (c) processPub() | client.go:2822 | PORTED | src/NATS.Server/NatsClient.cs:627 (ProcessPub) | Subject validation, permission check, router dispatch |
| (c) processHeaderPub() | client.go:2745 | PORTED | src/NATS.Server/NatsClient.cs:660 | HPUB handling with header size split |
| (c) parseSub() / processSub() | client.go:2898/2927 | PORTED | src/NATS.Server/NatsClient.cs:560 (ProcessSub) | Permission check, limit check, sublist insert |
| (c) processUnsub() | client.go:3366 | PORTED | src/NATS.Server/NatsClient.cs:605 (ProcessUnsub) | Max messages auto-unsub, sublist remove |
| (c) processPing() | client.go:2628 | PORTED | src/NATS.Server/NatsClient.cs:414 (Pong case in DispatchCommandAsync) | — |
| (c) processPong() | client.go:2680 | PORTED | src/NATS.Server/NatsClient.cs:419-428 | RTT calculation, pingsOut reset |
| (c) queueOutbound() | client.go:2448 | PORTED | src/NATS.Server/NatsClient.cs:140 (QueueOutbound) | Channel-based with pending bytes tracking |
| (c) flushOutbound() | client.go:1618 | PORTED | src/NATS.Server/NatsClient.cs:785 (RunWriteLoopAsync drain) | Inline in write loop |
| (c) flushSignal() | client.go:1996 | PORTED | src/NATS.Server/NatsClient.cs:197 (SignalFlushPending) | Interlocked counter |
| (c) sendPing() | client.go:2577 | PORTED | src/NATS.Server/NatsClient.cs:888 | RTT start tracking, write PING |
| (c) sendPong() | client.go:2540 | PORTED | src/NATS.Server/NatsClient.cs:397 | Write PONG bytes |
| (c) sendRTTPing() | client.go:2548 | PORTED | src/NATS.Server/NatsClient.cs:887 | RTT start tick recording |
| (c) sendErr() | client.go:2608 | PORTED | src/NATS.Server/NatsClient.cs:779 (SendErr) | — |
| (c) sendOK() | client.go:2619 | PORTED | src/NATS.Server/NatsClient.cs:344 (OkBytes write on Verbose) | — |
| (c) processPingTimer() | client.go:5537 | PORTED | src/NATS.Server/NatsClient.cs:853 (RunPingTimerAsync) | PeriodicTimer-based, stale detection |
| (c) closeConnection() | client.go:5868 | PORTED | src/NATS.Server/NatsClient.cs:834 (CloseWithReasonAsync) | Async close with reason |
| (c) markConnAsClosed() | client.go:1906 | PORTED | src/NATS.Server/NatsClient.cs:902 (MarkClosed) | Skip-flush flag, reason tracking |
| (c) deliverMsg() | client.go:3620 | PARTIAL | src/NATS.Server/NatsClient.cs:692 (SendMessage) | Basic MSG/HMSG delivery. Missing: echo check, MQTT interop, GW reply mapping, stall wait |
| (c) processInboundMsg() | client.go:4139 | PARTIAL | src/NATS.Server/NatsServer.cs:938 (ProcessMessage) | Routes to local subs; missing: route/gateway/leaf-specific inbound handlers |
| (c) processInboundClientMsg() | client.go:4166 | PARTIAL | src/NATS.Server/NatsServer.cs:938 | Core pub path ported; missing: subject mapping, GW reply mapping, NRG prefix check |
| (c) processMsgResults() | client.go:4932 | PARTIAL | src/NATS.Server/NatsServer.cs:938-1088 | Queue group selection ported; missing: route forwarding, gateway forwarding, queue name collection |
| (c) stalledWait() | client.go:3575 | PORTED | src/NATS.Server/NatsClient.cs:1017 (StallGate class) | SemaphoreSlim-based stall gate |
| (c) flushClients() | client.go:1324 | MISSING | — | Post-readLoop batch flush of producer clients (pcd) not implemented |
| (c) handleWriteTimeout() | client.go:1842 | PARTIAL | src/NATS.Server/NatsClient.cs:807-813 | Slow consumer detection on timeout; missing: retry policy for routes |
| (c) msgHeader() | client.go:3534 | PORTED | src/NATS.Server/NatsClient.cs:692 (SendMessage) | Inline MSG line construction |
| (c) msgHeaderForRouteOrLeaf() | client.go:3447 | MISSING | — | RMSG header construction for routes/leafnodes |
| (c) setPermissions() | client.go:1055 | PORTED | src/NATS.Server/NatsClient.cs:477 | Via ClientPermissions.Build(authResult.Permissions) |
| (c) pubAllowed() | client.go:4052 | PORTED | src/NATS.Server/Auth/ClientPermissions.cs | IsPublishAllowed() |
| (c) canSubscribe() | client.go:3230 | PORTED | src/NATS.Server/Auth/ClientPermissions.cs | IsSubscribeAllowed() |
| (c) setExpiration() | client.go:1243 | PORTED | src/NATS.Server/NatsClient.cs:534-557 | Auth expiry timer via Task.Delay |
| (c) authTimeout() | client.go:2383 | PORTED | src/NATS.Server/NatsClient.cs:238-253 | Auth timeout Task |
| (c) authViolation() | client.go:2398 | PORTED | src/NATS.Server/NatsClient.cs:471-473 | Auth violation error + close |
| (c) maxPayloadViolation() | client.go:2440 | PORTED | src/NATS.Server/NatsClient.cs:633-639 | Max payload check and close |
| (c) maxSubsExceeded() | client.go:2433 | PORTED | src/NATS.Server/NatsClient.cs:571-576 | — |
| (c) maxConnExceeded() | client.go:2428 | PARTIAL | src/NATS.Server/NatsServer.cs:610 | Server-level max conn check; no per-client method |
| (c) subsAtLimit() | client.go:900 | PORTED | src/NATS.Server/NatsClient.cs:571 | MaxSubs check in ProcessSub |
| (c) applyAccountLimits() | client.go:923 | PARTIAL | src/NATS.Server/NatsClient.cs:488-494 | Account client count check; missing: maxPayload/maxSubs per-account override |
| (c) registerWithAccount() | client.go:854 | PORTED | src/NATS.Server/NatsClient.cs:480-494 | Account binding during connect |
| (c) setTraceLevel() | client.go:695 | PORTED | src/NATS.Server/NatsClient.cs:68 (SetTraceMode) | — |
| (c) clientType() | client.go:599 | PARTIAL | src/NATS.Server/NatsClient.cs:107 (IsWebSocket) | Bool for WS; no MQTT/NATS sub-type dispatch |
| (c) addShadowSubscriptions() | client.go:3057 | MISSING | — | Account import shadow subscription system |
| (c) pruneDenyCache() / prunePubPermsCache() / pruneReplyPerms() | client.go:4007-4019 | MISSING | — | Permission cache pruning |
| (c) trackRemoteReply() / pruneRemoteTracking() | client.go:3915/3956 | MISSING | — | Reply tracking for latency |
| (c) loadMsgDenyFilter() | client.go:1265 | MISSING | — | Message-level deny filter loading |
| (c) generateClientInfoJSON() | client.go:2589 | PARTIAL | src/NATS.Server/NatsClient.cs:674 (SendInfo) | Generates INFO JSON; missing: per-client IP inclusion, connect_urls |
| (c) updateS2AutoCompressionLevel() | client.go:2723 | MISSING | — | RTT-based compression adjustment |
| (c) addToPCD() | client.go:3905 | MISSING | — | Producer client data batch tracking |
| (c) collapsePtoNB() | client.go:1608 | NOT_APPLICABLE | — | Go-specific net.Buffers collapse; .NET uses Channel<T> |
| nbPoolGet/nbPoolPut | client.go:393-423 | NOT_APPLICABLE | — | Go-specific sync.Pool; .NET uses ArrayPool/MemoryPool |
| **golang/nats-server/server/opts.go — Types** | | | | |
| Options struct | opts.go:326 | PORTED | src/NATS.Server/NatsOptions.cs:8 | Core fields present. Missing: DontListen, NoLog, NoSigs, ProxyRequired, CustomAuth, CheckConfig, JetStreamTpm, resolverPreloads |
| ClusterOpts struct | opts.go:64 | PORTED | src/NATS.Server/Configuration/ClusterOptions.cs | Core fields; missing: compression, pool size, pinned accounts, write timeout policy |
| GatewayOpts struct | opts.go:114 | PORTED | src/NATS.Server/Configuration/GatewayOptions.cs | Core fields; missing: compression, reject unknown, send qsubs buf |
| RemoteGatewayOpts struct | opts.go:145 | PORTED | src/NATS.Server/Configuration/GatewayOptions.cs:27 | — |
| LeafNodeOpts struct | opts.go:154 | PORTED | src/NATS.Server/Configuration/LeafNodeOptions.cs:22 | Core fields; missing: compression, isolate subjects, min version |
| RemoteLeafOpts struct | opts.go:218 | PORTED | src/NATS.Server/Configuration/LeafNodeOptions.cs:7 | — |
| WebsocketOpts struct | opts.go:518 | PORTED | src/NATS.Server/NatsOptions.cs:138 (WebSocketOptions) | — |
| MQTTOpts struct | opts.go:613 | PORTED | src/NATS.Server/MqttOptions.cs:8 | — |
| TLSConfigOpts struct | opts.go:790 | PARTIAL | src/NATS.Server/NatsOptions.cs:96-107 | Flat TLS fields on NatsOptions; no TLSConfigOpts class |
| OCSPConfig struct | opts.go:823 | PARTIAL | src/NATS.Server/NatsOptions.cs:110 (OcspConfig) | Basic config; missing: full OCSP mode selection |
| AuthCallout struct | opts.go:308 | MISSING | — | External auth callout configuration |
| JSLimitOpts struct | opts.go:289 | MISSING | — | Per-account JetStream limit options |
| JSTpmOpts struct | opts.go:300 | NOT_APPLICABLE | — | TPM (Trusted Platform Module) not applicable to .NET |
| ProxiesConfig struct | opts.go:832 | MISSING | — | Proxy configuration |
| PinnedCertSet type | opts.go:59 | PORTED | src/NATS.Server/NatsOptions.cs:106 (TlsPinnedCerts HashSet) | — |
| **golang/nats-server/server/opts.go — Exported Functions** | | | | |
| ProcessConfigFile() | opts.go:870 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs:15 | Full config file parsing |
| ConfigureOptions() | opts.go:6023 | PORTED | src/NATS.Server.Host/Program.cs:25-137 | CLI flag parsing inline |
| MergeOptions() | opts.go:5714 | PORTED | src/NATS.Server/Configuration/ConfigReloader.cs MergeCliOverrides | — |
| RoutesFromStr() | opts.go:5797 | MISSING | — | Parse comma-separated route URLs |
| GenTLSConfig() | opts.go:5633 | PARTIAL | src/NATS.Server/Tls/ | TLS setup exists but not as a standalone GenTLSConfig function |
| PrintTLSHelpAndDie() | opts.go:4886 | NOT_APPLICABLE | — | Go-specific CLI help |
| NoErrOnUnknownFields() | opts.go:50 | MISSING | — | Config parsing error control |
| **golang/nats-server/server/opts.go — Exported Options Methods** | | | | |
| (o) Clone() | opts.go:715 | MISSING | — | Deep copy of Options not implemented |
| (o) ProcessConfigFile() | opts.go:974 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs:17 | — |
| (o) ProcessConfigString() | opts.go:990 | MISSING | — | Parse config from string |
| (o) ConfigDigest() | opts.go:1000 | MISSING | — | Config file digest |
| **golang/nats-server/server/reload.go** | | | | |
| FlagSnapshot var | reload.go:36 | PORTED | src/NATS.Server/NatsServer.cs:44-46 (_cliSnapshot, _cliFlags) | — |
| option interface | reload.go:43 | PORTED | src/NATS.Server/Configuration/IConfigChange.cs | IConfigChange with Apply, IsLoggingChange, etc. |
| noopOption / loggingOption / traceLevelOption | reload.go:77-129 | PORTED | src/NATS.Server/Configuration/IConfigChange.cs:42 (ConfigChange) | Simplified: single ConfigChange class with flags |
| traceOption / debugOption / logfileOption / etc. | reload.go:131-200+ | PARTIAL | src/NATS.Server/Configuration/ConfigReloader.cs | Diff-based; individual option types not replicated, uses property comparison |
| (s) Reload() | reload.go:1090 | PORTED | src/NATS.Server/NatsServer.cs:1623 (ReloadConfig) | Reads config file, diffs, applies |
| (s) ReloadOptions() | reload.go:1111 | PORTED | src/NATS.Server/NatsServer.cs:1633 (ReloadConfigCore) | Config diff and apply |
| reloadContext struct | reload.go:38 | MISSING | — | Reload context for cluster perms |
| applyBoolFlags() | reload.go:1185 | PARTIAL | src/NATS.Server/Configuration/ConfigReloader.cs MergeCliOverrides | CLI override merge exists; no bool flag reflection |
| **golang/nats-server/server/signal.go** | | | | |
| SetProcessName() | signal.go:32 | NOT_APPLICABLE | — | Unix-specific process name |
| (s) handleSignals() | signal.go:37 | PORTED | src/NATS.Server/NatsServer.cs:320 (HandleSignals) | PosixSignalRegistration for SIGINT/SIGTERM/SIGHUP/SIGUSR2 |
| ProcessSignal() | signal.go:89 | NOT_APPLICABLE | — | Unix-specific kill/pgrep signal dispatch |
| CommandToSignal() | signal.go:145 | NOT_APPLICABLE | — | Unix syscall.Signal mapping |
| resolvePids() | signal.go:165 | NOT_APPLICABLE | — | Unix-specific pgrep |
| **golang/nats-server/server/service.go** | | | | |
| Run() | service.go:20 | PORTED | src/NATS.Server.Host/Program.cs:235 | StartAsync + WaitForShutdown |
| isWindowsService() | service.go:26 | NOT_APPLICABLE | — | Build-tag guarded; not relevant for .NET |
### Summary Counts
| Status | Count |
|--------|-------|
| PORTED | 123 |
| PARTIAL | 30 |
| MISSING | 55 |
| NOT_APPLICABLE | 14 |
| DEFERRED | 0 |
| **Total** | **222** |
---
## 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/ -maxdepth 1 -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/ -maxdepth 1 -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Gap inventory populated: 222 symbols analyzed (123 PORTED, 30 PARTIAL, 55 MISSING, 14 NOT_APPLICABLE) across server.go, client.go, opts.go, reload.go, signal.go, service.go, main.go | claude-opus |

410
gaps/events.md Normal file
View File

@@ -0,0 +1,410 @@
# Events — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Events** 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 Events
- System events publish on `$SYS.ACCOUNT.<id>.CONNECT`, `$SYS.ACCOUNT.<id>.DISCONNECT`, etc.
- Events include client info, server info, and reason codes.
- Message tracing allows tracking a specific message through local delivery, routes, gateways, and leaf nodes.
- The system account (`$SYS`) is used for internal server communication.
---
## Go Reference Files (Source)
- `golang/nats-server/server/events.go` — System event generation/publishing (~4,080 lines). Connect/disconnect events, auth events, account events. Publishes on `$SYS.` subjects.
- `golang/nats-server/server/msgtrace.go` — Message tracing (trace a message through the system)
## Go Reference Files (Tests)
- `golang/nats-server/server/events_test.go`
- `golang/nats-server/server/msgtrace_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Events/` (all files)
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Events/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### events.go — Exported Types / Structs
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `TypedEvent` | `golang/nats-server/server/events.go:468` | PORTED | `src/NATS.Server/Events/EventTypes.cs` | Fields inlined directly into each event class (ConnectEventMsg, DisconnectEventMsg, etc.) rather than embedded; same JSON shape |
| `ServerStatsMsg` | `golang/nats-server/server/events.go:150` | PORTED | `src/NATS.Server/Events/EventTypes.cs:383` | Full field parity |
| `ConnectEventMsg` | `golang/nats-server/server/events.go:155` | PORTED | `src/NATS.Server/Events/EventTypes.cs:191` | Full field parity including schema type constant |
| `DisconnectEventMsg` | `golang/nats-server/server/events.go:167` | PORTED | `src/NATS.Server/Events/EventTypes.cs:211` | Full field parity including schema type constant |
| `OCSPPeerRejectEventMsg` | `golang/nats-server/server/events.go:182` | PARTIAL | `src/NATS.Server/Events/EventTypes.cs:520` | Go has `Peer certidp.CertInfo` with Subject/Issuer/Fingerprint/Raw; .NET `OcspPeerRejectEventMsg` omits the `Peer` CertInfo sub-object entirely — only Kind+Reason present |
| `OCSPPeerChainlinkInvalidEventMsg` | `golang/nats-server/server/events.go:196` | MISSING | — | No .NET equivalent. Go struct has Link+Peer CertInfo objects. .NET has a different `OcspChainValidationEvent` that does not match the Go shape or advisory type string (`io.nats.server.advisory.v1.ocsp_peer_link_invalid`) |
| `AccountNumConns` | `golang/nats-server/server/events.go:210` | PORTED | `src/NATS.Server/Events/EventTypes.cs:245` | Full field parity including schema type constant |
| `AccountStat` | `golang/nats-server/server/events.go:217` | PORTED | `src/NATS.Server/Events/EventTypes.cs:245` | Fields embedded inline in `AccountNumConns` — matches Go embedding pattern |
| `ServerInfo` | `golang/nats-server/server/events.go:249` | PORTED | `src/NATS.Server/Events/EventTypes.cs:9` | `EventServerInfo` — all fields present; `Flags` typed as `ulong` (Go `ServerCapability uint64`) |
| `ServerID` | `golang/nats-server/server/events.go:239` | MISSING | — | Simple struct with Name/Host/ID used in `idzReq` response; no .NET equivalent |
| `ServerCapability` (type + consts) | `golang/nats-server/server/events.go:246` | MISSING | — | `JetStreamEnabled`, `BinaryStreamSnapshot`, `AccountNRG` capability flags; .NET has the `Flags ulong` field but no typed enum/const for capability bits |
| `ClientInfo` | `golang/nats-server/server/events.go:308` | PORTED | `src/NATS.Server/Events/EventTypes.cs:62` | `EventClientInfo` — all fields present; RTT stored as `long RttNanos` vs Go `time.Duration` |
| `ServerStats` | `golang/nats-server/server/events.go:364` | PORTED | `src/NATS.Server/Events/EventTypes.cs:395` | `ServerStatsData` — full field parity; .NET adds `InMsgs/OutMsgs/InBytes/OutBytes` compat fields not in Go (extra, not missing) |
| `RouteStat` | `golang/nats-server/server/events.go:390` | PORTED | `src/NATS.Server/Events/EventTypes.cs:303` | Full field parity |
| `GatewayStat` | `golang/nats-server/server/events.go:398` | PORTED | `src/NATS.Server/Events/EventTypes.cs:326` | Full field parity |
| `MsgBytes` | `golang/nats-server/server/events.go:407` | PORTED | `src/NATS.Server/Events/EventTypes.cs:181` | `MsgBytesStats` — same fields |
| `DataStats` | `golang/nats-server/server/events.go:412` | PORTED | `src/NATS.Server/Events/EventTypes.cs:156` | Full field parity |
| `EventFilterOptions` | `golang/nats-server/server/events.go:1946` | MISSING | — | No .NET equivalent; used for server-side request filtering by name/cluster/host/tags/domain |
| `StatszEventOptions` | `golang/nats-server/server/events.go:1956` | MISSING | — | No .NET equivalent |
| `AccInfoEventOptions` | `golang/nats-server/server/events.go:1962` | MISSING | — | No .NET equivalent |
| `ConnzEventOptions` | `golang/nats-server/server/events.go:1968` | MISSING | — | No .NET equivalent |
| `RoutezEventOptions` | `golang/nats-server/server/events.go:1974` | MISSING | — | No .NET equivalent |
| `SubszEventOptions` | `golang/nats-server/server/events.go:1980` | MISSING | — | No .NET equivalent |
| `VarzEventOptions` | `golang/nats-server/server/events.go:1986` | MISSING | — | No .NET equivalent |
| `GatewayzEventOptions` | `golang/nats-server/server/events.go:1992` | MISSING | — | No .NET equivalent |
| `LeafzEventOptions` | `golang/nats-server/server/events.go:1997` | MISSING | — | No .NET equivalent |
| `AccountzEventOptions` | `golang/nats-server/server/events.go:2004` | MISSING | — | No .NET equivalent |
| `AccountStatzEventOptions` | `golang/nats-server/server/events.go:2010` | MISSING | — | No .NET equivalent |
| `JszEventOptions` | `golang/nats-server/server/events.go:2016` | MISSING | — | No .NET equivalent |
| `HealthzEventOptions` | `golang/nats-server/server/events.go:2022` | MISSING | — | No .NET equivalent |
| `ProfilezEventOptions` | `golang/nats-server/server/events.go:2028` | MISSING | — | No .NET equivalent |
| `ExpvarzEventOptions` | `golang/nats-server/server/events.go:2034` | MISSING | — | No .NET equivalent |
| `IpqueueszEventOptions` | `golang/nats-server/server/events.go:2039` | MISSING | — | No .NET equivalent |
| `RaftzEventOptions` | `golang/nats-server/server/events.go:2045` | MISSING | — | No .NET equivalent |
| `ServerAPIResponse` | `golang/nats-server/server/events.go:2092` | MISSING | — | Generic request-reply envelope; no .NET equivalent |
| `ServerAPIConnzResponse` | `golang/nats-server/server/events.go:2106` | MISSING | — | Typed response wrappers for Z endpoints; no .NET equivalent |
| `ServerAPIRoutezResponse` | `golang/nats-server/server/events.go:2113` | MISSING | — | No .NET equivalent |
| `ServerAPIGatewayzResponse` | `golang/nats-server/server/events.go:2119` | MISSING | — | No .NET equivalent |
| `ServerAPIJszResponse` | `golang/nats-server/server/events.go:2126` | MISSING | — | No .NET equivalent |
| `ServerAPIHealthzResponse` | `golang/nats-server/server/events.go:2133` | MISSING | — | No .NET equivalent |
| `ServerAPIVarzResponse` | `golang/nats-server/server/events.go:2141` | MISSING | — | No .NET equivalent |
| `ServerAPISubszResponse` | `golang/nats-server/server/events.go:2148` | MISSING | — | No .NET equivalent |
| `ServerAPILeafzResponse` | `golang/nats-server/server/events.go:2155` | MISSING | — | No .NET equivalent |
| `ServerAPIAccountzResponse` | `golang/nats-server/server/events.go:2162` | MISSING | — | No .NET equivalent |
| `ServerAPIExpvarzResponse` | `golang/nats-server/server/events.go:2169` | MISSING | — | No .NET equivalent |
| `ServerAPIpqueueszResponse` | `golang/nats-server/server/events.go:2175` | MISSING | — | No .NET equivalent |
| `ServerAPIRaftzResponse` | `golang/nats-server/server/events.go:2183` | MISSING | — | No .NET equivalent |
| `KickClientReq` | `golang/nats-server/server/events.go:3180` | MISSING | — | No .NET equivalent |
| `LDMClientReq` | `golang/nats-server/server/events.go:3184` | MISSING | — | No .NET equivalent |
| `UserInfo` | `golang/nats-server/server/events.go:1500` | MISSING | — | No .NET equivalent |
| `SlowConsumersStats` | `golang/nats-server/server/events.go:377` | PORTED | `src/NATS.Server/Events/EventTypes.cs:344` | Full field parity |
| `StaleConnectionStats` | `golang/nats-server/server/events.go:379` | PORTED | `src/NATS.Server/Events/EventTypes.cs:363` | Full field parity |
### events.go — Unexported Types (Important Helpers)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `internal` struct | `golang/nats-server/server/events.go:124` | PORTED | `src/NATS.Server/Events/InternalEventSystem.cs:83` | `InternalEventSystem` class — same responsibilities: send/receive queues, sequence counter, SID counter, callback registry, server hash |
| `inSysMsg` struct | `golang/nats-server/server/events.go:112` | PORTED | `src/NATS.Server/Events/InternalEventSystem.cs:29` | `InternalSystemMessage` sealed class — same fields |
| `pubMsg` struct | `golang/nats-server/server/events.go:421` | PORTED | `src/NATS.Server/Events/InternalEventSystem.cs:15` | `PublishMessage` sealed class — same fields |
| `sysMsgHandler` type | `golang/nats-server/server/events.go:109` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:67` | `SystemMessageHandler` delegate — same signature shape |
| `serverUpdate` struct | `golang/nats-server/server/events.go:461` | MISSING | — | Tracks seq + ltime for remote server heartbeat ordering; no .NET equivalent |
| `accNumConnsReq` struct | `golang/nats-server/server/events.go:233` | PORTED | `src/NATS.Server/Events/EventTypes.cs:797` | `AccNumConnsReq` — full field parity |
| `accNumSubsReq` struct | `golang/nats-server/server/events.go:2966` | MISSING | — | Used for debug subscriber count requests; no .NET equivalent |
| `compressionType` + consts | `golang/nats-server/server/events.go:2082` | PARTIAL | `src/NATS.Server/Events/EventCompressor.cs` | `EventCompressor` handles snappy compression; no gzip compression path; `compressionType` enum itself not present as typed enum — only Snappy supported |
| `msgHandler` type | `golang/nats-server/server/events.go:2751` | NOT_APPLICABLE | — | Go internal callback type merging header+body bytes; .NET uses the `SystemMessageHandler` delegate with separate header/body params |
### events.go — Exported Methods on ServerInfo
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `(*ServerInfo).SetJetStreamEnabled()` | `golang/nats-server/server/events.go:274` | MISSING | — | No .NET method; `EventServerInfo` has the raw `Flags` field but no typed capability methods |
| `(*ServerInfo).JetStreamEnabled()` bool | `golang/nats-server/server/events.go:281` | MISSING | — | No .NET equivalent |
| `(*ServerInfo).SetBinaryStreamSnapshot()` | `golang/nats-server/server/events.go:287` | MISSING | — | No .NET equivalent |
| `(*ServerInfo).BinaryStreamSnapshot()` bool | `golang/nats-server/server/events.go:292` | MISSING | — | No .NET equivalent |
| `(*ServerInfo).SetAccountNRG()` | `golang/nats-server/server/events.go:297` | MISSING | — | No .NET equivalent |
| `(*ServerInfo).AccountNRG()` bool | `golang/nats-server/server/events.go:302` | MISSING | — | No .NET equivalent |
### events.go — Unexported Methods on ClientInfo
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `(*ClientInfo).forAssignmentSnap()` | `golang/nats-server/server/events.go:334` | NOT_APPLICABLE | — | Internal helper for RAFT assignment snapshots; no JetStream clustering yet |
| `(*ClientInfo).forProposal()` | `golang/nats-server/server/events.go:343` | NOT_APPLICABLE | — | Internal helper for RAFT proposals; no JetStream clustering yet |
| `(*ClientInfo).forAdvisory()` | `golang/nats-server/server/events.go:353` | MISSING | — | Strips JWT/Alternates for JS advisory events; no .NET equivalent |
### events.go — Server Methods (Core Event System)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `(*Server).EventsEnabled()` | `golang/nats-server/server/events.go:807` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:125` | `InternalEventSystem.Start()` activates the system; no public boolean property equivalent on NatsServer |
| `(*Server).TrackedRemoteServers()` | `golang/nats-server/server/events.go:821` | MISSING | — | No .NET equivalent |
| `(*Server).Node()` | `golang/nats-server/server/events.go:1157` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:103` | `ServerHash` property computes SHA-256(8-char) server hash; not exposed as `Node()` on server |
| `(*Server).internalSendLoop()` | `golang/nats-server/server/events.go:495` | PORTED | `src/NATS.Server/Events/InternalEventSystem.cs:363` | `InternalSendLoopAsync` — Channel-based; handles JSON serialization and SubList delivery |
| `(*Server).internalReceiveLoop()` | `golang/nats-server/server/events.go:476` | PORTED | `src/NATS.Server/Events/InternalEventSystem.cs:431` | `InternalReceiveLoopAsync` — Channel-based dispatch |
| `(*Server).sendLDMShutdownEventLocked()` | `golang/nats-server/server/events.go:674` | MISSING | — | No .NET equivalent |
| `(*Server).sendShutdownEvent()` | `golang/nats-server/server/events.go:684` | MISSING | — | No .NET equivalent; `LameDuckEventMsg` / `ShutdownEventMsg` types exist but publishing method does not |
| `(*Server).sendInternalAccountMsg()` | `golang/nats-server/server/events.go:703` | MISSING | — | No .NET equivalent |
| `(*Server).sendInternalAccountMsgWithReply()` | `golang/nats-server/server/events.go:708` | MISSING | — | No .NET equivalent |
| `(*Server).sendInternalAccountSysMsg()` | `golang/nats-server/server/events.go:732` | MISSING | — | No .NET equivalent |
| `(*Server).sendInternalMsgLocked()` | `golang/nats-server/server/events.go:750` | MISSING | — | No .NET equivalent |
| `(*Server).sendInternalMsg()` | `golang/nats-server/server/events.go:758` | MISSING | — | No .NET equivalent |
| `(*Server).sendInternalResponse()` | `golang/nats-server/server/events.go:766` | MISSING | — | No .NET equivalent |
| `(*Server).initEventTracking()` | `golang/nats-server/server/events.go:1172` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:150` | `InitEventTracking` registers VARZ/HEALTHZ/SUBSZ/STATSZ/IDZ; missing: account-level services (CONNZ, LEAFZ, JSZ, STATZ, CONNS, INFO), latency tracking, reload/kick/LDM services, NSUBS, debug subscribers |
| `(*Server).sendStatsz()` | `golang/nats-server/server/events.go:891` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:188` | `PublishServerStats()` — publishes stats; missing: interest check before sending, gzip/snappy compression on response, JetStream stats section |
| `(*Server).limitStatsz()` | `golang/nats-server/server/events.go:1068` | MISSING | — | Rate-limit for statsz broadcasts; no .NET equivalent |
| `(*Server).heartbeatStatsz()` | `golang/nats-server/server/events.go:1098` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:134` | Periodic timer in `Start()` fires every 10s; Go ramps from 250ms to statsz interval |
| `(*Server).resetLastStatsz()` | `golang/nats-server/server/events.go:1115` | MISSING | — | No .NET equivalent |
| `(*Server).sendStatszUpdate()` | `golang/nats-server/server/events.go:1119` | PORTED | `src/NATS.Server/Events/InternalEventSystem.cs:188` | Effectively `PublishServerStats()` |
| `(*Server).startStatszTimer()` | `golang/nats-server/server/events.go:1124` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:134` | Timer started in `Start()`; Go does exponential backoff ramp-up; .NET is fixed 10s interval |
| `(*Server).startRemoteServerSweepTimer()` | `golang/nats-server/server/events.go:1133` | MISSING | — | Orphan remote server sweep; no .NET equivalent |
| `(*Server).checkRemoteServers()` | `golang/nats-server/server/events.go:832` | MISSING | — | No .NET equivalent |
| `(*Server).updateServerUsage()` | `golang/nats-server/server/events.go:848` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:193` | CPU via `Process.GetCurrentProcess()`; Go uses `pse.ProcUsage()` for actual CPU%; .NET does not populate CPU |
| `(*Server).sendAccConnsUpdate()` | `golang/nats-server/server/events.go:2407` | MISSING | — | No .NET equivalent |
| `(*Server).accConnsUpdate()` | `golang/nats-server/server/events.go:2506` | MISSING | — | No .NET equivalent |
| `(*Server).nextEventID()` | `golang/nats-server/server/events.go:2516` | PARTIAL | `src/NATS.Server/Events/EventTypes.cs:943` | `EventBuilder.GenerateEventId()` uses `Guid.NewGuid()`; Go uses nuid (faster nano-ID generator) |
| `(*Server).accountConnectEvent()` | `golang/nats-server/server/events.go:2522` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:303` | `SendConnectEvent()` exists; missing: JWT/IssuerKey/Tags/NameTag/Kind/ClientType/MQTTClientID fields in ConnectEventDetail |
| `(*Server).accountDisconnectEvent()` | `golang/nats-server/server/events.go:2569` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:329` | `SendDisconnectEvent()` exists; missing: RTT/JWT/IssuerKey/Tags/NameTag/Kind/ClientType/MQTTClientID fields in DisconnectEventDetail |
| `(*Server).sendAuthErrorEvent()` | `golang/nats-server/server/events.go:2631` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:277` | `SendAuthErrorEvent()` exists; Go uses `DisconnectEventMsg` shape for auth errors (surprising but correct); .NET uses `AuthErrorEventMsg` with different schema type |
| `(*Server).sendAccountAuthErrorEvent()` | `golang/nats-server/server/events.go:2690` | MISSING | — | Account-level auth error event to account subject; no .NET equivalent |
| `(*Server).sendOCSPPeerRejectEvent()` | `golang/nats-server/server/events.go:3267` | PARTIAL | `src/NATS.Server/Events/EventTypes.cs:612` | `OcspEventBuilder.BuildPeerReject()` helper exists; no publishing method on server; missing `Peer` CertInfo payload |
| `(*Server).sendOCSPPeerChainlinkInvalidEvent()` | `golang/nats-server/server/events.go:3300` | MISSING | — | No .NET equivalent at all; `OcspChainValidationEvent` is a different/new type with non-matching Go advisory shape |
| `(*Server).filterRequest()` | `golang/nats-server/server/events.go:2052` | MISSING | — | Server-side filter by name/host/cluster/tags/domain for Z-endpoint requests; no .NET equivalent |
| `(*Server).zReq()` | `golang/nats-server/server/events.go:2252` | MISSING | — | Generic Z request handler (parse options, filter, call respf, encode response); no .NET equivalent |
| `(*Server).statszReq()` | `golang/nats-server/server/events.go:2190` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:159` | `HandleStatszRequest` registered but implementation thin — no filter options parsing |
| `(*Server).idzReq()` | `golang/nats-server/server/events.go:2217` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:156` | `HandleIdzRequest` registered; no implementation visible in analyzed files |
| `(*Server).remoteConnsUpdate()` | `golang/nats-server/server/events.go:2283` | MISSING | — | No .NET equivalent |
| `(*Server).remoteServerUpdate()` | `golang/nats-server/server/events.go:1704` | MISSING | — | No .NET equivalent |
| `(*Server).remoteServerShutdown()` | `golang/nats-server/server/events.go:1665` | MISSING | — | No .NET equivalent |
| `(*Server).updateRemoteServer()` | `golang/nats-server/server/events.go:1763` | MISSING | — | No .NET equivalent |
| `(*Server).processRemoteServerShutdown()` | `golang/nats-server/server/events.go:1642` | MISSING | — | No .NET equivalent |
| `(*Server).processNewServer()` | `golang/nats-server/server/events.go:1781` | MISSING | — | No .NET equivalent |
| `(*Server).updateNRGAccountStatus()` | `golang/nats-server/server/events.go:1816` | NOT_APPLICABLE | — | JetStream NRG (RAFT) account status; not yet ported |
| `(*Server).ensureGWsInterestOnlyForLeafNodes()` | `golang/nats-server/server/events.go:1838` | NOT_APPLICABLE | — | Gateway + leaf node coordination; not yet ported |
| `(*Server).shutdownEventing()` | `golang/nats-server/server/events.go:1852` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:453` | `DisposeAsync()` cancels loops and drains queues; missing: clearing accounts' eventing state, sending shutdown event before draining |
| `(*Server).connsRequest()` | `golang/nats-server/server/events.go:1886` | MISSING | — | No .NET equivalent |
| `(*Server).leafNodeConnected()` | `golang/nats-server/server/events.go:1925` | NOT_APPLICABLE | — | Leaf node gateway interest mode; not yet ported |
| `(*Server).addSystemAccountExports()` | `golang/nats-server/server/events.go:1556` | NOT_APPLICABLE | — | Multi-tenant account exports; not yet ported |
| `(*Server).registerSystemImports()` | `golang/nats-server/server/events.go:2331` | NOT_APPLICABLE | — | Multi-tenant account imports; not yet ported |
| `(*Server).registerSystemImportsForExisting()` | `golang/nats-server/server/events.go:1532` | NOT_APPLICABLE | — | Multi-tenant; not yet ported |
| `(*Server).enableAccountTracking()` | `golang/nats-server/server/events.go:2366` | MISSING | — | No .NET equivalent |
| `(*Server).sendLeafNodeConnect()` | `golang/nats-server/server/events.go:2383` | NOT_APPLICABLE | — | Leaf node; not yet ported |
| `(*Server).sendLeafNodeConnectMsg()` | `golang/nats-server/server/events.go:2398` | NOT_APPLICABLE | — | Leaf node; not yet ported |
| `(*Server).accountClaimUpdate()` | `golang/nats-server/server/events.go:1608` | NOT_APPLICABLE | — | JWT account claim update; not yet ported |
| `(*Server).userInfoReq()` | `golang/nats-server/server/events.go:1508` | MISSING | — | No .NET equivalent |
| `(*Server).debugSubscribers()` | `golang/nats-server/server/events.go:3001` | MISSING | — | No .NET equivalent |
| `(*Server).nsubsRequest()` | `golang/nats-server/server/events.go:3129` | MISSING | — | No .NET equivalent |
| `(*Server).reloadConfig()` | `golang/nats-server/server/events.go:3168` | MISSING | — | No .NET equivalent |
| `(*Server).kickClient()` | `golang/nats-server/server/events.go:3188` | MISSING | — | No .NET equivalent |
| `(*Server).ldmClient()` | `golang/nats-server/server/events.go:3206` | MISSING | — | No .NET equivalent |
| `(*Server).sysSubscribe()` | `golang/nats-server/server/events.go:2796` | PORTED | `src/NATS.Server/Events/InternalEventSystem.cs:231` | `SysSubscribe()` — same purpose |
| `(*Server).sysSubscribeQ()` | `golang/nats-server/server/events.go:2801` | MISSING | — | Queue-group variant; no .NET equivalent |
| `(*Server).sysSubscribeInternal()` | `golang/nats-server/server/events.go:2806` | MISSING | — | Internal-only sub (no interest propagation); no .NET equivalent |
| `(*Server).systemSubscribe()` | `golang/nats-server/server/events.go:2810` | PARTIAL | `src/NATS.Server/Events/InternalEventSystem.cs:231` | `SysSubscribe()` combines subscribe + callback registration; Go version supports queue groups and internal-only flag |
| `(*Server).sysUnsubscribe()` | `golang/nats-server/server/events.go:2842` | MISSING | — | No .NET equivalent |
| `(*Server).inboxReply()` | `golang/nats-server/server/events.go:2928` | MISSING | — | WC inbox reply dispatcher; no .NET equivalent |
| `(*Server).newRespInbox()` | `golang/nats-server/server/events.go:2953` | MISSING | — | No .NET equivalent |
| `(*Server).remoteLatencyUpdate()` | `golang/nats-server/server/events.go:2870` | NOT_APPLICABLE | — | Service latency tracking; not yet ported |
| `(*Server).noInlineCallback()` | `golang/nats-server/server/events.go:2760` | NOT_APPLICABLE | — | Routes callback through recvq to avoid blocking route/GW threads; .NET Channel already provides this decoupling |
| `(*Server).noInlineCallbackStatsz()` | `golang/nats-server/server/events.go:2766` | NOT_APPLICABLE | — | Same — statsz queue variant |
| `(*Server).wrapChk()` | `golang/nats-server/server/events.go:3253` | NOT_APPLICABLE | — | Lock + eventsEnabled guard wrapper; idiom not needed in async .NET code |
| `(*Server).sameDomain()` | `golang/nats-server/server/events.go:1660` | MISSING | — | No .NET equivalent |
### events.go — Standalone / Package-Level Functions
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `getHash()` | `golang/nats-server/server/events.go:1141` | PORTED | `src/NATS.Server/Events/InternalEventSystem.cs:118` | SHA-256 8-char hash; same algorithm |
| `getHashSize()` | `golang/nats-server/server/events.go:1146` | MISSING | — | Parameterized size variant; only 8-char version ported |
| `routeStat()` | `golang/nats-server/server/events.go:859` | NOT_APPLICABLE | — | Route stat collection; clustering not yet ported |
| `newPubMsg()` | `golang/nats-server/server/events.go:435` | NOT_APPLICABLE | — | Pool-based pubMsg factory; .NET uses `PublishMessage` record without pooling |
| `(*pubMsg).returnToPool()` | `golang/nats-server/server/events.go:452` | NOT_APPLICABLE | — | Pool return; .NET has no pool |
| `accForClient()` | `golang/nats-server/server/events.go:3224` | MISSING | — | No .NET equivalent |
| `issuerForClient()` | `golang/nats-server/server/events.go:3232` | MISSING | — | No .NET equivalent |
| `clearTimer()` | `golang/nats-server/server/events.go:3244` | NOT_APPLICABLE | — | Go timer management idiom; .NET uses `PeriodicTimer` / `CancellationToken` |
| `totalSubs()` | `golang/nats-server/server/events.go:2973` | MISSING | — | No .NET equivalent |
| `remoteLatencySubjectForResponse()` | `golang/nats-server/server/events.go:2860` | NOT_APPLICABLE | — | Latency tracking; not yet ported |
| `getAcceptEncoding()` | `golang/nats-server/server/events.go:2238` | MISSING | — | Parses Accept-Encoding header for compression type; no .NET equivalent |
| `(*Account).statz()` | `golang/nats-server/server/events.go:2446` | NOT_APPLICABLE | — | Account stats snapshot; Account class not yet in Events module |
### events.go — Constants / Subjects
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `connectEventSubj` | `golang/nats-server/server/events.go:49` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:13` | `ConnectEvent` — matching subject pattern |
| `disconnectEventSubj` | `golang/nats-server/server/events.go:50` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:14` | `DisconnectEvent` — matching |
| `accConnsEventSubjNew` | `golang/nats-server/server/events.go:58` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:15` | `AccountConnsNew` — matching |
| `accConnsEventSubjOld` | `golang/nats-server/server/events.go:59` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:16` | `AccountConnsOld` — matching |
| `serverStatsSubj` | `golang/nats-server/server/events.go:66` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:19` | `ServerStats` — matching |
| `shutdownEventSubj` | `golang/nats-server/server/events.go:61` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:20` | `ServerShutdown` — matching |
| `lameDuckEventSubj` | `golang/nats-server/server/events.go:60` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:21` | `ServerLameDuck` — matching |
| `authErrorEventSubj` | `golang/nats-server/server/events.go:64` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:22` | `AuthError` — matching |
| `authErrorAccountEventSubj` | `golang/nats-server/server/events.go:65` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:23` | `AuthErrorAccount` — matching |
| `serverDirectReqSubj` | `golang/nats-server/server/events.go:67` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:32` | `ServerReq` — matching |
| `serverPingReqSubj` | `golang/nats-server/server/events.go:68` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:35` | `ServerPing` — matching |
| `accDirectReqSubj` | `golang/nats-server/server/events.go:51` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:38` | `AccountReq` — matching |
| `inboxRespSubj` | `golang/nats-server/server/events.go:73` | PORTED | `src/NATS.Server/Events/EventSubjects.cs:41` | `InboxResponse` — matching |
| `ocspPeerRejectEventSubj` | `golang/nats-server/server/events.go:95` | PARTIAL | `src/NATS.Server/Events/EventSubjects.cs:45` | Go: `$SYS.SERVER.%s.OCSP.PEER.CONN.REJECT`; .NET: `$SYS.SERVER.{0}.OCSP.PEER.REJECT` — different path segment |
| `ocspPeerChainlinkInvalidEventSubj` | `golang/nats-server/server/events.go:96` | MISSING | — | Go: `$SYS.SERVER.%s.OCSP.PEER.LINK.INVALID`; .NET `OcspChainValidation` uses `$SYS.SERVER.{0}.OCSP.CHAIN.VALIDATION` — different subject |
| `leafNodeConnectEventSubj` | `golang/nats-server/server/events.go:71` | PARTIAL | `src/NATS.Server/Events/EventSubjects.cs:28` | .NET subject `$SYS.SERVER.{0}.LEAFNODE.CONNECT` differs from Go `$SYS.ACCOUNT.%s.LEAFNODE.CONNECT` |
| `remoteLatencyEventSubj` | `golang/nats-server/server/events.go:72` | MISSING | — | No .NET equivalent |
| `userDirectInfoSubj` | `golang/nats-server/server/events.go:76` | MISSING | — | No .NET equivalent |
| `userDirectReqSubj` | `golang/nats-server/server/events.go:77` | MISSING | — | No .NET equivalent |
| `accNumSubsReqSubj` | `golang/nats-server/server/events.go:81` | MISSING | — | No .NET equivalent |
| `accSubsSubj` | `golang/nats-server/server/events.go:84` | MISSING | — | No .NET equivalent |
| `clientKickReqSubj` | `golang/nats-server/server/events.go:62` | MISSING | — | No .NET equivalent |
| `clientLDMReqSubj` | `golang/nats-server/server/events.go:63` | MISSING | — | No .NET equivalent |
| `serverStatsPingReqSubj` | `golang/nats-server/server/events.go:69` | MISSING | — | No .NET equivalent |
| `serverReloadReqSubj` | `golang/nats-server/server/events.go:70` | MISSING | — | No .NET equivalent |
| `accPingReqSubj` | `golang/nats-server/server/events.go:52` | MISSING | — | No .NET equivalent |
| `connsRespSubj` | `golang/nats-server/server/events.go:57` | MISSING | — | No .NET equivalent |
| `accLookupReqSubj` / JWT subjects | `golang/nats-server/server/events.go:43` | NOT_APPLICABLE | — | JWT operator resolver subjects; not yet ported |
| `InboxPrefix` | `golang/nats-server/server/events.go:2946` | MISSING | — | No .NET equivalent |
| `acceptEncodingHeader` / `contentEncodingHeader` | `golang/nats-server/server/events.go:2232` | MISSING | — | Header name constants for compression negotiation; no .NET equivalent |
| `ConnectEventMsgType` | `golang/nats-server/server/events.go:163` | PORTED | `src/NATS.Server/Events/EventTypes.cs:193` | `ConnectEventMsg.EventType` constant — matching value |
| `DisconnectEventMsgType` | `golang/nats-server/server/events.go:177` | PORTED | `src/NATS.Server/Events/EventTypes.cs:214` | `DisconnectEventMsg.EventType` — matching value |
| `OCSPPeerRejectEventMsgType` | `golang/nats-server/server/events.go:191` | PORTED | `src/NATS.Server/Events/EventTypes.cs:522` | `OcspPeerRejectEventMsg.EventType` — matching value |
| `OCSPPeerChainlinkInvalidEventMsgType` | `golang/nats-server/server/events.go:205` | MISSING | — | No .NET equivalent with matching type string `io.nats.server.advisory.v1.ocsp_peer_link_invalid` |
| `AccountNumConnsMsgType` | `golang/nats-server/server/events.go:229` | PORTED | `src/NATS.Server/Events/EventTypes.cs:247` | `AccountNumConns.EventType` — matching value |
---
### msgtrace.go — Exported Types
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `MsgTraceType` | `golang/nats-server/server/msgtrace.go:50` | MISSING | — | No .NET equivalent |
| `MsgTraceEvent` | `golang/nats-server/server/msgtrace.go:63` | MISSING | — | Top-level trace event container; no .NET equivalent |
| `MsgTraceRequest` | `golang/nats-server/server/msgtrace.go:70` | MISSING | — | No .NET equivalent |
| `MsgTraceEvents` | `golang/nats-server/server/msgtrace.go:76` | MISSING | — | Slice type with custom UnmarshalJSON; no .NET equivalent |
| `MsgTrace` | `golang/nats-server/server/msgtrace.go:78` | MISSING | — | Interface; no .NET equivalent |
| `MsgTraceBase` | `golang/nats-server/server/msgtrace.go:83` | MISSING | — | No .NET equivalent |
| `MsgTraceIngress` | `golang/nats-server/server/msgtrace.go:88` | MISSING | — | No .NET equivalent |
| `MsgTraceSubjectMapping` | `golang/nats-server/server/msgtrace.go:98` | MISSING | — | No .NET equivalent |
| `MsgTraceStreamExport` | `golang/nats-server/server/msgtrace.go:103` | MISSING | — | No .NET equivalent |
| `MsgTraceServiceImport` | `golang/nats-server/server/msgtrace.go:109` | MISSING | — | No .NET equivalent |
| `MsgTraceJetStream` | `golang/nats-server/server/msgtrace.go:116` | MISSING | — | No .NET equivalent |
| `MsgTraceEgress` | `golang/nats-server/server/msgtrace.go:124` | MISSING | — | No .NET equivalent |
### msgtrace.go — Exported Constants
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `MsgTraceDest` | `golang/nats-server/server/msgtrace.go:28` | MISSING | — | Header name constant; no .NET equivalent |
| `MsgTraceDestDisabled` | `golang/nats-server/server/msgtrace.go:29` | MISSING | — | No .NET equivalent |
| `MsgTraceHop` | `golang/nats-server/server/msgtrace.go:30` | MISSING | — | No .NET equivalent |
| `MsgTraceOriginAccount` | `golang/nats-server/server/msgtrace.go:31` | MISSING | — | No .NET equivalent |
| `MsgTraceOnly` | `golang/nats-server/server/msgtrace.go:32` | MISSING | — | No .NET equivalent |
| `MsgTraceIngressType` | `golang/nats-server/server/msgtrace.go:55` | MISSING | — | No .NET equivalent |
| `MsgTraceSubjectMappingType` | `golang/nats-server/server/msgtrace.go:56` | MISSING | — | No .NET equivalent |
| `MsgTraceStreamExportType` | `golang/nats-server/server/msgtrace.go:57` | MISSING | — | No .NET equivalent |
| `MsgTraceServiceImportType` | `golang/nats-server/server/msgtrace.go:58` | MISSING | — | No .NET equivalent |
| `MsgTraceJetStreamType` | `golang/nats-server/server/msgtrace.go:59` | MISSING | — | No .NET equivalent |
| `MsgTraceEgressType` | `golang/nats-server/server/msgtrace.go:60` | MISSING | — | No .NET equivalent |
### msgtrace.go — Exported Methods on MsgTraceEvent
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `(*MsgTraceEvent).Ingress()` | `golang/nats-server/server/msgtrace.go:193` | MISSING | — | No .NET equivalent |
| `(*MsgTraceEvent).SubjectMapping()` | `golang/nats-server/server/msgtrace.go:200` | MISSING | — | No .NET equivalent |
| `(*MsgTraceEvent).StreamExports()` | `golang/nats-server/server/msgtrace.go:209` | MISSING | — | No .NET equivalent |
| `(*MsgTraceEvent).ServiceImports()` | `golang/nats-server/server/msgtrace.go:219` | MISSING | — | No .NET equivalent |
| `(*MsgTraceEvent).JetStream()` | `golang/nats-server/server/msgtrace.go:229` | MISSING | — | No .NET equivalent |
| `(*MsgTraceEvent).Egresses()` | `golang/nats-server/server/msgtrace.go:238` | MISSING | — | No .NET equivalent |
| `(*MsgTraceEvents).UnmarshalJSON()` | `golang/nats-server/server/msgtrace.go:160` | MISSING | — | Polymorphic JSON deserialization of trace event list; no .NET equivalent |
### msgtrace.go — Unexported Core Functions (>20 lines)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `(*client).initMsgTrace()` | `golang/nats-server/server/msgtrace.go:332` | MISSING | — | Trace header parsing and msgTrace struct initialization; no .NET equivalent |
| `(*client).isMsgTraceEnabled()` | `golang/nats-server/server/msgtrace.go:285` | MISSING | — | No .NET equivalent |
| `(*client).msgTraceSupport()` | `golang/nats-server/server/msgtrace.go:295` | MISSING | — | No .NET equivalent |
| `(*client).initAndSendIngressErrEvent()` | `golang/nats-server/server/msgtrace.go:596` | MISSING | — | No .NET equivalent |
| `(*msgTrace).traceOnly()` | `golang/nats-server/server/msgtrace.go:626` | MISSING | — | No .NET equivalent |
| `(*msgTrace).setOriginAccountHeaderIfNeeded()` | `golang/nats-server/server/msgtrace.go:630` | MISSING | — | No .NET equivalent |
| `(*msgTrace).setHopHeader()` | `golang/nats-server/server/msgtrace.go:646` | MISSING | — | No .NET equivalent |
| `(*msgTrace).setIngressError()` | `golang/nats-server/server/msgtrace.go:657` | MISSING | — | No .NET equivalent |
| `(*msgTrace).addSubjectMappingEvent()` | `golang/nats-server/server/msgtrace.go:663` | MISSING | — | No .NET equivalent |
| `(*msgTrace).addEgressEvent()` | `golang/nats-server/server/msgtrace.go:676` | MISSING | — | No .NET equivalent |
| `(*msgTrace).addStreamExportEvent()` | `golang/nats-server/server/msgtrace.go:713` | MISSING | — | No .NET equivalent |
| `(*msgTrace).addServiceImportEvent()` | `golang/nats-server/server/msgtrace.go:730` | MISSING | — | No .NET equivalent |
| `(*msgTrace).addJetStreamEvent()` | `golang/nats-server/server/msgtrace.go:745` | MISSING | — | No .NET equivalent |
| `(*msgTrace).updateJetStreamEvent()` | `golang/nats-server/server/msgtrace.go:759` | MISSING | — | No .NET equivalent |
| `(*msgTrace).sendEventFromJetStream()` | `golang/nats-server/server/msgtrace.go:774` | MISSING | — | No .NET equivalent |
| `(*msgTrace).sendEvent()` | `golang/nats-server/server/msgtrace.go:788` | MISSING | — | No .NET equivalent |
| `genHeaderMapIfTraceHeadersPresent()` | `golang/nats-server/server/msgtrace.go:509` | MISSING | — | Parses raw NATS header bytes for trace headers; no .NET equivalent |
| `getConnName()` | `golang/nats-server/server/msgtrace.go:300` | MISSING | — | No .NET equivalent |
| `getCompressionType()` | `golang/nats-server/server/msgtrace.go:318` | MISSING | — | No .NET equivalent |
| `sample()` | `golang/nats-server/server/msgtrace.go:494` | MISSING | — | Probabilistic sampling for external trace; no .NET equivalent |
---
## 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/Events/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Events/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated from Go source analysis | auto |

251
gaps/gateways.md Normal file
View File

@@ -0,0 +1,251 @@
# Gateways — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Gateways** 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 Gateways
- Gateways connect separate NATS clusters. They start in "optimistic" mode (forward everything) then switch to "interest-only" mode.
- Reply subject mapping: `_GR_.<cluster>.<hash>.<reply>` prevents routing loops.
- Gateway connections use `ClientKind = GATEWAY`.
---
## Go Reference Files (Source)
- `golang/nats-server/server/gateway.go` — Inter-cluster bridges (~3,400 lines). Interest-only mode optimizes traffic. Reply subject mapping (`_GR_.` prefix) avoids cross-cluster conflicts.
## Go Reference Files (Tests)
- `golang/nats-server/server/gateway_test.go`
- `golang/nats-server/test/gateway_test.go` (integration)
## .NET Implementation Files (Source)
- `src/NATS.Server/Gateways/` (all files)
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Gateways/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### `golang/nats-server/server/gateway.go`
#### Constants, Variables, and Enums
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `SetGatewaysSolicitDelay` | gateway.go:76 | NOT_APPLICABLE | — | Test-only helper; sets atomic delay before soliciting gateways. Go-specific test hook. |
| `ResetGatewaysSolicitDelay` | gateway.go:83 | NOT_APPLICABLE | — | Test-only helper; resets atomic delay. Go-specific test hook. |
| `GatewayDoNotForceInterestOnlyMode` | gateway.go:130 | NOT_APPLICABLE | — | Test-only global flag; disables forced interest-only mode in tests. Go-specific test hook. |
| `GatewayInterestMode` (enum: Optimistic/Transitioning/InterestOnly) | gateway.go:94111 | PORTED | `src/NATS.Server/Gateways/GatewayInterestTracker.cs:16` | `GatewayInterestMode` enum with identical three values. |
| `GatewayInterestMode.String()` | gateway.go:113 | NOT_APPLICABLE | — | Go stringer pattern; C# uses `ToString()` automatically. |
| `gwReplyPrefix` / `gwReplyPrefixLen` / `gwHashLen` / offset constants | gateway.go:4958 | PARTIAL | `src/NATS.Server/Gateways/ReplyMapper.cs:12` | `GatewayReplyPrefix = "_GR_."` is present. Hash length and byte-offset arithmetic constants are missing; .NET uses string-segment parsing instead of fixed-width offsets. |
| `oldGWReplyPrefix` / `oldGWReplyPrefixLen` / `oldGWReplyStart` | gateway.go:4346 | MISSING | — | Old `$GR.` reply prefix for backward-compat with pre-v2.9 servers is not represented in .NET. `ReplyMapper.TryRestoreGatewayReply` handles it partially via numeric-hash detection but has no dedicated constant. |
| `gatewayTLSInsecureWarning` | gateway.go:71 | MISSING | — | TLS insecure warning string; no TLS gateway support yet. |
#### Structs / Types
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `srvGateway` struct | gateway.go:134 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:66` | `GatewayManager` covers outbound/inbound maps, accept loop, discovery, registration, and stats. Missing: RTT-ordered outbound list (`outo`), `totalQSubs` atomic counter, `pasi` per-account subscription interest map, `rsubs` recent-subscription sync.Map, `sIDHash`/`routesIDByHash` for reply routing, `sqbsz`/`recSubExp`, and `oldHash`/`oldReplyPfx` for backward compat. |
| `sitally` struct | gateway.go:189 | MISSING | — | Subject-interest tally (ref count + queue flag) used in `pasi.m`. No equivalent in .NET. |
| `gatewayCfg` struct | gateway.go:194 | PARTIAL | `src/NATS.Server/Configuration/GatewayOptions.cs:27` (`RemoteGatewayOptions`) | `RemoteGatewayOptions` covers name and URLs. Missing: `hash`/`oldHash` byte arrays, `implicit` flag, `connAttempts` counter, `tlsName`, `varzUpdateURLs`, URL management methods (`addURLs`, `updateURLs`, `getURLs`, `saveTLSHostname`), and per-remote TLS config. |
| `gateway` struct (per-client) | gateway.go:207 | MISSING | — | Per-connection gateway state (outbound flag, `outsim` sync.Map, `insim` map, `connectURL`, `useOldPrefix`, `interestOnlyMode`, `remoteName`). Absorbed into `GatewayConnection` but without full per-message interest tracking. |
| `outsie` struct | gateway.go:229 | PARTIAL | `src/NATS.Server/Gateways/GatewayInterestTracker.cs` | `GatewayInterestTracker.AccountState` covers mode and no-interest set. Missing: per-account `sl *Sublist` for queue-sub tracking in InterestOnly mode, `qsubs` counter. |
| `insie` struct | gateway.go:256 | MISSING | — | Inbound per-account no-interest set with mode tracking. No distinct type in .NET; `GatewayInterestTracker` handles outbound-side equivalent but not the inbound RS-/RS+ tracking map. |
| `gwReplyMap` struct | gateway.go:261 | PARTIAL | `src/NATS.Server/Gateways/ReplyMapper.cs:289` (`CacheEntry`) | `ReplyMapCache.CacheEntry` provides key/value/TTL. Missing: Go stores `ms string` (routed subject) and `exp int64` (Unix nano expiry); .NET uses `DateTime` but does not store the destination server/cluster hash separately. |
| `gwReplyMapping` struct | gateway.go:266 | PARTIAL | `src/NATS.Server/Gateways/ReplyMapper.cs:181` (`ReplyMapCache`) | `ReplyMapCache` provides LRU+TTL. Missing: atomic `check int32` field for fast-path bypass; Go mapping is per-client or per-account with explicit locker, .NET is a single shared cache. |
| `GatewayConnectionState` enum | gateway.go (n/a) | PORTED | `src/NATS.Server/Gateways/GatewayManager.cs:14` | .NET addition: lifecycle states (Connecting/Connected/Disconnected/Draining). No direct Go equivalent but covers the connection state machine. |
| `GatewayRegistration` | gateway.go (n/a) | PORTED | `src/NATS.Server/Gateways/GatewayManager.cs:26` | .NET-specific registration record with stats counters. |
| `GatewayReconnectPolicy` | gateway.go:689 | PORTED | `src/NATS.Server/Gateways/GatewayManager.cs:43` | Exponential backoff with jitter matches Go's reconnect delay logic. |
| `GatewayInfo` | gateway.go (Info struct) | PARTIAL | `src/NATS.Server/Gateways/GatewayInfo.cs:7` | Covers name and URLs for gossip discovery. Go's `Info` struct has many more gateway-related fields (`GatewayCmd`, `GatewayCmdPayload`, `GatewayNRP`, `GatewayIOM`, `GatewayURLs`, etc.) that are not in this type. |
#### Functions and Methods — `srvGateway` receiver
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `srvGateway.updateRemotesTLSConfig` | gateway.go:426 | MISSING | — | Config-reload TLS update for remote gateway connections. TLS not implemented in .NET. |
| `srvGateway.rejectUnknown` | gateway.go:476 | PARTIAL | `src/NATS.Server/Configuration/GatewayOptions.cs:11` | `GatewayOptions.RejectUnknown` property exists but is not enforced in the accept-loop or handshake logic. |
| `srvGateway.generateInfoJSON` | gateway.go:649 | MISSING | — | Generates the gateway `INFO` protocol JSON. No INFO protocol generation in .NET. |
| `srvGateway.getClusterHash` | gateway.go:2892 | MISSING | — | Returns 6-byte cluster hash for reply routing. No cluster hash computed at runtime in .NET. |
| `srvGateway.shouldMapReplyForGatewaySend` | gateway.go:2507 | MISSING | — | Checks recent subscriptions (`rsubs`) to decide whether to map a reply subject. `rsubs` not ported. |
| `srvGateway.orderOutboundConnectionsLocked` | gateway.go:1764 | MISSING | — | Sorts outbound connections by lowest RTT. RTT-based ordering not implemented. |
| `srvGateway.orderOutboundConnections` | gateway.go:1771 | MISSING | — | Locked wrapper for `orderOutboundConnectionsLocked`. |
#### Functions and Methods — `Server` receiver
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `validateGatewayOptions` | gateway.go:306 | MISSING | — | Validates gateway config (name, port, remote URLs). No validation equivalent in .NET. |
| `getGWHash` (standalone) | gateway.go:335 | MISSING | — | Computes 6-char hash for gateway name. Used for reply prefix construction. Not ported. |
| `getOldHash` (standalone) | gateway.go:339 | MISSING | — | SHA-256-based 4-char hash for old `$GR.` prefix. Not ported. |
| `Server.newGateway` | gateway.go:350 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:90` | `GatewayManager` constructor covers basic setup. Missing: hash computation, reply prefix assembly, `oldReplyPfx`, `pasi` init, resolver config. |
| `Server.startGateways` | gateway.go:487 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:161` (`StartAsync`) | `StartAsync` starts accept loop and connects to remotes. Missing: solicit delay, cluster-formation wait. |
| `Server.startGatewayAcceptLoop` | gateway.go:511 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:326` (`AcceptLoopAsync`) | Accept loop exists. Missing: TLS config, `authRequired`, reject-unknown check, advertising, `GatewayIOM` flag, INFO protocol send. |
| `Server.setGatewayInfoHostPort` | gateway.go:595 | MISSING | — | Configures gateway listener host/port with advertise override and non-local IP resolution. |
| `Server.solicitGateways` | gateway.go:668 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:173` (`StartAsync` remote loop) | Connects to configured remotes. Missing: implicit vs explicit distinction, goroutine-per-remote with proper lifecycle. |
| `Server.reconnectGateway` | gateway.go:689 | PORTED | `src/NATS.Server/Gateways/GatewayManager.cs:149` (`ReconnectGatewayAsync`) | Exponential backoff reconnect delay with jitter. |
| `Server.solicitGateway` | gateway.go:706 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:358` (`ConnectWithRetryAsync`) | Retry loop with multiple URL support. Missing: random URL selection, `shouldReportConnectErr` throttling, implicit gateway retry limits, DNS resolution via `resolver`, `ConnectBackoff` flag. |
| `srvGateway.hasInbound` | gateway.go:790 | MISSING | — | Checks if an inbound connection for a named gateway exists. Not implemented. |
| `Server.createGateway` | gateway.go:805 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:344` (`HandleInboundAsync`) and `ConnectWithRetryAsync` | Creates client connection for inbound/outbound. Missing: full client lifecycle, TLS handshake, CONNECT/INFO protocol, `expectConnect` flag, ping timer setup, temp-client registration. |
| `client.sendGatewayConnect` | gateway.go:958 | PARTIAL | `src/NATS.Server/Gateways/GatewayConnection.cs:113` (`PerformOutboundHandshakeAsync`) | Handshake sends server ID. Go sends full CONNECT JSON with auth, TLS flag, gateway name. .NET sends a simplified `GATEWAY {serverId}` line. |
| `client.processGatewayConnect` | gateway.go:993 | MISSING | — | Parses CONNECT from inbound gateway; validates gateway field, rejects wrong port/unknown gateways. No protocol parsing in .NET. |
| `client.processGatewayInfo` | gateway.go:1045 | MISSING | — | Handles INFO protocol for both outbound (first connect + gossip) and inbound (register, switch to interest-only, send queue subs). Core of gateway handshake. Not ported. |
| `Server.gossipGatewaysToInboundGateway` | gateway.go:1253 | MISSING | — | Sends INFO gossip for all known gateways to a new inbound connection for full-mesh formation. |
| `Server.forwardNewGatewayToLocalCluster` | gateway.go:1279 | MISSING | — | Floods cluster routes with new gateway INFO to ensure all nodes connect. |
| `Server.sendQueueSubsToGateway` | gateway.go:1311 | PARTIAL | `src/NATS.Server/Gateways/GatewayConnection.cs:72` (`AddQueueSubscription`) | Registers queue subs locally. Missing: actual RS+ wire protocol emission to the remote peer. |
| `Server.sendAccountSubsToGateway` | gateway.go:1319 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:210` (`SendAccountSubscriptions`) | Sends subject list to named gateway. Missing: wire protocol emission (`RS+` with account prefix) and mode switch to InterestOnly. |
| `gwBuildSubProto` (standalone) | gateway.go:1323 | MISSING | — | Builds `RS+`/`RS-` binary protocol buffer for a given account/subject map. |
| `Server.sendSubsToGateway` | gateway.go:1342 | MISSING | — | Full subscription send loop (queue subs on connect, or all subs for an account on mode switch). |
| `Server.processGatewayInfoFromRoute` | gateway.go:1394 | MISSING | — | Handles gateway gossip INFO received via a cluster route connection. |
| `Server.sendGatewayConfigsToRoute` | gateway.go:1406 | MISSING | — | Sends known outbound gateway configs to a new route connection. |
| `Server.processImplicitGateway` | gateway.go:1453 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:119` (`ProcessImplicitGateway`) | Records discovered gateway name. Missing: URL augmentation of existing config, creation of `gatewayCfg`, launching `solicitGateway` goroutine for the new implicit remote. |
| `Server.NumOutboundGateways` | gateway.go:1501 | MISSING | — | Public test-facing count of outbound connections. No exact equivalent. |
| `Server.numOutboundGateways` | gateway.go:1506 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:284` (`GetConnectedGatewayCount`) | Returns connected count from registrations. Does not distinguish inbound vs outbound. |
| `Server.numInboundGateways` | gateway.go:1514 | MISSING | — | Count of inbound gateway connections. No equivalent. |
| `Server.getRemoteGateway` | gateway.go:1522 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:264` (`GetRegistration`) | Returns registration by name. Missing: returns `gatewayCfg` in Go (with TLS, URLs, hash); .NET returns `GatewayRegistration` (only state/stats). |
| `gatewayCfg.bumpConnAttempts` | gateway.go:1530 | MISSING | — | Test helper to increment connection attempts counter. |
| `gatewayCfg.getConnAttempts` | gateway.go:1537 | MISSING | — | Test helper to read connection attempts. |
| `gatewayCfg.resetConnAttempts` | gateway.go:1545 | MISSING | — | Test helper to reset connection attempts. |
| `gatewayCfg.isImplicit` | gateway.go:1552 | MISSING | — | Returns whether a gateway config was discovered (implicit) vs configured (explicit). |
| `gatewayCfg.getURLs` | gateway.go:1561 | MISSING | — | Returns randomly shuffled URL slice for connection attempts. |
| `gatewayCfg.getURLsAsStrings` | gateway.go:1576 | MISSING | — | Returns URL host strings for gossip INFO. |
| `gatewayCfg.updateURLs` | gateway.go:1588 | MISSING | — | Rebuilds URL map from config + INFO-discovered URLs. |
| `gatewayCfg.saveTLSHostname` | gateway.go:1612 | MISSING | — | Saves TLS ServerName from a URL hostname. TLS not implemented. |
| `gatewayCfg.addURLs` | gateway.go:1621 | MISSING | — | Adds newly discovered URLs into the URL map. |
| `Server.addGatewayURL` | gateway.go:1648 | MISSING | — | Adds a URL to the server's gateway URL set and regenerates INFO JSON. |
| `Server.removeGatewayURL` | gateway.go:1661 | MISSING | — | Removes a URL from the gateway URL set and regenerates INFO JSON. |
| `Server.sendAsyncGatewayInfo` | gateway.go:1676 | MISSING | — | Sends updated INFO to all inbound gateway connections (e.g., after URL change). |
| `Server.getGatewayURL` | gateway.go:1688 | MISSING | — | Returns this server's gateway listen URL string. |
| `Server.getGatewayName` | gateway.go:1697 | MISSING | — | Returns this server's gateway cluster name. |
| `Server.getAllGatewayConnections` | gateway.go:1703 | MISSING | — | Collects all inbound + outbound gateway clients into a map. |
| `Server.registerInboundGatewayConnection` | gateway.go:1720 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:392` (`Register`) | `Register` adds connection to dictionary. Missing: separate inbound map keyed by CID. |
| `Server.registerOutboundGatewayConnection` | gateway.go:1728 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:392` (`Register`) | `Register` handles registration. Missing: duplicate-prevention logic (return false if name already exists), RTT-ordered `outo` list. |
| `Server.getOutboundGatewayConnection` | gateway.go:1743 | MISSING | — | Returns outbound connection by name. No named lookup of outbound connections. |
| `Server.getOutboundGatewayConnections` | gateway.go:1752 | MISSING | — | Returns all outbound connections in RTT order. |
| `Server.getInboundGatewayConnections` | gateway.go:1778 | MISSING | — | Returns all inbound connections. |
| `Server.removeRemoteGatewayConnection` | gateway.go:1788 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:416` (`WatchConnectionAsync`) | Removes connection and decrements stats. Missing: outbound-specific cleanup (delete from `outo`/`out`, remove qsub tracking from `totalQSubs`), inbound-specific cleanup (remove `_R_` subscriptions). |
| `Server.GatewayAddr` | gateway.go:1862 | MISSING | — | Returns `*net.TCPAddr` for the gateway listener. No equivalent (only `ListenEndpoint` string). |
| `client.processGatewayAccountUnsub` | gateway.go:1875 | PARTIAL | `src/NATS.Server/Gateways/GatewayInterestTracker.cs:86` (`TrackNoInterest`) | Tracks no-interest at account level. Missing: handling of queue subs (reset `ni` map but keep entry if `qsubs > 0`), Go's nil-vs-entry distinction in `outsim`. |
| `client.processGatewayAccountSub` | gateway.go:1904 | PARTIAL | `src/NATS.Server/Gateways/GatewayInterestTracker.cs:61` (`TrackInterest`) | Clears no-interest in optimistic mode. Missing: queue-sub check (don't delete entry if `qsubs > 0`). |
| `client.processGatewayRUnsub` | gateway.go:1934 | MISSING | — | Parses RS- protocol; for optimistic mode stores in ni map, for InterestOnly/queue removes from sublist. Full RS- processing not ported. |
| `client.processGatewayRSub` | gateway.go:2029 | MISSING | — | Parses RS+ protocol; registers interest in sublist for queue subs and InterestOnly mode. Full RS+ processing not ported. |
| `client.gatewayInterest` | gateway.go:2165 | PARTIAL | `src/NATS.Server/Gateways/GatewayInterestTracker.cs:113` (`ShouldForward`) | `ShouldForward` handles Optimistic/Transitioning/InterestOnly modes. Missing: separate queue-sub result (`*SublistResult`) return, `interestOnlyMode` flag override, `emptyResult` guard. |
| `Server.switchAccountToInterestMode` | gateway.go:2211 | PORTED | `src/NATS.Server/Gateways/GatewayInterestTracker.cs:144` (`SwitchToInterestOnly`) | Switches account to InterestOnly. Go iterates all inbound connections; .NET operates per-tracker instance. |
| `Server.maybeSendSubOrUnsubToGateways` | gateway.go:2237 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:193` (`PropagateLocalSubscription/Unsubscription`) | Propagates sub/unsub to inbound gateways. Missing: per-mode decision (optimistic ni-map check vs InterestOnly always-send), wildcard cleanup of ni-map, A+ send when clearing A-, actual wire protocol. |
| `Server.sendQueueSubOrUnsubToGateways` | gateway.go:2335 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:199` (`PropagateLocalUnsubscription`) | Propagates queue sub changes. Missing: wire RS+/RS- protocol, A- clearing logic. |
| `Server.gatewayUpdateSubInterest` | gateway.go:2391 | MISSING | — | Ref-counted `pasi` map update + recent-sub tracking + triggers send to gateways. Core subscription-interest accounting not ported. |
| `isGWRoutedReply` (standalone) | gateway.go:2484 | PORTED | `src/NATS.Server/Gateways/ReplyMapper.cs:17` (`HasGatewayReplyPrefix`) | Detects `_GR_.` prefix with length guard. |
| `isGWRoutedSubjectAndIsOldPrefix` (standalone) | gateway.go:2490 | PARTIAL | `src/NATS.Server/Gateways/ReplyMapper.cs:17` | `HasGatewayReplyPrefix` checks new prefix only. Old `$GR.` prefix detection missing. |
| `hasGWRoutedReplyPrefix` (standalone) | gateway.go:2502 | PORTED | `src/NATS.Server/Gateways/ReplyMapper.cs:17` (`HasGatewayReplyPrefix`) | Equivalent prefix check. |
| `srvGateway.shouldMapReplyForGatewaySend` | gateway.go:2507 | MISSING | — | Checks `rsubs` sync.Map to decide if a reply subject needs gateway mapping. No `rsubs` equivalent. |
| `client.sendMsgToGateways` | gateway.go:2540 | PARTIAL | `src/NATS.Server/Gateways/GatewayManager.cs:181` (`ForwardMessageAsync`) | Iterates connections and sends. Missing: direct-send path for `_GR_` subjects (hash routing), queue group filtering, reply subject mapping, header stripping for non-header peers, message tracing, per-account stats, RTT-ordered iteration. |
| `Server.gatewayHandleAccountNoInterest` | gateway.go:2787 | PARTIAL | Partially in `GatewayInterestTracker.TrackNoInterest` | Sends A- under `pasi` lock. Missing: pasi lock coordination, actual A- wire protocol emission from inbound side. |
| `client.sendAccountUnsubToGateway` | gateway.go:2802 | PARTIAL | `src/NATS.Server/Gateways/GatewayConnection.cs:142` (`SendAMinusAsync`) | Sends A- wire protocol. Missing: idempotency guard (don't send if already sent, tracked via nil entry in `insim`). |
| `Server.gatewayHandleSubjectNoInterest` | gateway.go:2830 | MISSING | — | On missing subject interest: sends RS- under pasi lock, counts RS-, triggers mode switch at threshold. Core interest-only negotiation not ported. |
| `Server.storeRouteByHash` | gateway.go:2901 | MISSING | — | Stores route client keyed by server-ID hash for gateway reply routing. |
| `Server.removeRouteByHash` | gateway.go:2909 | MISSING | — | Removes route entry by hash. |
| `Server.getRouteByHash` | gateway.go:2918 | MISSING | — | Looks up route by hash for per-account or pool routing. Used in `handleGatewayReply`. |
| `getSubjectFromGWRoutedReply` (standalone) | gateway.go:2948 | PORTED | `src/NATS.Server/Gateways/ReplyMapper.cs:74` (`TryRestoreGatewayReply`) | Extracts original subject from routed reply; handles both old and new prefix. |
| `client.handleGatewayReply` | gateway.go:2963 | MISSING | — | On inbound message: detects `_GR_` prefix, decodes cluster/server hash, routes to origin route or delivers locally. Core gateway reply routing not ported. |
| `client.processInboundGatewayMsg` | gateway.go:3107 | MISSING | — | Main inbound message processor: handles GW reply, account lookup, no-interest signaling, message delivery. Not ported. |
| `client.gatewayAllSubsReceiveStart` | gateway.go:3183 | PARTIAL | `src/NATS.Server/Gateways/GatewayInterestTracker.cs:86` (mode=Transitioning path) | Go sets mode to Transitioning; .NET sets Transitioning mode when threshold crossed. Missing: command-protocol parsing, explicit start triggered by INFO command. |
| `client.gatewayAllSubsReceiveComplete` | gateway.go:3216 | PARTIAL | `src/NATS.Server/Gateways/GatewayInterestTracker.cs:144` (`SwitchToInterestOnly`) | Final switch to InterestOnly. Go clears `ni` map and sets mode; .NET does equivalent. Missing: triggered by INFO command with `gatewayCmdAllSubsComplete`. |
| `getAccountFromGatewayCommand` (standalone) | gateway.go:3240 | MISSING | — | Extracts account from `GatewayCmdPayload` in INFO protocol. Not ported. |
| `client.gatewaySwitchAccountToSendAllSubs` | gateway.go:3260 | PARTIAL | `src/NATS.Server/Gateways/GatewayInterestTracker.cs:144` (`SwitchToInterestOnly`) | Switches mode and triggers async all-subs send. Missing: INFO command emission (`gatewayCmdAllSubsStart`/`gatewayCmdAllSubsComplete`), async `sendAccountSubsToGateway` goroutine. |
| `Server.trackGWReply` | gateway.go:3324 | PARTIAL | `src/NATS.Server/Gateways/ReplyMapper.cs:231` (`ReplyMapCache.Set`) | Caches reply mapping with TTL. Missing: per-client vs per-account duality, `gwrm.m` sync.Map for background cleanup, `check int32` atomic flag, `gwrm.ch` channel to trigger expiry timer. |
| `Server.startGWReplyMapExpiration` | gateway.go:3371 | PARTIAL | `src/NATS.Server/Gateways/ReplyMapper.cs:267` (`ReplyMapCache.PurgeExpired`) | Purge is manual on-demand. Go runs a dedicated goroutine with timer reset on new entries via channel. No background expiry goroutine in .NET. |
| `gwReplyMapping.get` | gateway.go:280 | PORTED | `src/NATS.Server/Gateways/ReplyMapper.cs:202` (`ReplyMapCache.TryGet`) | LRU get with TTL check. |
| `RemoteGatewayOpts.clone` | gateway.go:290 | MISSING | — | Deep-copies a `RemoteGatewayOpts` including TLS config clone. No clone method on `RemoteGatewayOptions`. |
#### Additional .NET-Only Types (No Go Equivalent)
| .NET Symbol | .NET File:Line | Notes |
|-------------|:---------------|-------|
| `GatewayCommandType` enum | `src/NATS.Server/Gateways/GatewayCommands.cs:9` | .NET-specific wire command enum; Go uses string constants and byte dispatch. |
| `GatewayCommands` static class | `src/NATS.Server/Gateways/GatewayCommands.cs:31` | Wire-protocol constants and formatters. Partially maps to Go's `rSubBytes`/`rUnsubBytes`/`aSubBytes`/`aUnsubBytes`/`InfoProto` but uses a simplified format. |
| `GatewayMessage` record | `src/NATS.Server/Gateways/GatewayConnection.cs:346` | DTO for received messages; no direct Go equivalent. |
| `ReplyMapCache` | `src/NATS.Server/Gateways/ReplyMapper.cs:181` | LRU+TTL cache for reply mappings. |
---
## 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/Gateways/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Gateways/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory completed: analyzed all ~3,427 lines of gateway.go; classified 80+ Go symbols against 6 .NET source files. Final counts: 9 PORTED, 35 PARTIAL, 37 MISSING, 5 NOT_APPLICABLE. | claude-sonnet-4-6 |

341
gaps/instructions.md Normal file
View File

@@ -0,0 +1,341 @@
# Gap Analysis — Orchestrator Instructions
> Instructions for an LLM to launch parallel subagents that analyze and maintain the gap inventory across all 19 categories.
## Overview
The `gaps/` directory tracks what has been ported from the Go NATS server to the .NET port. Each category has its own `<category>.md` file with:
- Go reference file paths
- .NET implementation file paths
- A Gap Inventory table (populated or empty)
- Instructions for how to analyze the gap
The orchestrator's job is to launch one subagent per category, collect results, and update `stillmissing.md` with fresh LOC numbers.
---
## Mode Detection
Before launching subagents, determine the mode:
```
IF the Gap Inventory table in a category file has only the empty template row → INITIAL mode
IF the Gap Inventory table has populated rows → UPDATE mode
```
You can check all files at once:
```bash
for f in gaps/*.md; do
[ "$f" = "gaps/stillmissing.md" ] && continue
[ "$f" = "gaps/instructions.md" ] && continue
rows=$(grep -c '^|[^|]*|[^|]*|' "$f" 2>/dev/null)
name=$(basename "$f" .md)
if [ "$rows" -le 3 ]; then
echo "INITIAL: $name"
else
echo "UPDATE: $name"
fi
done
```
---
## Category Registry
Each category file and its complexity tier (determines model selection and context budget):
| Category File | Tier | Model | Go Source LOC | Notes |
|---------------|------|-------|-------------:|-------|
| `core-server.md` | LARGE | opus | 21,223 | client.go alone is ~6,700 lines |
| `protocol.md` | SMALL | sonnet | 1,829 | 3 files, well-scoped |
| `subscriptions.md` | SMALL | sonnet | 2,416 | 2 files, well-scoped |
| `auth-and-accounts.md` | LARGE | opus | 7,260 | accounts.go is ~4,100 lines |
| `configuration.md` | SMALL | sonnet | 1,871 | 3 files |
| `routes.md` | MEDIUM | sonnet | 3,314 | 1 file |
| `gateways.md` | MEDIUM | sonnet | 3,426 | 1 file |
| `leaf-nodes.md` | MEDIUM | sonnet | 3,470 | 1 file |
| `jetstream.md` | X-LARGE | opus | 55,228 | Must be split into sub-passes |
| `raft.md` | MEDIUM | opus | 5,037 | 1 file, complex consensus logic |
| `mqtt.md` | MEDIUM | opus | 5,882 | 1 file |
| `websocket.md` | SMALL | sonnet | 1,550 | 1 file, near-complete (99%) |
| `monitoring.md` | MEDIUM | sonnet | 4,396 | 2 files |
| `events.md` | MEDIUM | sonnet | 4,133 | 2 files |
| `tls-security.md` | MEDIUM | opus | 4,207 | Multiple files + subdirs |
| `internal-ds.md` | MEDIUM | sonnet | 4,020 | Already at 105% parity |
| `logging.md` | TRIVIAL | haiku | 936 | Likely all NOT_APPLICABLE |
| `utilities-and-other.md` | MEDIUM | sonnet | 3,282 | Mixed bag of small files |
| `misc-uncategorized.md` | SMALL | haiku | 660 | Remainder items |
---
## Launching Subagents
### Parallelism Strategy
Launch all subagents in a **single message** using multiple `Task` tool calls. All 19 categories are independent — they read different Go files and write to different output files.
**Exception: JetStream (X-LARGE)**. JetStream has 55,228 Go source LOC across 15 files. A single subagent cannot read all files in one context window. Launch it as a **foreground** task and handle it specially (see JetStream section below).
### Subagent Tool Configuration
For each category, use the `Task` tool with:
```
subagent_type: "general-purpose"
model: <from Category Registry table>
description: "Analyze <category> gaps"
```
### Prompt Templates
#### INITIAL Mode Prompt
Use this prompt when the Gap Inventory table is empty:
```
You are analyzing the Go-to-.NET porting gap for the {CATEGORY_NAME} module of the NATS server.
## Your Task
Read the category file at `gaps/{CATEGORY_FILE}` — it contains:
- Detailed analysis instructions (Steps 1-5)
- Go reference file paths
- .NET implementation file paths
- Key porting notes specific to this category
Follow the instructions in that file exactly. Specifically:
1. Read each Go source file listed under "Go Reference Files (Source)"
2. For each file, extract all exported types, methods, and standalone functions
3. Read the .NET files listed under ".NET Implementation Files (Source)"
4. For each Go symbol, search for its .NET equivalent
5. Classify each item as PORTED, PARTIAL, MISSING, NOT_APPLICABLE, or DEFERRED
6. Write the results into the Gap Inventory table in `gaps/{CATEGORY_FILE}`
## Output Format
Edit `gaps/{CATEGORY_FILE}` to populate the Gap Inventory table. Each row must have:
- **Go Symbol**: The type or function name (e.g., `Server.Start`, `processMsg`, `RouteInfo`)
- **Go File:Line**: Path relative to repo root with line number (e.g., `golang/nats-server/server/route.go:142`)
- **Status**: One of PORTED, PARTIAL, MISSING, NOT_APPLICABLE, DEFERRED
- **NET Equivalent**: Path to .NET file:line if it exists (e.g., `src/NATS.Server/Routes/RouteManager.cs:55`)
- **Notes**: Brief explanation, especially for PARTIAL (what's missing) and NOT_APPLICABLE (why)
Group rows by Go source file. Use markdown sub-headers within the table comments if helpful.
## Important Rules
- Do NOT modify any source code (.go or .cs files) — this is a read-only analysis
- Do NOT modify `gaps/stillmissing.md` — the orchestrator handles LOC updates
- DO update the Change Log at the bottom of the category file
- Focus on **exported/public API surface** first, then important unexported helpers
- For large files (>2,000 lines), work through them methodically — top to bottom, section by section
- Skip trivial getters/setters unless they contain logic
```
#### UPDATE Mode Prompt
Use this prompt when the Gap Inventory table already has data:
```
You are updating the gap analysis for the {CATEGORY_NAME} module of the NATS server.
## Your Task
The file `gaps/{CATEGORY_FILE}` already has a populated Gap Inventory table from a previous analysis. Your job is to:
1. Read the current Gap Inventory in `gaps/{CATEGORY_FILE}`
2. For each item marked MISSING or PARTIAL:
- Search the .NET codebase to check if it has been ported since the last analysis
- If now ported, update the status to PORTED and fill in the .NET Equivalent path
- If partially ported, update notes with current state
3. Check for NEW Go symbols that were added since the last analysis:
- Read the Go source files listed in the category file
- Compare against the existing inventory
- Add rows for any new symbols not yet tracked
4. Check for NEW .NET files that may cover previously MISSING items:
- Glob for new .cs files in the .NET directories listed in the category file
- Cross-reference against MISSING items
5. Update the Change Log at the bottom
## Output Format
Edit `gaps/{CATEGORY_FILE}` in place:
- Update existing rows (change status, add .NET paths)
- Add new rows for newly discovered symbols
- Do NOT remove existing rows — even if the Go symbol was removed, mark it as NOT_APPLICABLE with a note
- Update the Change Log with today's date and a summary
## After Updating the Inventory
Run the LOC re-count commands from the "Keeping This File Updated" section of the category file and report the new numbers in your output so the orchestrator can update stillmissing.md.
## Important Rules
- Do NOT modify any source code — read-only analysis
- Do NOT modify `gaps/stillmissing.md` — report numbers, the orchestrator updates it
- Preserve all existing inventory rows
```
---
## JetStream Special Handling
JetStream is too large for a single subagent pass. Split it into 5 sub-passes, launched sequentially or with careful scoping:
| Sub-pass | Go Files | Focus |
|----------|----------|-------|
| JS-1: Core | `jetstream.go`, `jetstream_api.go`, `jetstream_events.go`, `jetstream_errors.go`, `jetstream_versioning.go`, `jetstream_batching.go` | Orchestration, API handlers |
| JS-2: Stream | `stream.go`, `store.go` | Stream lifecycle, retention policies |
| JS-3: Consumer | `consumer.go` | Consumer state machine, delivery modes |
| JS-4: Storage | `filestore.go`, `memstore.go`, `dirstore.go`, `disk_avail.go` | Persistent storage, compression, encryption |
| JS-5: Cluster | `jetstream_cluster.go` | Clustered JetStream coordination |
Launch JS-1 through JS-5 as separate subagents (can run in parallel). Each writes to `gaps/jetstream.md` — but since they write different sections of the Gap Inventory table, use **section headers** within the table to avoid conflicts:
```
<!-- === JS-1: Core === -->
| JetStream.enableJetStream | jetstream.go:245 | PORTED | ... | ... |
...
<!-- === JS-2: Stream === -->
| stream.processInboundJetStreamMsg | stream.go:112 | MISSING | | ... |
...
```
Alternatively, launch them sequentially (JS-1 first, then resume the agent for JS-2, etc.) to avoid write conflicts.
---
## After All Subagents Complete
### Step 1: Collect LOC Numbers
Each subagent in UPDATE mode should report its re-counted LOC. If running INITIAL mode, the orchestrator runs LOC counts itself:
```bash
cd /Users/dohertj2/Desktop/natsdotnet
echo "=== .NET Source LOC by Category ==="
echo "core-server|$(find src/NATS.Server/ -maxdepth 1 -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "protocol|$(find src/NATS.Server/Protocol/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "subscriptions|$(find src/NATS.Server/Subscriptions/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "auth-and-accounts|$(find src/NATS.Server/Auth/ src/NATS.Server/Imports/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "configuration|$(find src/NATS.Server/Configuration/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "routes|$(find src/NATS.Server/Routes/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "gateways|$(find src/NATS.Server/Gateways/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "leaf-nodes|$(find src/NATS.Server/LeafNodes/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "jetstream|$(find src/NATS.Server/JetStream/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "raft|$(find src/NATS.Server/Raft/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "mqtt|$(find src/NATS.Server/Mqtt/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "websocket|$(find src/NATS.Server/WebSocket/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "monitoring|$(find src/NATS.Server/Monitoring/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "events|$(find src/NATS.Server/Events/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "tls-security|$(find src/NATS.Server/Tls/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "internal-ds|$(find src/NATS.Server/Internal/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo "logging|0"
echo "utilities-and-other|$(find src/NATS.Server/IO/ src/NATS.Server/Server/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l)"
echo ""
echo "=== .NET Source TOTAL ==="
find src/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l
echo ""
echo "=== .NET Test TOTAL ==="
find tests/ -name '*.cs' -type f -exec cat {} + 2>/dev/null | wc -l
echo ""
echo "=== .NET File Counts ==="
echo "source|$(find src/ -name '*.cs' -type f | wc -l)"
echo "test|$(find tests/ -name '*.cs' -type f | wc -l)"
```
### Step 2: Update stillmissing.md
Compare the new LOC numbers against the values in `gaps/stillmissing.md`. For each category where the .NET LOC changed:
1. Edit the row in the "Source Code by Functional Area" table
2. Recalculate the `.NET/Go` percentage: `round(.NET LOC / Go LOC * 100)`
3. Update the `.NET Files` count if new files were added
4. Update the Summary table totals
5. Update the "Generated" date in the header
**Do NOT change Go LOC numbers** unless the Go reference has been updated (rare).
### Step 3: Generate Summary Report
After all subagents complete, produce a summary for the user:
```
## Gap Analysis Summary — {DATE}
### Status Counts (across all categories)
- PORTED: {n}
- PARTIAL: {n}
- MISSING: {n}
- NOT_APPLICABLE: {n}
- DEFERRED: {n}
### Categories with Most MISSING Items
1. {category}: {n} missing
2. {category}: {n} missing
3. {category}: {n} missing
### LOC Changes Since Last Run
| Category | Previous .NET LOC | Current .NET LOC | Delta |
|----------|------------------:|-----------------:|------:|
| ... | ... | ... | ... |
### Categories Needing Attention
- {category}: {reason}
```
Count statuses by grepping the inventory tables:
```bash
for f in gaps/*.md; do
[ "$f" = "gaps/stillmissing.md" ] && continue
[ "$f" = "gaps/instructions.md" ] && continue
name=$(basename "$f" .md)
ported=$(grep -c '| PORTED |' "$f" 2>/dev/null || echo 0)
partial=$(grep -c '| PARTIAL |' "$f" 2>/dev/null || echo 0)
missing=$(grep -c '| MISSING |' "$f" 2>/dev/null || echo 0)
na=$(grep -c '| NOT_APPLICABLE |' "$f" 2>/dev/null || echo 0)
deferred=$(grep -c '| DEFERRED |' "$f" 2>/dev/null || echo 0)
echo "$name|$ported|$partial|$missing|$na|$deferred"
done
```
---
## Quick-Start: Full Initial Run
Copy-paste orchestration sequence:
1. **Check mode** — run the mode detection script above
2. **Launch all subagents** — use the Task tool 19 times in a single message (see parallelism strategy)
3. **Handle JetStream** — if INITIAL mode, launch 5 JS sub-passes
4. **Wait for completion** — all subagents run in background; you're notified on completion
5. **Collect LOC** — run the LOC counting script
6. **Update stillmissing.md** — edit the table with new numbers
7. **Generate summary** — run the status counting script and format the report
## Quick-Start: Incremental Update
After porting work has been done:
1. **Identify changed categories** — check git diff for which `src/` directories were modified:
```bash
git diff --name-only HEAD~10 -- src/ | sed 's|src/NATS.Server/||' | cut -d/ -f1 | sort -u
```
2. **Launch subagents only for changed categories** — no need to re-analyze unchanged modules
3. **Collect LOC and update stillmissing.md** — same as full run but only for changed categories
---
## Error Handling
- **Subagent hits context limit**: This typically happens with LARGE/X-LARGE categories. If a subagent reports it couldn't finish, resume it with the `resume` parameter and instruct it to continue from where it left off.
- **Subagent finds no .NET files**: The category may not be ported yet. Mark all items as MISSING and note the empty state.
- **Conflicting writes to jetstream.md**: If running JS sub-passes in parallel, use the section-header approach described above. If conflicts occur, run sub-passes sequentially instead.
- **Go files changed upstream**: If the Go reference was updated (`golang/nats-server/`), run a full INITIAL analysis to catch new symbols. Check with: `git log --oneline -5 -- golang/`

402
gaps/internal-ds.md Normal file
View File

@@ -0,0 +1,402 @@
# Internal Data Structures — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Internal Data Structures** 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 Internal Data Structures
- This category is at **105% LOC parity** — above Go.
- The **subject tree** (stree) uses adaptive node sizing (4→10→16→48→256 children) for memory efficiency.
- The **AVL tree** is used for tracking acknowledged sequences in JetStream consumers.
- The **time hash wheel** provides O(1) TTL expiration checks.
- **PSE** (platform-specific extensions) queries process RSS/CPU — may use `System.Diagnostics.Process` in .NET.
- **sysmem** queries total system memory — may use `GC.GetGCMemoryInfo()` or P/Invoke in .NET.
- Many Go files in pse/ and sysmem/ are platform-specific build-tagged files — classify platform-irrelevant ones as NOT_APPLICABLE.
---
## Go Reference Files (Source)
- `golang/nats-server/server/avl/seqset.go` — AVL tree for sparse sequence sets (JetStream ack tracking)
- `golang/nats-server/server/stree/` — Subject tree with adaptive nodes (node4, node10, node16, node48, node256) for per-subject state in streams. 11 files.
- `golang/nats-server/server/thw/thw.go` — Time hash wheel for efficient TTL expiration
- `golang/nats-server/server/gsl/gsl.go` — Generic subject list, optimized trie variant
- `golang/nats-server/server/pse/` — Platform-specific extensions (proc info: RSS, CPU). 12 files across platforms.
- `golang/nats-server/server/sysmem/` — System memory queries. 8 files across platforms.
## Go Reference Files (Tests)
- `golang/nats-server/server/avl/seqset_test.go`
- `golang/nats-server/server/stree/stree_test.go`
- `golang/nats-server/server/thw/thw_test.go`
- `golang/nats-server/server/gsl/gsl_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Internal/` (all files including subdirectories)
- `src/NATS.Server/Internal/Avl/`
- `src/NATS.Server/Internal/Gsl/`
- `src/NATS.Server/Internal/SubjectTree/`
- `src/NATS.Server/Internal/TimeHashWheel/`
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Internal/` (all subdirectories)
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### avl/seqset.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `SequenceSet` (struct) | `golang/nats-server/server/avl/seqset.go:33` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:26` | Full class with all fields |
| `SequenceSet.Insert` | `golang/nats-server/server/avl/seqset.go:44` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:52` | |
| `SequenceSet.Exists` | `golang/nats-server/server/avl/seqset.go:52` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:63` | |
| `SequenceSet.SetInitialMin` | `golang/nats-server/server/avl/seqset.go:69` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:89` | Go returns error; .NET throws |
| `SequenceSet.Delete` | `golang/nats-server/server/avl/seqset.go:80` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:103` | |
| `SequenceSet.Size` | `golang/nats-server/server/avl/seqset.go:97` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:43` | Property in .NET |
| `SequenceSet.Nodes` | `golang/nats-server/server/avl/seqset.go:102` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:46` | Property in .NET |
| `SequenceSet.Empty` | `golang/nats-server/server/avl/seqset.go:107` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:127` | |
| `SequenceSet.IsEmpty` | `golang/nats-server/server/avl/seqset.go:114` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:49` | Property in .NET |
| `SequenceSet.Range` | `golang/nats-server/server/avl/seqset.go:124` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:138` | |
| `SequenceSet.Heights` | `golang/nats-server/server/avl/seqset.go:129` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:141` | Returns tuple in .NET |
| `SequenceSet.State` | `golang/nats-server/server/avl/seqset.go:143` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:153` | Returns named tuple |
| `SequenceSet.MinMax` | `golang/nats-server/server/avl/seqset.go:152` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:166` | |
| `clone` (unexported) | `golang/nats-server/server/avl/seqset.go:169` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:424` | `CloneNode` private static method |
| `SequenceSet.Clone` | `golang/nats-server/server/avl/seqset.go:180` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:195` | |
| `SequenceSet.Union` (method) | `golang/nats-server/server/avl/seqset.go:191` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:203` | |
| `Union` (function) | `golang/nats-server/server/avl/seqset.go:208` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:228` | `SequenceSet.CreateUnion` static method |
| `SequenceSet.EncodeLen` | `golang/nats-server/server/avl/seqset.go:238` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:252` | `EncodeLength()` in .NET |
| `SequenceSet.Encode` | `golang/nats-server/server/avl/seqset.go:242` | PARTIAL | `src/NATS.Server/Internal/Avl/SequenceSet.cs:255` | Go signature takes optional buf to reuse; .NET always allocates new. Behavior equivalent. |
| `ErrBadEncoding` | `golang/nats-server/server/avl/seqset.go:276` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:288` | Thrown as `InvalidOperationException` |
| `ErrBadVersion` | `golang/nats-server/server/avl/seqset.go:277` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:295` | Thrown as `InvalidOperationException` |
| `ErrSetNotEmpty` | `golang/nats-server/server/avl/seqset.go:278` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:93` | Thrown as `InvalidOperationException` |
| `Decode` | `golang/nats-server/server/avl/seqset.go:282` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:285` | Static method, same v1/v2 dispatch |
| `decodev2` (unexported) | `golang/nats-server/server/avl/seqset.go:298` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:300` | `DecodeV2` private static |
| `decodev1` (unexported) | `golang/nats-server/server/avl/seqset.go:329` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:338` | `DecodeV1` private static |
| `SequenceSet.insertNode` (unexported) | `golang/nats-server/server/avl/seqset.go:376` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:389` | `InsertNode` private method |
| `node` (struct) | `golang/nats-server/server/avl/seqset.go:407` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:439` | `Node` nested class |
| `node.set` | `golang/nats-server/server/avl/seqset.go:418` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:448` | `SetBit` |
| `node.insert` | `golang/nats-server/server/avl/seqset.go:428` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:521` | `Node.Insert` static |
| `node.rotateL` | `golang/nats-server/server/avl/seqset.go:464` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:672` | `RotateLeft` private static |
| `node.rotateR` | `golang/nats-server/server/avl/seqset.go:478` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:692` | `RotateRight` private static |
| `balanceF` | `golang/nats-server/server/avl/seqset.go:492` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:711` | `BalanceFactor` internal static |
| `maxH` | `golang/nats-server/server/avl/seqset.go:506` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:724` | `MaxHeight` internal static |
| `node.clear` | `golang/nats-server/server/avl/seqset.go:526` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:461` | `ClearBit` |
| `node.delete` | `golang/nats-server/server/avl/seqset.go:542` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:571` | `Node.Delete` static |
| `node.insertNodePrev` | `golang/nats-server/server/avl/seqset.go:588` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:635` | `InsertNodePrev` private static |
| `node.exists` | `golang/nats-server/server/avl/seqset.go:613` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:484` | `ExistsBit` |
| `node.min` | `golang/nats-server/server/avl/seqset.go:622` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:493` | `Min()` |
| `node.max` | `golang/nats-server/server/avl/seqset.go:635` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:508` | `Max()` |
| `node.nodeIter` | `golang/nats-server/server/avl/seqset.go:647` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:738` | `NodeIter` internal static |
| `node.iter` | `golang/nats-server/server/avl/seqset.go:658` | PORTED | `src/NATS.Server/Internal/Avl/SequenceSet.cs:751` | `Iter` internal static |
---
### stree/stree.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `SubjectTree[T]` (struct) | `golang/nats-server/server/stree/stree.go:28` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:9` | |
| `NewSubjectTree[T]` | `golang/nats-server/server/stree/stree.go:34` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:9` | In .NET, `new SubjectTree<T>()` used directly |
| `SubjectTree.Size` | `golang/nats-server/server/stree/stree.go:39` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:17` | |
| `SubjectTree.Empty` | `golang/nats-server/server/stree/stree.go:47` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:22` | |
| `SubjectTree.Insert` | `golang/nats-server/server/stree/stree.go:56` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:33` | Signature uses `ReadOnlySpan<byte>` |
| `SubjectTree.Find` | `golang/nats-server/server/stree/stree.go:74` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:52` | |
| `SubjectTree.Delete` | `golang/nats-server/server/stree/stree.go:106` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:97` | |
| `SubjectTree.Match` | `golang/nats-server/server/stree/stree.go:119` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:115` | |
| `SubjectTree.MatchUntil` | `golang/nats-server/server/stree/stree.go:137` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:135` | |
| `SubjectTree.IterOrdered` | `golang/nats-server/server/stree/stree.go:149` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:149` | |
| `SubjectTree.IterFast` | `golang/nats-server/server/stree/stree.go:158` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:158` | |
| `SubjectTree.insert` (internal) | `golang/nats-server/server/stree/stree.go:169` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:170` | `InsertInternal` |
| `SubjectTree.delete` (internal) | `golang/nats-server/server/stree/stree.go:253` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:286` | `DeleteInternal` |
| `SubjectTree.match` (internal) | `golang/nats-server/server/stree/stree.go:318` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:383` | `MatchInternal` |
| `SubjectTree.iter` (internal) | `golang/nats-server/server/stree/stree.go:418` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:512` | `IterInternal` |
| `LazyIntersect[TL,TR]` | `golang/nats-server/server/stree/stree.go:463` | PORTED | `src/NATS.Server/Internal/SubjectTree/SubjectTree.cs:584` | `SubjectTreeHelper.LazyIntersect` static |
| `IntersectGSL[T,SL]` | `golang/nats-server/server/stree/stree.go:488` | MISSING | — | No .NET equivalent. Intersects stree with gsl.GenericSublist. Used in JetStream consumer NumPending. |
| `_intersectGSL` (internal) | `golang/nats-server/server/stree/stree.go:496` | MISSING | — | Helper for IntersectGSL |
| `hasInterestForTokens` (internal) | `golang/nats-server/server/stree/stree.go:521` | MISSING | — | Token-boundary interest check for GSL intersection |
| `bytesToString` (internal) | `golang/nats-server/server/stree/stree.go:534` | NOT_APPLICABLE | — | Go `unsafe` zero-copy string conversion. In .NET, `System.Text.Encoding.UTF8.GetString` or `MemoryMarshal.Cast` used instead |
---
### stree/node.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `node` (interface) | `golang/nats-server/server/stree/node.go:17` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:7` | `INode` interface |
| `meta` (struct) | `golang/nats-server/server/stree/node.go:35` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:46` | `NodeMeta` class |
| `meta.isLeaf` | `golang/nats-server/server/stree/node.go:40` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:9` | Via `INode.IsLeaf` |
| `meta.base` | `golang/nats-server/server/stree/node.go:41` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:10` | Via `INode.Base` |
| `meta.setPrefix` | `golang/nats-server/server/stree/node.go:43` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:11` | Via `INode.SetPrefix` |
| `meta.numChildren` | `golang/nats-server/server/stree/node.go:47` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:26` | Via `INode.NumChildren` |
| `meta.path` | `golang/nats-server/server/stree/node.go:48` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:27` | Via `INode.Path` |
| `meta.matchParts` | `golang/nats-server/server/stree/node.go:51` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:22` | Via `INode.MatchParts` |
---
### stree/leaf.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `leaf[T]` (struct) | `golang/nats-server/server/stree/leaf.go:23` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:58` | `Leaf<T>` class |
| `newLeaf[T]` | `golang/nats-server/server/stree/leaf.go:30` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:63` | `new Leaf<T>(suffix, value)` constructor |
| `leaf.isLeaf` | `golang/nats-server/server/stree/leaf.go:34` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:69` | |
| `leaf.base` | `golang/nats-server/server/stree/leaf.go:34` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:70` | Returns null |
| `leaf.match` | `golang/nats-server/server/stree/leaf.go:36` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:75` | |
| `leaf.setSuffix` | `golang/nats-server/server/stree/leaf.go:37` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:77` | |
| `leaf.matchParts` | `golang/nats-server/server/stree/leaf.go:39` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:85` | |
| `leaf.iter` | `golang/nats-server/server/stree/leaf.go:40` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:83` | No-op on leaf |
| `leaf.children` | `golang/nats-server/server/stree/leaf.go:41` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:81` | Returns empty array |
| `leaf.numChildren` | `golang/nats-server/server/stree/leaf.go:42` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:72` | Returns 0 |
| `leaf.path` | `golang/nats-server/server/stree/leaf.go:43` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:79` | Returns suffix |
---
### stree/node4.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `node4` (struct) | `golang/nats-server/server/stree/node4.go:18` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:105` | `Node4` class |
| `newNode4` | `golang/nats-server/server/stree/node4.go:24` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:111` | Constructor |
| `node4.addChild` | `golang/nats-server/server/stree/node4.go:31` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:128` | |
| `node4.findChild` | `golang/nats-server/server/stree/node4.go:40` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:136` | Returns `ChildRef?` wrapper |
| `node4.isFull` | `golang/nats-server/server/stree/node4.go:49` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:119` | |
| `node4.grow` | `golang/nats-server/server/stree/node4.go:51` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:174` | |
| `node4.deleteChild` | `golang/nats-server/server/stree/node4.go:60` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:149` | |
| `node4.shrink` | `golang/nats-server/server/stree/node4.go:80` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:184` | |
| `node4.iter` | `golang/nats-server/server/stree/node4.go:88` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:190` | |
| `node4.children` | `golang/nats-server/server/stree/node4.go:96` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:198` | |
---
### stree/node10.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `node10` (struct) | `golang/nats-server/server/stree/node10.go:20` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:217` | `Node10` class |
| `newNode10` | `golang/nats-server/server/stree/node10.go:26` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:223` | Constructor |
| `node10.addChild` | `golang/nats-server/server/stree/node10.go:34` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:240` | |
| `node10.findChild` | `golang/nats-server/server/stree/node10.go:43` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:248` | |
| `node10.isFull` | `golang/nats-server/server/stree/node10.go:52` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:231` | |
| `node10.grow` | `golang/nats-server/server/stree/node10.go:54` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:286` | |
| `node10.deleteChild` | `golang/nats-server/server/stree/node10.go:63` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:258` | |
| `node10.shrink` | `golang/nats-server/server/stree/node10.go:83` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:295` | |
| `node10.iter` | `golang/nats-server/server/stree/node10.go:95` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:307` | |
| `node10.children` | `golang/nats-server/server/stree/node10.go:103` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:315` | |
---
### stree/node16.go, stree/node48.go, stree/node256.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `node16` (struct) | `golang/nats-server/server/stree/node16.go` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:334` | `Node16` class |
| `node48` (struct) | `golang/nats-server/server/stree/node48.go` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:451` | `Node48` class |
| `node256` (struct) | `golang/nats-server/server/stree/node256.go` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:569` | `Node256` class |
---
### stree/parts.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `genParts` | `golang/nats-server/server/stree/parts.go:23` | PORTED | `src/NATS.Server/Internal/SubjectTree/Parts.cs:57` | `Parts.GenParts` static method |
| `matchParts` | `golang/nats-server/server/stree/parts.go:78` | PORTED | `src/NATS.Server/Internal/SubjectTree/Parts.cs:145` | `Parts.MatchPartsAgainstFragment` |
---
### stree/util.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `pwc`, `fwc`, `tsep` constants | `golang/nats-server/server/stree/util.go:17` | PORTED | `src/NATS.Server/Internal/SubjectTree/Parts.cs:10` | `Parts.Pwc`, `Parts.Fwc`, `Parts.Tsep` |
| `commonPrefixLen` | `golang/nats-server/server/stree/util.go:24` | PORTED | `src/NATS.Server/Internal/SubjectTree/Parts.cs:33` | `Parts.CommonPrefixLen` |
| `copyBytes` | `golang/nats-server/server/stree/util.go:36` | PORTED | `src/NATS.Server/Internal/SubjectTree/Parts.cs:47` | `Parts.CopyBytes` |
| `position` (interface) | `golang/nats-server/server/stree/util.go:45` | NOT_APPLICABLE | — | Go generic constraint on numeric types; not needed in .NET |
| `noPivot` constant | `golang/nats-server/server/stree/util.go:48` | PORTED | `src/NATS.Server/Internal/SubjectTree/Parts.cs:17` | `Parts.NoPivot` |
| `pivot` | `golang/nats-server/server/stree/util.go:52` | PORTED | `src/NATS.Server/Internal/SubjectTree/Parts.cs:23` | `Parts.Pivot` |
---
### stree/dump.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `SubjectTree.Dump` | `golang/nats-server/server/stree/dump.go:23` | MISSING | — | Debug utility for printing tree structure to an `io.Writer`. No .NET equivalent. Low priority — debug/diagnostic only. |
| `SubjectTree.dump` (internal) | `golang/nats-server/server/stree/dump.go:29` | MISSING | — | Internal helper for Dump |
| `dumpPre` | `golang/nats-server/server/stree/dump.go:59` | MISSING | — | Indentation helper for Dump |
| `leaf.kind`, `node4.kind`, etc. | `golang/nats-server/server/stree/dump.go:51` | PARTIAL | `src/NATS.Server/Internal/SubjectTree/Nodes.cs` | `INode.Kind` property exists on each node type, but `Dump` method is not implemented |
---
### thw/thw.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `ErrTaskNotFound` | `golang/nats-server/server/thw/thw.go:25` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:91` | Returned as `false`/exception instead of error |
| `ErrInvalidVersion` | `golang/nats-server/server/thw/thw.go:28` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:289` | Thrown as `InvalidOperationException` |
| `slot` (struct) | `golang/nats-server/server/thw/thw.go:39` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:406` | `Slot` internal class |
| `HashWheel` (struct) | `golang/nats-server/server/thw/thw.go:45` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:17` | `HashWheel` class |
| `HashWheelEntry` (struct) | `golang/nats-server/server/thw/thw.go:52` | MISSING | — | Go uses this struct for entry representation in some contexts; .NET uses `(ulong, long)` tuples inline instead |
| `NewHashWheel` | `golang/nats-server/server/thw/thw.go:58` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:34` | Constructor in .NET |
| `HashWheel.getPosition` (internal) | `golang/nats-server/server/thw/thw.go:66` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:50` | `GetPosition` private static |
| `newSlot` (internal) | `golang/nats-server/server/thw/thw.go:71` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:406` | Inline slot initialization in .NET |
| `HashWheel.Add` | `golang/nats-server/server/thw/thw.go:79` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:60` | Return type `void` vs `error` (never errors in practice) |
| `HashWheel.Remove` | `golang/nats-server/server/thw/thw.go:103` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:91` | Returns `bool` instead of `error` |
| `HashWheel.Update` | `golang/nats-server/server/thw/thw.go:123` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:122` | |
| `HashWheel.ExpireTasks` | `golang/nats-server/server/thw/thw.go:133` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:135` | |
| `HashWheel.expireTasks` (internal) | `golang/nats-server/server/thw/thw.go:138` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:148` | `ExpireTasksInternal` — public in .NET for testability |
| `HashWheel.GetNextExpiration` | `golang/nats-server/server/thw/thw.go:182` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:219` | |
| `HashWheel.Count` | `golang/nats-server/server/thw/thw.go:190` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:44` | Property in .NET |
| `HashWheel.Encode` | `golang/nats-server/server/thw/thw.go:197` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:235` | |
| `HashWheel.Decode` | `golang/nats-server/server/thw/thw.go:216` | PORTED | `src/NATS.Server/Internal/TimeHashWheel/HashWheel.cs:282` | Returns `(highSeq, bytesRead)` tuple; Go returns `(uint64, error)` |
---
### gsl/gsl.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `ErrInvalidSubject` | `golang/nats-server/server/gsl/gsl.go:41` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:11` | `GslErrors.InvalidSubject` |
| `ErrNotFound` | `golang/nats-server/server/gsl/gsl.go:42` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:12` | `GslErrors.NotFound` |
| `ErrNilChan` | `golang/nats-server/server/gsl/gsl.go:43` | NOT_APPLICABLE | — | Go channel-specific error; channels don't apply to .NET pattern |
| `ErrAlreadyRegistered` | `golang/nats-server/server/gsl/gsl.go:44` | NOT_APPLICABLE | — | Used by notification channels in Go; no notification channel pattern in .NET port |
| `SimpleSublist` (type alias) | `golang/nats-server/server/gsl/gsl.go:49` | PARTIAL | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:650` | `SimpleSubjectList` uses `int` instead of `struct{}`. Functionally equivalent but not a true zero-allocation alias |
| `NewSimpleSublist` | `golang/nats-server/server/gsl/gsl.go:52` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:650` | `new SimpleSubjectList()` |
| `GenericSublist[T]` (struct) | `golang/nats-server/server/gsl/gsl.go:57` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:76` | `GenericSubjectList<T>` |
| `node[T]` (struct) | `golang/nats-server/server/gsl/gsl.go:64` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:52` | `Node<T>` |
| `level[T]` (struct) | `golang/nats-server/server/gsl/gsl.go:71` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:19` | `Level<T>` |
| `newNode[T]` | `golang/nats-server/server/gsl/gsl.go:77` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:52` | Inline `new Node<T>()` |
| `newLevel[T]` | `golang/nats-server/server/gsl/gsl.go:82` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:19` | Inline `new Level<T>()` |
| `NewSublist[T]` | `golang/nats-server/server/gsl/gsl.go:87` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:76` | `new GenericSubjectList<T>()` constructor |
| `GenericSublist.Insert` | `golang/nats-server/server/gsl/gsl.go:92` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:110` | Throws instead of returning error |
| `GenericSublist.Match` | `golang/nats-server/server/gsl/gsl.go:150` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:205` | |
| `GenericSublist.MatchBytes` | `golang/nats-server/server/gsl/gsl.go:156` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:214` | |
| `GenericSublist.HasInterest` | `golang/nats-server/server/gsl/gsl.go:162` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:225` | |
| `GenericSublist.NumInterest` | `golang/nats-server/server/gsl/gsl.go:168` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:234` | |
| `GenericSublist.match` (internal) | `golang/nats-server/server/gsl/gsl.go:173` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:274` | `MatchInternal` |
| `GenericSublist.hasInterest` (internal) | `golang/nats-server/server/gsl/gsl.go:198` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:295` | `HasInterestInternal` |
| `matchLevelForAny[T]` | `golang/nats-server/server/gsl/gsl.go:223` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:400` | `MatchLevelForAny` private static |
| `callbacksForResults[T]` | `golang/nats-server/server/gsl/gsl.go:266` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:448` | `CallbacksForResults` private static |
| `matchLevel[T]` | `golang/nats-server/server/gsl/gsl.go:273` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:371` | `MatchLevel` private static |
| `lnt[T]` (struct) | `golang/nats-server/server/gsl/gsl.go:301` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:68` | `Lnt<T>` record struct |
| `GenericSublist.remove` (internal) | `golang/nats-server/server/gsl/gsl.go:308` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:458` | `RemoveInternal` |
| `GenericSublist.Remove` | `golang/nats-server/server/gsl/gsl.go:368` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:188` | |
| `GenericSublist.HasInterestStartingIn` | `golang/nats-server/server/gsl/gsl.go:373` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:245` | |
| `hasInterestStartingIn[T]` (internal) | `golang/nats-server/server/gsl/gsl.go:381` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:537` | `HasInterestStartingInLevel` private static |
| `level.pruneNode` | `golang/nats-server/server/gsl/gsl.go:403` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:37` | `Level<T>.PruneNode` |
| `node.isEmpty` | `golang/nats-server/server/gsl/gsl.go:418` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:61` | `Node<T>.IsEmpty()` |
| `level.numNodes` | `golang/nats-server/server/gsl/gsl.go:423` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:25` | `Level<T>.NumNodes()` |
| `GenericSublist.removeFromNode` (internal) | `golang/nats-server/server/gsl/gsl.go:438` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:527` | `RemoveFromNode` private static |
| `GenericSublist.Count` | `golang/nats-server/server/gsl/gsl.go:449` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:90` | |
| `GenericSublist.numLevels` (internal) | `golang/nats-server/server/gsl/gsl.go:457` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:264` | `NumLevels()` internal |
| `visitLevel[T]` | `golang/nats-server/server/gsl/gsl.go:463` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:557` | `VisitLevel` private static |
| `tokenizeSubjectIntoSlice` | `golang/nats-server/server/gsl/gsl.go:496` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:348` | `TokenizeSubjectIntoSpan` |
---
### pse/ (Platform-Specific Extensions)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `ProcUsage` (darwin) | `golang/nats-server/server/pse/pse_darwin.go:83` | PORTED | `src/NATS.Server/Monitoring/VarzHandler.cs:35` | CPU via `Process.TotalProcessorTime`; RSS via `proc.WorkingSet64`. Equivalent behavior in `/varz` reporting. |
| `ProcUsage` (linux) | `golang/nats-server/server/pse/pse_linux.go` | PORTED | `src/NATS.Server/Monitoring/VarzHandler.cs:35` | Same cross-platform .NET equivalent |
| `ProcUsage` (windows) | `golang/nats-server/server/pse/pse_windows.go` | PORTED | `src/NATS.Server/Monitoring/VarzHandler.cs:35` | Same cross-platform .NET equivalent |
| `ProcUsage` (freebsd/netbsd/openbsd/dragonfly/solaris/zos) | `golang/nats-server/server/pse/pse_*.go` | NOT_APPLICABLE | — | Platform-specific Go build-tagged files. .NET runtime abstracts these OS differences. |
| `ProcUsage` (wasm/rumprun) | `golang/nats-server/server/pse/pse_wasm.go` | NOT_APPLICABLE | — | Stub/no-op implementations for unsupported platforms; not needed in .NET |
| `updateUsage` (darwin, internal) | `golang/nats-server/server/pse/pse_darwin.go:56` | PORTED | `src/NATS.Server/Monitoring/VarzHandler.cs:39` | CPU sampling logic in VarzHandler |
| `periodic` (darwin, internal) | `golang/nats-server/server/pse/pse_darwin.go:76` | PARTIAL | `src/NATS.Server/Monitoring/VarzHandler.cs:39` | Go runs periodic background timer; .NET samples on each `/varz` request with 1s cache. Semantics slightly different. |
---
### sysmem/ (System Memory Queries)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `Memory` (darwin) | `golang/nats-server/server/sysmem/mem_darwin.go:18` | MISSING | — | Queries total physical RAM via `hw.memsize` sysctl. No .NET equivalent in codebase. Used by JetStream for sizing decisions. |
| `Memory` (linux) | `golang/nats-server/server/sysmem/mem_linux.go:20` | MISSING | — | Queries via `syscall.Sysinfo`. No .NET equivalent found. |
| `Memory` (windows) | `golang/nats-server/server/sysmem/mem_windows.go` | MISSING | — | No .NET equivalent found. Can be implemented via `GC.GetGCMemoryInfo().TotalAvailableMemoryBytes`. |
| `Memory` (bsd/solaris/wasm/zos) | `golang/nats-server/server/sysmem/mem_bsd.go` etc. | NOT_APPLICABLE | — | Platform-specific stubs; .NET runtime abstracts these. `GCMemoryInfo` is the cross-platform equivalent. |
| `sysctlInt64` | `golang/nats-server/server/sysmem/sysctl.go:23` | NOT_APPLICABLE | — | Darwin/BSD internal helper using unsafe sysctl; .NET abstracts this entirely |
---
## 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/Internal/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Internal/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated: 157 PORTED, 4 PARTIAL, 10 MISSING, 8 NOT_APPLICABLE, 0 DEFERRED | auto |

1935
gaps/jetstream.md Normal file

File diff suppressed because it is too large Load Diff

280
gaps/leaf-nodes.md Normal file
View File

@@ -0,0 +1,280 @@
# 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](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 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.go`
- `golang/nats-server/server/leafnode_proxy_test.go`
- `golang/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
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### 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 (`+`/`|`), queue-group args, header-size field |
| `(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:
1. **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.
2. **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.
3. **No INFO protocol handling**: Dynamic URL list updates, compression negotiation, and permission updates over async INFO are unimplemented.
4. **No compression**: S2 compression negotiation between hub and leaf is entirely absent.
5. **No HTTP proxy tunnel**: `establishHTTPProxyTunnel` for WebSocket-via-proxy leaf connections is not ported.
6. **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.
7. **No route/gateway propagation**: When a leaf sub arrives, Go also updates routes and gateways; .NET does not.
8. **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.
9. **No JetStream RAFT integration**: `clearObserverState`, `checkJetStreamMigrate` (actual RAFT step-down), and `checkInternalSyncConsumers` are all absent.
10. **WebSocket path incomplete**: The `WebSocketStreamAdapter` adapts the byte stream, but the HTTP upgrade handshake (`GET /leafnode` with `Sec-WebSocket-Key`) is not implemented for solicited WS leaf connections.
---
## 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/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
```
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-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 |

217
gaps/logging.md Normal file
View File

@@ -0,0 +1,217 @@
# Logging — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Logging** 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 Logging
- .NET has **0 LOC** for logging because it uses `Microsoft.Extensions.Logging` (`ILogger<T>`) with Serilog as the provider.
- This is an intentional architectural difference, not a gap.
- Analysis should verify:
1. All log levels from Go (`Debug`, `Trace`, `Notice`, `Warn`, `Error`, `Fatal`) are mapped to .NET equivalents
2. Log output format includes the same fields (timestamps, client IDs, subjects)
3. Syslog output capability exists if needed (Serilog has syslog sinks)
- Most items in this category will be **NOT_APPLICABLE** or **PORTED** (via framework).
---
## Go Reference Files (Source)
- `golang/nats-server/server/log.go` — Server logging facade (~287 lines)
- `golang/nats-server/logger/log.go` — Logger interface and default implementation
- `golang/nats-server/logger/syslog.go` — Syslog backend
- `golang/nats-server/logger/syslog_windows.go` — Windows-specific syslog
## Go Reference Files (Tests)
- `golang/nats-server/server/log_test.go`
- `golang/nats-server/logger/log_test.go`
- `golang/nats-server/logger/syslog_test.go`
- `golang/nats-server/logger/syslog_windows_test.go`
## .NET Implementation Files (Source)
- (none — .NET uses `Microsoft.Extensions.Logging` + `Serilog` NuGet packages)
## .NET Implementation Files (Tests)
- (none — logging tests are integrated into other test areas)
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### golang/nats-server/server/log.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `Logger` interface | log.go:27-46 | PORTED | ILogger via Microsoft.Extensions.Logging | .NET uses standard Microsoft.Extensions.Logging framework instead of custom Logger interface |
| `Logger.Noticef()` | log.go:30 | PORTED | ILogger.LogInformation() | Maps to LogInformation (notice level) |
| `Logger.Warnf()` | log.go:33 | PORTED | ILogger.LogWarning() | Maps to LogWarning level |
| `Logger.Fatalf()` | log.go:36 | PORTED | ILogger.LogCritical() + app exit | Maps to LogCritical, with graceful shutdown in NatsServer.cs |
| `Logger.Errorf()` | log.go:39 | PORTED | ILogger.LogError() | Maps to LogError level |
| `Logger.Debugf()` | log.go:42 | PORTED | ILogger.LogDebug() | Maps to LogDebug level |
| `Logger.Tracef()` | log.go:45 | PORTED | ILogger.LogTrace() (Verbose in Serilog) | Maps to Verbose/Trace level |
| `Server.ConfigureLogger()` | log.go:49-101 | PORTED | Program.cs LoggerConfiguration setup | .NET uses Serilog configuration in Program.cs instead of per-server method |
| `Server.Logger()` | log.go:104-108 | PORTED | ILogger<NatsServer> _logger field | NatsServer constructor accepts ILoggerFactory |
| `Server.SetLogger()` | log.go:111-113 | PARTIAL | ILoggerFactory injected at construction | .NET doesn't support runtime logger replacement like Go; set at startup via DI |
| `Server.SetLoggerV2()` | log.go:116-145 | PARTIAL | Serilog dynamic configuration + ILoggerFactory | .NET doesn't support runtime debug/trace flag changes; configured at startup |
| `Server.ReOpenLogFile()` | log.go:150-178 | PORTED | Program.cs server.ReOpenLogFile callback | Handler delegate set in Program.cs to close and recreate Serilog logger |
| `Server.Noticef()` | log.go:181-185 | PORTED | _logger.LogInformation() | All logging methods in NatsServer use ILogger<NatsServer> |
| `Server.Errorf()` | log.go:188-192 | PORTED | _logger.LogError() | Direct logging to ILogger |
| `Server.Errors()` | log.go:195-199 | PORTED | _logger.LogError() + structured args | Error logging with scope context |
| `Server.Errorc()` | log.go:202-206 | PORTED | _logger.LogError() + structured args | Error logging with context |
| `Server.Errorsc()` | log.go:209-213 | PORTED | _logger.LogError() + structured args | Error logging with scope and context |
| `Server.Warnf()` | log.go:216-220 | PORTED | _logger.LogWarning() | Direct logging to ILogger |
| `Server.rateLimitFormatWarnf()` | log.go:222-228 | NOT_APPLICABLE | Rate limiting via sync.Map, not implemented in .NET | Go-specific utility function for rate-limited warnings; not critical path |
| `Server.RateLimitWarnf()` | log.go:230-236 | NOT_APPLICABLE | Rate limiting via sync.Map, not implemented in .NET | Go-specific utility function; can be added if needed |
| `Server.RateLimitDebugf()` | log.go:238-244 | NOT_APPLICABLE | Rate limiting via sync.Map, not implemented in .NET | Go-specific utility function; can be added if needed |
| `Server.Fatalf()` | log.go:247-255 | PARTIAL | _logger.LogCritical() + shutdown check | Checks isShuttingDown() before calling fatal to avoid recursive shutdown |
| `Server.Debugf()` | log.go:258-266 | PORTED | _logger.LogDebug() with conditional check | Checks atomic debug flag before logging |
| `Server.Tracef()` | log.go:269-277 | PORTED | _logger.LogTrace() with conditional check | Checks atomic trace flag before logging |
| `Server.executeLogCall()` | log.go:279-287 | PORTED | ILogger methods directly called | .NET doesn't need wrapper; calls ILogger directly |
### golang/nats-server/logger/log.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `Logger` struct | log.go:32-44 | PORTED | Serilog ILogger via Microsoft.Extensions.Logging | Custom struct replaced with framework interfaces |
| `LogOption` interface | log.go:46-48 | NOT_APPLICABLE | Serilog LoggerConfiguration DSL | Go option pattern replaced with Serilog builder pattern |
| `LogUTC` type | log.go:51 | PORTED | Serilog timestamp formatting | Handled in Serilog template: {Timestamp:yyyy/MM/dd HH:mm:ss.ffffff} or local |
| `logFlags()` | log.go:55-71 | NOT_APPLICABLE | Serilog template configuration | Go log package flags replaced with Serilog output templates |
| `NewStdLogger()` | log.go:74-95 | PORTED | logConfig.WriteTo.Console() in Program.cs | Creates console sink with color detection (AnsiConsoleTheme.Code) |
| `NewFileLogger()` | log.go:98-124 | PORTED | logConfig.WriteTo.File() in Program.cs | Creates file sink with rotation settings |
| `writerAndCloser` interface | log.go:126-130 | NOT_APPLICABLE | io.Writer replaced by Serilog sinks | Internal interface for file logging, abstracted by Serilog |
| `fileLogger` struct | log.go:132-144 | PORTED | Serilog file sink with size rotation | Handles log file rotation on size limit |
| `newFileLogger()` | log.go:146-165 | PORTED | Serilog WriteTo.File() setup | File creation and sizing handled by Serilog |
| `fileLogger.setLimit()` | log.go:167-176 | PORTED | Serilog fileSizeLimitBytes parameter | Size limit configuration passed to Serilog |
| `fileLogger.setMaxNumFiles()` | log.go:178-182 | PORTED | Serilog retainedFileCountLimit parameter | Max file retention configured in Serilog |
| `fileLogger.logDirect()` | log.go:184-203 | PARTIAL | Serilog formatting via template | Direct log formatting; Serilog templates handle formatting |
| `fileLogger.logPurge()` | log.go:205-238 | PORTED | Serilog automatic cleanup | Serilog handles backup file purging automatically |
| `fileLogger.Write()` | log.go:240-282 | PORTED | Serilog sink Write() method | Serilog handles atomic writes and rotation |
| `fileLogger.close()` | log.go:284-293 | PORTED | Log.CloseAndFlush() in Program.cs | Proper cleanup via Serilog disposal |
| `Logger.SetSizeLimit()` | log.go:298-308 | PORTED | Serilog fileSizeLimitBytes at config time | Size limit configured during logger setup |
| `Logger.SetMaxNumFiles()` | log.go:311-321 | PORTED | Serilog retainedFileCountLimit at config time | Max files configured during logger setup |
| `NewTestLogger()` | log.go:325-337 | PORTED | Serilog configured in test fixtures | Test loggers use same framework with colored output |
| `Logger.Close()` | log.go:342-347 | PORTED | Log.CloseAndFlush() via finally block | Cleanup handled in Program.cs finally block |
| `pidPrefix()` | log.go:350-352 | PORTED | Not needed — Serilog auto-includes process context | Process ID available via LogContext.PushProperty() if needed |
| `setPlainLabelFormats()` | log.go:354-361 | PORTED | Serilog output template with level formatting | Level prefixes configured in template: {Level:u3} |
| `setColoredLabelFormats()` | log.go:363-371 | PORTED | Serilog AnsiConsoleTheme.Code for colors | Color formatting handled by Serilog theme |
| `Logger.Noticef()` | log.go:374-376 | PORTED | ILogger.LogInformation() | Maps notice → information |
| `Logger.Warnf()` | log.go:379-381 | PORTED | ILogger.LogWarning() | Maps warning → warning |
| `Logger.Errorf()` | log.go:384-386 | PORTED | ILogger.LogError() | Maps error → error |
| `Logger.Fatalf()` | log.go:389-391 | PORTED | ILogger.LogCritical() + log.Fatal() | Maps fatal → critical + exit behavior |
| `Logger.Debugf()` | log.go:394-398 | PORTED | ILogger.LogDebug() with conditional check | Conditional on debug flag |
| `Logger.Tracef()` | log.go:401-405 | PORTED | ILogger.LogTrace() with conditional check | Conditional on trace flag |
### golang/nats-server/logger/syslog.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `SysLogger` struct (Unix) | syslog.go:28-32 | PORTED | Serilog.Sinks.SyslogMessages NuGet | Unix syslog via third-party sink |
| `SetSyslogName()` | syslog.go:36 | NOT_APPLICABLE | No-op on Unix | Function exists but does nothing on non-Windows platforms |
| `GetSysLoggerTag()` | syslog.go:42-51 | PORTED | Hardcoded "nats-server" in Program.cs | Tag name extracted once at startup |
| `NewSysLogger()` | syslog.go:54-65 | PORTED | logConfig.WriteTo.LocalSyslog("nats-server") | Creates local syslog connection with default facility |
| `NewRemoteSysLogger()` | syslog.go:68-80 | PORTED | logConfig.WriteTo.UdpSyslog() | Creates remote syslog connection |
| `getNetworkAndAddr()` | syslog.go:82-98 | PORTED | URL parsing in Program.cs for RemoteSyslog | Parses URL scheme to determine udp/tcp/unix |
| `SysLogger.Noticef()` | syslog.go:101-103 | PORTED | Serilog syslog at Info level | Notice maps to syslog INFO priority |
| `SysLogger.Warnf()` | syslog.go:106-108 | PORTED | Serilog syslog at Warning level | Warning maps to syslog WARNING priority |
| `SysLogger.Fatalf()` | syslog.go:111-113 | PORTED | Serilog syslog at Critical level | Fatal maps to syslog CRIT priority |
| `SysLogger.Errorf()` | syslog.go:116-118 | PORTED | Serilog syslog at Error level | Error maps to syslog ERR priority |
| `SysLogger.Debugf()` | syslog.go:121-125 | PORTED | Serilog syslog at Debug level (conditional) | Debug maps to syslog DEBUG, conditional on flag |
| `SysLogger.Tracef()` | syslog.go:128-132 | PORTED | Serilog syslog at Info level (conditional) | Trace maps to syslog INFO, conditional on flag |
### golang/nats-server/logger/syslog_windows.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `SysLogger` struct (Windows) | syslog_windows.go:33-37 | DEFERRED | Windows Event Log not yet integrated | Go uses golang.org/x/sys/windows/svc/eventlog; .NET side not implemented |
| `SetSyslogName()` | syslog_windows.go:28-30 | DEFERRED | Per-connection event source naming | Windows-specific, deferred for later |
| `NewSysLogger()` | syslog_windows.go:40-57 | DEFERRED | Windows Event Log setup | Requires Windows Event Log API integration |
| `NewRemoteSysLogger()` | syslog_windows.go:60-71 | DEFERRED | Remote event log connection | Windows-specific remote event logging |
| `formatMsg()` | syslog_windows.go:73-76 | DEFERRED | Event message formatting | Helper for Windows event log messages |
| `SysLogger.Noticef()` | syslog_windows.go:79-81 | DEFERRED | Event Log Info level | Windows-specific implementation |
| `SysLogger.Warnf()` | syslog_windows.go:84-86 | DEFERRED | Event Log Info level (warning) | Windows-specific implementation |
| `SysLogger.Fatalf()` | syslog_windows.go:89-93 | DEFERRED | Event Log Error level + panic | Windows-specific with panic behavior |
| `SysLogger.Errorf()` | syslog_windows.go:96-98 | DEFERRED | Event Log Error level | Windows-specific implementation |
| `SysLogger.Debugf()` | syslog_windows.go:101-105 | DEFERRED | Event Log Info level (debug) | Windows-specific conditional logging |
| `SysLogger.Tracef()` | syslog_windows.go:108-112 | DEFERRED | Event Log Info level (trace) | Windows-specific conditional logging |
---
## 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
# No custom logging files — uses Microsoft.Extensions.Logging NuGet
# Re-count .NET test LOC for this module
# No dedicated logging test files
```
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-25 | Complete gap inventory analysis: 56 Go symbols analyzed, 44 PORTED, 7 PARTIAL, 0 MISSING, 4 NOT_APPLICABLE, 11 DEFERRED | Claude Code |
| 2026-02-25 | File created with LLM analysis instructions | auto |

169
gaps/misc-uncategorized.md Normal file
View File

@@ -0,0 +1,169 @@
# Misc / Uncategorized — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Misc / Uncategorized** 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 Misc / Uncategorized
- This file captures items that don't cleanly fit elsewhere.
- `norace_*_test.go` files (12,009 LOC) contain long-running stress/race tests spanning multiple subsystems — map to `Stress/` tests in .NET.
- `benchmark_*.go` files contain Go benchmarks — may map to `[Fact]` tests with timing assertions or BenchmarkDotNet.
- The Go `test/` integration directory (35 files, 29,812 LOC) contains end-to-end tests that may overlap with .NET test subdirectories.
- As items are identified and categorized, move them to the appropriate category file.
---
## Go Reference Files (Source)
- Files not fitting into other categories
- Small Go source files that don't belong to a major subsystem
- Platform-specific stubs or build-tag-only files
## Go Reference Files (Tests)
- `golang/nats-server/server/ping_test.go`
- `golang/nats-server/server/closed_conns_test.go`
- `golang/nats-server/server/norace_1_test.go`
- `golang/nats-server/server/norace_2_test.go`
- `golang/nats-server/server/benchmark_publish_test.go`
- `golang/nats-server/server/core_benchmarks_test.go`
- Various integration tests in `golang/nats-server/test/`
## .NET Implementation Files (Source)
- Any .NET source files not covered by other category files
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Stress/`
- `tests/NATS.Server.Tests/Parity/`
- Other root-level test files
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### Platform-Specific Source Files
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| diskAvailable | golang/nats-server/server/disk_avail.go:1 | NOT_APPLICABLE | N/A | Base function; platform implementations below override it |
| diskAvailable (openbsd) | golang/nats-server/server/disk_avail_openbsd.go:23 | PARTIAL | NatsServer.cs (JetStream disk checking stub) | .NET has minimal disk-space checking; full syscall.Statfs equivalent not implemented |
| diskAvailable (netbsd) | golang/nats-server/server/disk_avail_netbsd.go:19 | NOT_APPLICABLE | N/A | NetBSD stub returning default; .NET runs on Windows/.NET platforms only |
| diskAvailable (solaris) | golang/nats-server/server/disk_avail_solaris.go:23 | NOT_APPLICABLE | N/A | Solaris/illumos stub; not supported platform for .NET |
| diskAvailable (wasm) | golang/nats-server/server/disk_avail_wasm.go:18 | NOT_APPLICABLE | N/A | WASM stub; .NET does not compile to WASM for NATS server |
| diskAvailable (windows) | golang/nats-server/server/disk_avail_windows.go:19 | PARTIAL | NatsServer.cs (JetStream disk checking stub) | .NET has minimal implementation; full Windows disk space API not used |
| SetServiceName | golang/nats-server/server/service_windows.go:34 | MISSING | N/A | .NET host app does not implement Windows service mode |
| winServiceWrapper.Execute | golang/nats-server/server/service_windows.go:64 | MISSING | N/A | .NET uses standard .NET Worker Service abstraction instead |
| Run (service) | golang/nats-server/server/service_windows.go:115 | MISSING | N/A | .NET app startup does not support Windows service wrapper |
| isWindowsService | golang/nats-server/server/service_windows.go:132 | MISSING | N/A | .NET does not expose Windows service detection |
| handleSignals (wasm) | golang/nats-server/server/signal_wasm.go:18 | NOT_APPLICABLE | N/A | WASM stub; .NET does not target WASM |
| ProcessSignal (wasm) | golang/nats-server/server/signal_wasm.go:22 | NOT_APPLICABLE | N/A | WASM stub; .NET does not target WASM |
| handleSignals (windows) | golang/nats-server/server/signal_windows.go:28 | PARTIAL | NatsServer.cs (event loop shutdown) | .NET uses CancellationToken instead of signal channels |
| ProcessSignal (windows) | golang/nats-server/server/signal_windows.go:53 | MISSING | N/A | .NET does not support remote signal commands to Windows services |
| reopenLogCode, ldmCode | golang/nats-server/server/service_windows.go:24-28 | PARTIAL | Host program configuration | .NET logging uses Serilog; log rotation not exposed as service commands |
### Test Files — Miscellaneous
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| TestPing | golang/nats-server/server/ping_test.go:34 | PORTED | tests/NATS.Server.Tests/ServerTests.cs:PingKeepaliveTests | Raw socket PING/PONG protocol test ported |
| DefaultPingOptions | golang/nats-server/server/ping_test.go:26 | PORTED | tests/NATS.Server.Tests/ServerTests.cs:PingKeepaliveTests | Test options reflected in C# test setup |
| TestClosedConnsAccounting | golang/nats-server/server/closed_conns_test.go:46 | PARTIAL | tests/NATS.Server.Tests/Monitoring/ClosedConnectionRingBufferTests.cs | Ring buffer implementation exists; not all closed-conn tracking tests ported |
| TestClosedConnsSubsAccounting | golang/nats-server/server/closed_conns_test.go:102 | PARTIAL | tests/NATS.Server.Tests/Monitoring/ClosedConnectionRingBufferTests.cs | Subscription tracking in closed conns; basic tests exist |
| TestClosedAuthorizationTimeout | golang/nats-server/server/closed_conns_test.go:143 | MISSING | N/A | Auth timeout closure tracking not fully tested |
| TestClosedAuthorizationViolation | golang/nats-server/server/closed_conns_test.go:164 | PARTIAL | tests/NATS.Server.Tests/Monitoring/MonitorGoParityTests.cs | Auth violation tracking partially tested |
| TestClosedUPAuthorizationViolation | golang/nats-server/server/closed_conns_test.go:187 | PARTIAL | tests/NATS.Server.Tests/Configuration/OptsGoParityTests.cs | Username/password auth failure tracking not fully tested |
| TestClosedMaxPayload | golang/nats-server/server/closed_conns_test.go:219 | MISSING | N/A | Max payload violation closure tracking not tested |
| TestClosedTLSHandshake | golang/nats-server/server/closed_conns_test.go:247 | MISSING | N/A | TLS handshake failure closure tracking not tested |
| NoRace tests (norace_1_test.go) | golang/nats-server/server/norace_1_test.go:1 | PARTIAL | tests/NATS.Server.Tests/Stress/ (2,342 LOC vs 8,497 Go LOC) | Long-running race/concurrency tests; ~27% mapped to .NET Stress tests |
| NoRace tests (norace_2_test.go) | golang/nats-server/server/norace_2_test.go:1 | PARTIAL | tests/NATS.Server.Tests/Stress/ | Additional race/concurrency scenarios; ~27% coverage in .NET |
| BenchmarkPublish | golang/nats-server/server/benchmark_publish_test.go:1 | DEFERRED | N/A | Go benchmarks not directly portable; .NET uses different perf tooling (BenchmarkDotNet) |
| CoreBenchmarks | golang/nats-server/server/core_benchmarks_test.go:1 | DEFERRED | N/A | Core server benchmarks; .NET performance profiling uses different approach |
### Integration Tests (test/ directory)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| test/ping_test.go | golang/nats-server/test/ping_test.go:1 | PARTIAL | tests/NATS.Server.Tests/ServerTests.cs | Integration-level ping tests; basic coverage exists in .NET |
| test/norace_test.go | golang/nats-server/test/norace_test.go:1 | PARTIAL | tests/NATS.Server.Tests/Stress/ | Race-condition integration tests; some mapped to Stress tests |
| Other test/*.go (35 files total) | golang/nats-server/test/:1 | PARTIAL | tests/NATS.Server.Tests/ (spread across multiple categories) | Integration tests distributed across functional categories; not all scenarios covered |
---
## 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
# Various files — re-count as needed
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Stress/ tests/NATS.Server.Tests/Parity/ -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-25 | Initial gap inventory analysis: 8 platform-specific source files, 14 test symbols, 35 integration tests | claude |
| 2026-02-25 | File created with LLM analysis instructions | auto |

314
gaps/monitoring.md Normal file
View File

@@ -0,0 +1,314 @@
# Monitoring — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Monitoring** 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 Monitoring
- The monitoring JSON response shapes must match the Go server for tooling compatibility (nats-top, Prometheus exporters, etc.).
- Each endpoint has query parameters for filtering and sorting (e.g., `/connz?sort=bytes_to&limit=10`).
- `/healthz` is used for Kubernetes liveness probes and has JetStream-aware health checks.
- Port 8222 by default.
---
## Go Reference Files (Source)
- `golang/nats-server/server/monitor.go` — HTTP monitoring endpoints (~4,200 lines): `/varz`, `/connz`, `/routez`, `/gatewayz`, `/leafz`, `/subsz`, `/jsz`, `/healthz`, `/accountz`
- `golang/nats-server/server/monitor_sort_opts.go` — Sort options for monitoring endpoints
## Go Reference Files (Tests)
- `golang/nats-server/server/monitor_test.go`
- `golang/nats-server/test/monitor_test.go` (integration)
## .NET Implementation Files (Source)
- `src/NATS.Server/Monitoring/MonitorServer.cs`
- `src/NATS.Server/Monitoring/Varz.cs`
- `src/NATS.Server/Monitoring/Subsz.cs`
- `src/NATS.Server/Monitoring/VarzHandler.cs`
- `src/NATS.Server/Monitoring/SubszHandler.cs`
- `src/NATS.Server/Monitoring/JszHandler.cs`
- `src/NATS.Server/Monitoring/AccountzHandler.cs`
- `src/NATS.Server/Monitoring/GatewayzHandler.cs`
- `src/NATS.Server/Monitoring/LeafzHandler.cs`
- `src/NATS.Server/Monitoring/RoutezHandler.cs`
- `src/NATS.Server/Monitoring/PprofHandler.cs`
- `src/NATS.Server/Monitoring/ClosedClient.cs`
- All other files in `src/NATS.Server/Monitoring/`
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Monitoring/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### monitor_sort_opts.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `ConnInfos` (type alias `[]*ConnInfo`) | golang/nats-server/server/monitor_sort_opts.go:21 | PORTED | src/NATS.Server/Monitoring/Connz.cs:30 | .NET uses `ConnInfo[]` arrays instead |
| `ConnInfos.Len()` | golang/nats-server/server/monitor_sort_opts.go:25 | NOT_APPLICABLE | — | Go sort.Interface boilerplate; .NET LINQ handles sorting natively |
| `ConnInfos.Swap()` | golang/nats-server/server/monitor_sort_opts.go:28 | NOT_APPLICABLE | — | Go sort.Interface boilerplate; .NET LINQ handles sorting natively |
| `SortOpt` (string type) | golang/nats-server/server/monitor_sort_opts.go:31 | PORTED | src/NATS.Server/Monitoring/Connz.cs:595 | .NET has both `SortOpt` enum (internal) and `ConnzSortOption` enum (public API) |
| `ByCid` constant | golang/nats-server/server/monitor_sort_opts.go:35 | PORTED | src/NATS.Server/Monitoring/Connz.cs:597 | `SortOpt.ByCid` |
| `ByStart` constant | golang/nats-server/server/monitor_sort_opts.go:36 | PORTED | src/NATS.Server/Monitoring/Connz.cs:598 | `SortOpt.ByStart` |
| `BySubs` constant | golang/nats-server/server/monitor_sort_opts.go:37 | PORTED | src/NATS.Server/Monitoring/Connz.cs:599 | `SortOpt.BySubs` |
| `ByPending` constant | golang/nats-server/server/monitor_sort_opts.go:38 | PORTED | src/NATS.Server/Monitoring/Connz.cs:600 | `SortOpt.ByPending` |
| `ByOutMsgs` constant | golang/nats-server/server/monitor_sort_opts.go:39 | PORTED | src/NATS.Server/Monitoring/Connz.cs:601 | `SortOpt.ByMsgsTo` |
| `ByInMsgs` constant | golang/nats-server/server/monitor_sort_opts.go:40 | PORTED | src/NATS.Server/Monitoring/Connz.cs:602 | `SortOpt.ByMsgsFrom` |
| `ByOutBytes` constant | golang/nats-server/server/monitor_sort_opts.go:41 | PORTED | src/NATS.Server/Monitoring/Connz.cs:603 | `SortOpt.ByBytesTo` |
| `ByInBytes` constant | golang/nats-server/server/monitor_sort_opts.go:42 | PORTED | src/NATS.Server/Monitoring/Connz.cs:604 | `SortOpt.ByBytesFrom` |
| `ByLast` constant | golang/nats-server/server/monitor_sort_opts.go:43 | PORTED | src/NATS.Server/Monitoring/Connz.cs:605 | `SortOpt.ByLast` |
| `ByIdle` constant | golang/nats-server/server/monitor_sort_opts.go:44 | PORTED | src/NATS.Server/Monitoring/Connz.cs:606 | `SortOpt.ByIdle` |
| `ByUptime` constant | golang/nats-server/server/monitor_sort_opts.go:45 | PORTED | src/NATS.Server/Monitoring/Connz.cs:607 | `SortOpt.ByUptime` |
| `ByStop` constant | golang/nats-server/server/monitor_sort_opts.go:46 | PORTED | src/NATS.Server/Monitoring/Connz.cs:608 | `SortOpt.ByStop` |
| `ByReason` constant | golang/nats-server/server/monitor_sort_opts.go:47 | PORTED | src/NATS.Server/Monitoring/Connz.cs:609 | `SortOpt.ByReason` |
| `ByRTT` constant | golang/nats-server/server/monitor_sort_opts.go:48 | PORTED | src/NATS.Server/Monitoring/Connz.cs:610 | `SortOpt.ByRtt` |
| `SortByCid` | golang/nats-server/server/monitor_sort_opts.go:53 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:59 | Implemented as LINQ OrderBy in the sort switch |
| `SortBySubs` | golang/nats-server/server/monitor_sort_opts.go:58 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:61 | LINQ OrderByDescending |
| `SortByPending` | golang/nats-server/server/monitor_sort_opts.go:63 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:62 | LINQ OrderByDescending |
| `SortByOutMsgs` | golang/nats-server/server/monitor_sort_opts.go:68 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:63 | LINQ OrderByDescending |
| `SortByInMsgs` | golang/nats-server/server/monitor_sort_opts.go:73 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:64 | LINQ OrderByDescending |
| `SortByOutBytes` | golang/nats-server/server/monitor_sort_opts.go:78 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:65 | LINQ OrderByDescending |
| `SortByInBytes` | golang/nats-server/server/monitor_sort_opts.go:83 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:66 | LINQ OrderByDescending |
| `SortByLast` | golang/nats-server/server/monitor_sort_opts.go:88 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:67 | LINQ OrderByDescending |
| `SortByIdle` | golang/nats-server/server/monitor_sort_opts.go:95 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:68 | LINQ OrderByDescending with now capture |
| `SortByUptime` | golang/nats-server/server/monitor_sort_opts.go:105 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:69 | LINQ OrderByDescending |
| `SortByStop` | golang/nats-server/server/monitor_sort_opts.go:128 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:70 | LINQ OrderByDescending |
| `SortByReason` | golang/nats-server/server/monitor_sort_opts.go:137 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:71 | LINQ OrderBy |
| `SortByRTT` | golang/nats-server/server/monitor_sort_opts.go:144 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:72 | LINQ OrderBy |
| `SortOpt.IsValid()` | golang/nats-server/server/monitor_sort_opts.go:149 | PARTIAL | src/NATS.Server/Monitoring/ConnzHandler.cs:246 | Validity is implicit in the parse switch (falls through to ByCid default); no explicit IsValid method |
---
### monitor.go — Types and Structs
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `Connz` struct | golang/nats-server/server/monitor.go:48 | PORTED | src/NATS.Server/Monitoring/Connz.cs:8 | All JSON fields match |
| `ConnzOptions` struct | golang/nats-server/server/monitor.go:59 | PORTED | src/NATS.Server/Monitoring/Connz.cs:628 | All options ported |
| `ConnState` type | golang/nats-server/server/monitor.go:102 | PORTED | src/NATS.Server/Monitoring/Connz.cs:617 | `ConnState` enum with Open/Closed/All |
| `ConnOpen` constant | golang/nats-server/server/monitor.go:105 | PORTED | src/NATS.Server/Monitoring/Connz.cs:619 | `ConnState.Open` |
| `ConnClosed` constant | golang/nats-server/server/monitor.go:107 | PORTED | src/NATS.Server/Monitoring/Connz.cs:620 | `ConnState.Closed` |
| `ConnAll` constant | golang/nats-server/server/monitor.go:109 | PORTED | src/NATS.Server/Monitoring/Connz.cs:621 | `ConnState.All` |
| `ConnInfo` struct | golang/nats-server/server/monitor.go:114 | PARTIAL | src/NATS.Server/Monitoring/Connz.cs:36 | Missing: `Stalls` field (`stalls,omitempty`), `TLSPeerCerts []*TLSPeerCert` (replaced with single subject string `TlsPeerCertSubject`), `JWT` field, `IssuerKey` field, `NameTag` field, `Tags` field, `Proxy *ProxyInfo` (replaced with plain string) |
| `ProxyInfo` struct | golang/nats-server/server/monitor.go:157 | PARTIAL | src/NATS.Server/Monitoring/Connz.cs:135 | .NET uses a plain `string Proxy` field instead of a struct with `Key` |
| `TLSPeerCert` struct | golang/nats-server/server/monitor.go:163 | MISSING | — | Go has Subject, SubjectPKISha256, CertSha256; .NET `ConnInfo.TlsPeerCertSubject` only captures Subject, no SHA256 fields |
| `DefaultConnListSize` constant | golang/nats-server/server/monitor.go:169 | PORTED | src/NATS.Server/Monitoring/Connz.cs:660 | Default 1024 on `ConnzOptions.Limit` |
| `DefaultSubListSize` constant | golang/nats-server/server/monitor.go:172 | PORTED | src/NATS.Server/Monitoring/Subsz.cs:41 | Default 1024 on `SubszOptions.Limit` |
| `Routez` struct | golang/nats-server/server/monitor.go:782 | PARTIAL | src/NATS.Server/Monitoring/RoutezHandler.cs:12 | Go has full Routez struct with ID, Name, Now, Import, Export, NumRoutes, Routes; .NET returns anonymous object with only `routes` and `num_routes` counts |
| `RoutezOptions` struct | golang/nats-server/server/monitor.go:793 | MISSING | — | No options struct; handler accepts no query params |
| `RouteInfo` struct | golang/nats-server/server/monitor.go:801 | MISSING | — | Go has full per-route details (Rid, RemoteID, RTT, etc.); .NET returns just route count |
| `Subsz` struct | golang/nats-server/server/monitor.go:928 | PARTIAL | src/NATS.Server/Monitoring/Subsz.cs:8 | Missing `SublistStats` embedded fields; .NET has `NumSubs` and `NumCache` instead of the full `SublistStats` type |
| `SubszOptions` struct | golang/nats-server/server/monitor.go:940 | PORTED | src/NATS.Server/Monitoring/Subsz.cs:38 | All options present |
| `SubDetail` struct | golang/nats-server/server/monitor.go:961 | PARTIAL | src/NATS.Server/Monitoring/Connz.cs:142 | Missing `AccountTag` field; present fields match |
| `Varz` struct | golang/nats-server/server/monitor.go:1211 | PARTIAL | src/NATS.Server/Monitoring/Varz.cs:8 | Missing: `ConfigDigest`, `TrustedOperatorsJwt`, `TrustedOperatorsClaim`, `PinnedAccountFail` (present), `OCSPResponseCache`, `StaleConnectionStats` (present), `Proxies`, `WriteTimeout` string field; `StalledClients` field missing; `Metadata` field missing |
| `JetStreamVarz` struct | golang/nats-server/server/monitor.go:1286 | PARTIAL | src/NATS.Server/Monitoring/Varz.cs:403 | Missing `Meta *MetaClusterInfo` and `Limits *JSLimitOpts` fields |
| `ClusterOptsVarz` struct | golang/nats-server/server/monitor.go:1294 | PARTIAL | src/NATS.Server/Monitoring/Varz.cs:251 | Missing `WriteDeadline`, `WriteTimeout`, `TLSCertNotAfter` fields |
| `GatewayOptsVarz` struct | golang/nats-server/server/monitor.go:1310 | PARTIAL | src/NATS.Server/Monitoring/Varz.cs:285 | Missing `Gateways []RemoteGatewayOptsVarz`, `WriteDeadline`, `WriteTimeout`, `TLSCertNotAfter` fields |
| `RemoteGatewayOptsVarz` struct | golang/nats-server/server/monitor.go:1328 | MISSING | — | No .NET equivalent |
| `LeafNodeOptsVarz` struct | golang/nats-server/server/monitor.go:1335 | PARTIAL | src/NATS.Server/Monitoring/Varz.cs:322 | Missing `Remotes []RemoteLeafOptsVarz`, `WriteDeadline`, `WriteTimeout`, `TLSCertNotAfter` fields |
| `DenyRules` struct | golang/nats-server/server/monitor.go:1350 | MISSING | — | No .NET equivalent |
| `RemoteLeafOptsVarz` struct | golang/nats-server/server/monitor.go:1356 | MISSING | — | No .NET equivalent |
| `MQTTOptsVarz` struct | golang/nats-server/server/monitor.go:1365 | PARTIAL | src/NATS.Server/Monitoring/Varz.cs:350 | Missing `TLSOCSPPeerVerify`, `TLSCertNotAfter` fields |
| `WebsocketOptsVarz` struct | golang/nats-server/server/monitor.go:1381 | PARTIAL | src/NATS.Server/Monitoring/Varz.cs:387 | Go has many more fields (Advertise, NoAuthUser, JWTCookie, HandshakeTimeout, AuthTimeout, NoTLS, TLSMap, TLSPinnedCerts, SameOrigin, AllowedOrigins, Compression, TLSOCSPPeerVerify, TLSCertNotAfter); .NET only has Host, Port, TlsTimeout |
| `OCSPResponseCacheVarz` struct | golang/nats-server/server/monitor.go:1399 | MISSING | — | No .NET equivalent; OCSP cache monitoring not ported |
| `ProxiesOptsVarz` struct | golang/nats-server/server/monitor.go:1411 | MISSING | — | No .NET equivalent |
| `ProxyOptsVarz` struct | golang/nats-server/server/monitor.go:1416 | MISSING | — | No .NET equivalent |
| `VarzOptions` struct | golang/nats-server/server/monitor.go:1422 | PORTED | — | Empty struct, not needed in .NET since handler takes no options |
| `SlowConsumersStats` struct | golang/nats-server/server/monitor.go:1424 | PORTED | src/NATS.Server/Monitoring/Varz.cs:213 | All four fields present |
| `StaleConnectionStats` struct | golang/nats-server/server/monitor.go:1432 | PORTED | src/NATS.Server/Monitoring/Varz.cs:232 | All four fields present |
| `GatewayzOptions` struct | golang/nats-server/server/monitor.go:2004 | MISSING | — | .NET GatewayzHandler has no options; returns only counts |
| `Gatewayz` struct | golang/nats-server/server/monitor.go:2024 | PARTIAL | src/NATS.Server/Monitoring/GatewayzHandler.cs:12 | Go has full struct with OutboundGateways/InboundGateways maps; .NET returns anonymous count object |
| `RemoteGatewayz` struct | golang/nats-server/server/monitor.go:2036 | MISSING | — | No .NET equivalent |
| `AccountGatewayz` struct | golang/nats-server/server/monitor.go:2043 | MISSING | — | No .NET equivalent |
| `Leafz` struct | golang/nats-server/server/monitor.go:2348 | PARTIAL | src/NATS.Server/Monitoring/LeafzHandler.cs:12 | Go has full struct with NumLeafs and Leafs array; .NET returns anonymous count object |
| `LeafzOptions` struct | golang/nats-server/server/monitor.go:2356 | MISSING | — | No options struct; handler accepts no query params |
| `LeafInfo` struct | golang/nats-server/server/monitor.go:2364 | MISSING | — | No .NET equivalent for per-leafnode detail |
| `AccountStatz` struct | golang/nats-server/server/monitor.go:2471 | PARTIAL | src/NATS.Server/Monitoring/AccountzHandler.cs:27 | Go has `ID`, `Now`, `Accounts []*AccountStat`; .NET returns anonymous object with total counts |
| `AccountStatzOptions` struct | golang/nats-server/server/monitor.go:2478 | MISSING | — | No .NET equivalent; `?unused` query param not parsed |
| `AccountzOptions` struct | golang/nats-server/server/monitor.go:2646 | MISSING | — | `?acc=` query param not parsed for account detail |
| `ExtImport` struct | golang/nats-server/server/monitor.go:2662 | MISSING | — | JWT import monitoring detail not ported |
| `ExtExport` struct | golang/nats-server/server/monitor.go:2672 | MISSING | — | JWT export monitoring detail not ported |
| `ExtVrIssues` struct | golang/nats-server/server/monitor.go:2678 | MISSING | — | JWT validation result reporting not ported |
| `ExtMap` type | golang/nats-server/server/monitor.go:2684 | MISSING | — | Account subject mapping detail not ported |
| `AccountInfo` struct | golang/nats-server/server/monitor.go:2686 | MISSING | — | Detailed per-account monitoring not ported |
| `Accountz` struct | golang/nats-server/server/monitor.go:2710 | PARTIAL | src/NATS.Server/Monitoring/AccountzHandler.cs:14 | Go has ID, Now, SystemAccount, Accounts list, and Account detail; .NET returns simplified anonymous object |
| `JSzOptions` struct | golang/nats-server/server/monitor.go:2928 | PARTIAL | — | Most options not parsed from query string; handler builds static JSZ response |
| `HealthzOptions` struct | golang/nats-server/server/monitor.go:2943 | MISSING | — | .NET /healthz endpoint does not parse any options; returns static "ok" |
| `ProfilezOptions` struct | golang/nats-server/server/monitor.go:2956 | PARTIAL | src/NATS.Server/Monitoring/PprofHandler.cs:24 | Only `seconds` duration from CPU profile path; no `name` or `debug` level routing |
| `IpqueueszOptions` struct | golang/nats-server/server/monitor.go:2963 | MISSING | — | /ipqueuesz endpoint not implemented |
| `RaftzOptions` struct | golang/nats-server/server/monitor.go:2969 | MISSING | — | /raftz endpoint not implemented |
| `StreamDetail` struct | golang/nats-server/server/monitor.go:2975 | MISSING | — | JSZ stream detail not ported |
| `RaftGroupDetail` struct | golang/nats-server/server/monitor.go:2991 | MISSING | — | RAFT group detail not ported |
| `AccountDetail` struct | golang/nats-server/server/monitor.go:2996 | MISSING | — | JSZ per-account detail not ported |
| `MetaSnapshotStats` struct | golang/nats-server/server/monitor.go:3003 | MISSING | — | RAFT meta snapshot stats not ported |
| `MetaClusterInfo` struct | golang/nats-server/server/monitor.go:3011 | MISSING | — | RAFT meta cluster info not ported (referenced in JetStreamVarz.Meta) |
| `JSInfo` struct | golang/nats-server/server/monitor.go:3023 | PARTIAL | src/NATS.Server/Monitoring/JszHandler.cs:39 | Go JSInfo has JetStreamStats embedded, ID, Now, Disabled, Config, Limits, Streams, StreamsLeader, Consumers, ConsumersLeader, Messages, Bytes, Meta, AccountDetails, Total; .NET JszResponse has simplified fields with different JSON names (`api_total`/`api_errors` instead of nested `api`) |
| `HealthStatus` struct | golang/nats-server/server/monitor.go:3408 | MISSING | — | /healthz returns plain "ok" string, not the structured HealthStatus with Status/StatusCode/Error/Errors |
| `HealthzError` struct | golang/nats-server/server/monitor.go:3415 | MISSING | — | No .NET equivalent |
| `HealthZErrorType` type | golang/nats-server/server/monitor.go:3423 | MISSING | — | No .NET equivalent |
| `ExpvarzStatus` struct | golang/nats-server/server/monitor.go:4019 | MISSING | — | /expvarz endpoint not implemented |
| `ProfilezStatus` struct | golang/nats-server/server/monitor.go:4043 | MISSING | — | No structured response; CPU profile endpoint returns raw bytes |
| `RaftzGroup` struct | golang/nats-server/server/monitor.go:4086 | MISSING | — | /raftz endpoint not implemented |
| `RaftzGroupPeer` struct | golang/nats-server/server/monitor.go:4114 | MISSING | — | /raftz endpoint not implemented |
| `RaftzStatus` type | golang/nats-server/server/monitor.go:4121 | MISSING | — | /raftz endpoint not implemented |
| `IpqueueszStatusIPQ` struct | golang/nats-server/server/monitor.go:1162 | MISSING | — | /ipqueuesz endpoint not implemented |
| `IpqueueszStatus` type | golang/nats-server/server/monitor.go:1167 | MISSING | — | /ipqueuesz endpoint not implemented |
---
### monitor.go — Methods and Functions
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `newSubsDetailList()` | golang/nats-server/server/monitor.go:176 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:189 | Implemented inline in BuildConnInfo |
| `newSubsList()` | golang/nats-server/server/monitor.go:184 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:180 | Implemented inline in BuildConnInfo |
| `Server.Connz()` | golang/nats-server/server/monitor.go:193 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:12 | `ConnzHandler.HandleConnz()` |
| `ConnInfo.fill()` | golang/nats-server/server/monitor.go:556 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:144 | `BuildConnInfo()` static method |
| `createProxyInfo()` | golang/nats-server/server/monitor.go:609 | PARTIAL | src/NATS.Server/Monitoring/ConnzHandler.cs:174 | .NET sets `Proxy` to a plain string; no ProxyInfo struct |
| `makePeerCerts()` | golang/nats-server/server/monitor.go:616 | PARTIAL | src/NATS.Server/Monitoring/ConnzHandler.cs:170 | .NET only captures Subject, not SubjectPKISha256 or CertSha256 |
| `client.getRTT()` | golang/nats-server/server/monitor.go:629 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:321 | `FormatRtt()` formats from stored `client.Rtt` |
| `decodeBool()` | golang/nats-server/server/monitor.go:647 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:267 | Inline query param parsing in ParseQueryParams |
| `decodeUint64()` | golang/nats-server/server/monitor.go:661 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:291 | Inline query param parsing |
| `decodeInt()` | golang/nats-server/server/monitor.go:675 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:285 | Inline query param parsing |
| `decodeState()` | golang/nats-server/server/monitor.go:689 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:275 | State parsing in ParseQueryParams |
| `decodeSubs()` | golang/nats-server/server/monitor.go:709 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:266 | Subs + subs-detail parsing |
| `Server.HandleConnz()` | golang/nats-server/server/monitor.go:718 | PORTED | src/NATS.Server/Monitoring/MonitorServer.cs:70 | Mapped at `/connz` route |
| `Server.Routez()` | golang/nats-server/server/monitor.go:830 | PARTIAL | src/NATS.Server/Monitoring/RoutezHandler.cs:13 | Go returns full Routez struct with per-route RouteInfo; .NET returns only count |
| `Server.HandleRoutez()` | golang/nats-server/server/monitor.go:904 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:76 | Endpoint mapped, but subs/subscriptions_detail query params not parsed |
| `newSubDetail()` | golang/nats-server/server/monitor.go:973 | PORTED | src/NATS.Server/Monitoring/SubszHandler.cs:51 | Implemented inline |
| `newClientSubDetail()` | golang/nats-server/server/monitor.go:981 | PORTED | src/NATS.Server/Monitoring/ConnzHandler.cs:189 | Implemented inline |
| `Server.Subsz()` | golang/nats-server/server/monitor.go:992 | PORTED | src/NATS.Server/Monitoring/SubszHandler.cs:12 | `SubszHandler.HandleSubsz()` |
| `Server.HandleSubsz()` | golang/nats-server/server/monitor.go:1095 | PORTED | src/NATS.Server/Monitoring/MonitorServer.cs:91 | Mapped at `/subz` and `/subscriptionsz` |
| `Server.HandleStacksz()` | golang/nats-server/server/monitor.go:1142 | MISSING | — | No .NET /stacksz endpoint |
| `Server.Ipqueuesz()` | golang/nats-server/server/monitor.go:1169 | MISSING | — | No .NET /ipqueuesz endpoint |
| `Server.HandleIPQueuesz()` | golang/nats-server/server/monitor.go:1194 | MISSING | — | No .NET /ipqueuesz endpoint |
| `Server.updateJszVarz()` | golang/nats-server/server/monitor.go:1555 | PARTIAL | src/NATS.Server/Monitoring/VarzHandler.cs:125 | Partially in VarzHandler; missing Meta/Limits/Config update logic |
| `Server.Varz()` | golang/nats-server/server/monitor.go:1580 | PORTED | src/NATS.Server/Monitoring/VarzHandler.cs:30 | `VarzHandler.HandleVarzAsync()` |
| `Server.createVarz()` | golang/nats-server/server/monitor.go:1603 | PORTED | src/NATS.Server/Monitoring/VarzHandler.cs:67 | Logic inline in HandleVarzAsync |
| `urlsToStrings()` | golang/nats-server/server/monitor.go:1756 | NOT_APPLICABLE | — | Helper for URL slice conversion; .NET uses LINQ Select |
| `Server.updateVarzConfigReloadableFields()` | golang/nats-server/server/monitor.go:1768 | PARTIAL | src/NATS.Server/Monitoring/VarzHandler.cs:67 | Config fields set at construction; no reload tracking; missing TLSCertNotAfter for cluster/gateway/leafnode/mqtt/websocket |
| `Server.updateVarzRuntimeFields()` | golang/nats-server/server/monitor.go:1840 | PORTED | src/NATS.Server/Monitoring/VarzHandler.cs:67 | Runtime fields computed each request |
| `getPinnedCertsAsSlice()` | golang/nats-server/server/monitor.go:1825 | PORTED | src/NATS.Server/Monitoring/VarzHandler.cs:173 | Present for MQTT; no websocket equivalent |
| `Server.HandleVarz()` | golang/nats-server/server/monitor.go:1946 | PORTED | src/NATS.Server/Monitoring/MonitorServer.cs:64 | Mapped at `/varz` |
| `Server.HandleRoot()` | golang/nats-server/server/monitor.go:1480 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:47 | .NET returns JSON array of endpoint names; Go returns HTML page with links and NATS logo |
| `myUptime()` | golang/nats-server/server/monitor.go:1440 | PORTED | src/NATS.Server/Monitoring/VarzHandler.cs:183 | `FormatUptime()` and `FormatDuration()` — same format logic |
| `tlsCertNotAfter()` | golang/nats-server/server/monitor.go:1463 | PARTIAL | src/NATS.Server/Monitoring/VarzHandler.cs:53 | .NET only reads cert for client TLS; Go reads for all subsystem TLS configs |
| `Server.Gatewayz()` | golang/nats-server/server/monitor.go:2054 | PARTIAL | src/NATS.Server/Monitoring/GatewayzHandler.cs:13 | Go returns full outbound/inbound gateway maps; .NET returns only counts |
| `getMonitorGWOptions()` | golang/nats-server/server/monitor.go:2090 | MISSING | — | No options parsing in .NET GatewayzHandler |
| `Server.createOutboundsRemoteGatewayz()` | golang/nats-server/server/monitor.go:2108 | MISSING | — | No outbound gateway detail |
| `createOutboundRemoteGatewayz()` | golang/nats-server/server/monitor.go:2138 | MISSING | — | No outbound gateway detail |
| `createOutboundAccountsGatewayz()` | golang/nats-server/server/monitor.go:2163 | MISSING | — | No gateway account interest detail |
| `createAccountOutboundGatewayz()` | golang/nats-server/server/monitor.go:2192 | MISSING | — | No gateway account interest detail |
| `Server.createInboundsRemoteGatewayz()` | golang/nats-server/server/monitor.go:2233 | MISSING | — | No inbound gateway detail |
| `createInboundAccountsGatewayz()` | golang/nats-server/server/monitor.go:2265 | MISSING | — | No gateway account interest detail |
| `createInboundAccountGatewayz()` | golang/nats-server/server/monitor.go:2292 | MISSING | — | No gateway account interest detail |
| `Server.HandleGatewayz()` | golang/nats-server/server/monitor.go:2306 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:81 | Endpoint mapped; query params (gw_name, accs, acc_name, subs) not parsed |
| `Server.Leafz()` | golang/nats-server/server/monitor.go:2384 | PARTIAL | src/NATS.Server/Monitoring/LeafzHandler.cs:13 | Go returns full LeafInfo per leafnode; .NET returns only count |
| `Server.HandleLeafz()` | golang/nats-server/server/monitor.go:2445 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:86 | Endpoint mapped; `subs` and `acc` query params not parsed |
| `Server.AccountStatz()` | golang/nats-server/server/monitor.go:2484 | PARTIAL | src/NATS.Server/Monitoring/AccountzHandler.cs:27 | .NET BuildStats() returns totals; no per-account `AccountStat` detail or `accounts` / `include_unused` filter |
| `Server.HandleAccountStatz()` | golang/nats-server/server/monitor.go:2516 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:106 | Endpoint mapped at `/accstatz`; `unused` query param not parsed |
| `ResponseHandler()` | golang/nats-server/server/monitor.go:2542 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:47 | .NET uses ASP.NET Core Results.Ok(); no JSONP callback support |
| `handleResponse()` | golang/nats-server/server/monitor.go:2548 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:47 | No custom status code support from handler level; no JSONP support |
| `ClosedState.String()` | golang/nats-server/server/monitor.go:2565 | PORTED | src/NATS.Server/Monitoring/Connz.cs:511 | `ClosedReasonHelper.ToReasonString()` |
| `Server.HandleAccountz()` | golang/nats-server/server/monitor.go:2718 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:101 | Endpoint mapped; `acc` query param for account detail not parsed |
| `Server.Accountz()` | golang/nats-server/server/monitor.go:2735 | PARTIAL | src/NATS.Server/Monitoring/AccountzHandler.cs:14 | Go returns full Accountz with optional AccountInfo detail; .NET returns simplified list |
| `newExtServiceLatency()` | golang/nats-server/server/monitor.go:2652 | MISSING | — | JWT service latency detail not ported |
| `newExtImport()` | golang/nats-server/server/monitor.go:2759 | MISSING | — | JWT import detail not ported |
| `Server.accountInfo()` | golang/nats-server/server/monitor.go:2786 | MISSING | — | Full account info with exports/imports/mappings/JWT not ported |
| `Server.JszAccount()` | golang/nats-server/server/monitor.go:3142 | MISSING | — | Per-account JSZ detail not ported |
| `Server.raftNodeToClusterInfo()` | golang/nats-server/server/monitor.go:3162 | MISSING | — | RAFT cluster info helper not ported |
| `Server.Jsz()` | golang/nats-server/server/monitor.go:3180 | PARTIAL | src/NATS.Server/Monitoring/JszHandler.cs:16 | Go Jsz() has full JSInfo with accounts/streams/consumers/meta; .NET JszHandler.Build() returns simplified flat fields |
| `Server.accountDetail()` | golang/nats-server/server/monitor.go:3041 | MISSING | — | Per-account stream/consumer detail not ported |
| `Server.HandleJsz()` | golang/nats-server/server/monitor.go:3334 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:111 | Endpoint mapped; all query params (accounts, streams, consumers, config, offset, limit, leader-only, raft, stream-leader-only) not parsed |
| `Server.HandleHealthz()` | golang/nats-server/server/monitor.go:3478 | PARTIAL | src/NATS.Server/Monitoring/MonitorServer.cs:59 | Endpoint mapped; Go does JetStream-aware checks; .NET returns static "ok" with no JS checks |
| `Server.healthz()` | golang/nats-server/server/monitor.go:3537 | MISSING | — | Full healthz logic with JS stream/consumer recovery checks not ported |
| `Server.Healthz()` | golang/nats-server/server/monitor.go:4014 | MISSING | — | Public Healthz() method not ported |
| `Server.expvarz()` | golang/nats-server/server/monitor.go:4024 | MISSING | — | /expvarz endpoint not implemented |
| `Server.profilez()` | golang/nats-server/server/monitor.go:4048 | PARTIAL | src/NATS.Server/Monitoring/PprofHandler.cs:24 | .NET CaptureCpuProfile() captures process metrics as JSON; Go uses pprof binary format |
| `Server.HandleRaftz()` | golang/nats-server/server/monitor.go:4123 | MISSING | — | /raftz endpoint not implemented |
| `Server.Raftz()` | golang/nats-server/server/monitor.go:4145 | MISSING | — | RAFT node status reporting not ported |
---
## 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/Monitoring/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Monitoring/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated: 63 PORTED, 42 PARTIAL, 61 MISSING, 3 NOT_APPLICABLE, 0 DEFERRED (169 total rows) | claude-sonnet-4-6 |

408
gaps/mqtt.md Normal file
View File

@@ -0,0 +1,408 @@
# MQTT — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **MQTT** 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 MQTT
- MQTT runs on top of NATS — MQTT topics map to NATS subjects.
- Session state persists across reconnections (stored in JetStream streams).
- QoS 0 (at most once), QoS 1 (at least once), QoS 2 (exactly once — limited support).
- Retained messages stored as JetStream messages.
- MQTT connections use `ClientKind = MQTT` and have a different parser path.
---
## Go Reference Files (Source)
- `golang/nats-server/server/mqtt.go` — MQTT protocol support (~5,882 lines). MQTT 3.1.1 and 5.0. Session state, QoS levels, retained messages, will messages.
## Go Reference Files (Tests)
- `golang/nats-server/server/mqtt_test.go`
- `golang/nats-server/server/mqtt_ex_test_test.go`
- `golang/nats-server/server/mqtt_ex_bench_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Mqtt/` (all files)
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Mqtt/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### golang/nats-server/server/mqtt.go
#### Constants and Protocol Definitions (lines 41-199)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| mqttPacket* constants | mqtt.go:42-57 | PORTED | src/NATS.Server/Mqtt/MqttPacketReader.cs:3-15 | MqttControlPacketType enum covers the packet types |
| mqttProtoLevel | mqtt.go:59 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:87-88 | Checked in ParseConnect |
| mqttConnFlag* constants | mqtt.go:62-68 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:96-104 | Decoded inline in ParseConnect |
| mqttPubFlag* constants | mqtt.go:71-75 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:161-163 | Decoded inline in ParsePublish |
| mqttSubscribeFlags | mqtt.go:78 | MISSING | | Subscribe flag validation not implemented in binary parser |
| mqttConnAckRC* constants | mqtt.go:85-91 | MISSING | | ConnAck return code constants not defined |
| mqttMaxPayloadSize | mqtt.go:94 | MISSING | | Max payload size constant not defined |
| mqttTopicLevelSep, mqttSingleLevelWC, mqttMultiLevelWC | mqtt.go:97-100 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:254-258 | Used in TranslateFilterToNatsSubject |
| mqttMultiLevelSidSuffix | mqtt.go:105 | MISSING | | Multi-level SID suffix for '#' wildcard not implemented |
| mqttPrefix, mqttSubPrefix | mqtt.go:108-113 | MISSING | | MQTT internal subject prefixes not defined |
| mqttStreamName, mqttStreamSubjectPrefix | mqtt.go:116-117 | MISSING | | JetStream stream naming not implemented |
| mqttRetainedMsgsStreamName | mqtt.go:120-121 | MISSING | | Retained messages stream not implemented |
| mqttSessStreamName | mqtt.go:124-125 | MISSING | | Session stream naming not implemented |
| mqttQoS2IncomingMsgsStreamName | mqtt.go:131-132 | MISSING | | QoS2 incoming stream not implemented |
| mqttOutStreamName, mqttPubRel* | mqtt.go:135-139 | MISSING | | PUBREL stream/subject not implemented |
| mqttDefaultAckWait | mqtt.go:145 | MISSING | | Default ack wait not defined (Go: 30s) |
| mqttDefaultMaxAckPending | mqtt.go:149 | PARTIAL | src/NATS.Server/Mqtt/MqttFlowController.cs:15 | Default 1024 matches Go, but not wired to JetStream |
| mqttMaxAckTotalLimit | mqtt.go:153 | MISSING | | Max ack total limit (0xFFFF) not defined |
| mqttJSA* token constants | mqtt.go:156-177 | MISSING | | JetStream API reply subject tokens not implemented |
| mqttSessFlappingJailDur | mqtt.go:182 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:96-106 | Flap detection exists but uses different default timing |
| sparkb* constants | mqtt.go:201-211 | MISSING | | Sparkplug B protocol constants not implemented |
| mqttNatsHeader* constants | mqtt.go:474-492 | MISSING | | NATS header names for MQTT message encoding not defined |
#### Core Types (lines 246-498)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| srvMQTT | mqtt.go:246 | PARTIAL | src/NATS.Server/Mqtt/MqttListener.cs:8-12 | Listener exists but no integration with Server struct or authOverride |
| mqttSessionManager | mqtt.go:253-256 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:67 | Session store exists but not multi-account |
| mqttAccountSessionManager | mqtt.go:258-270 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:67 | Single-account only, no JetStream backing, no retained msg sublist, no session hash |
| mqttJSA | mqtt.go:277-289 | MISSING | | JetStream API helper struct not implemented |
| mqttJSPubMsg | mqtt.go:291-296 | MISSING | | JS publish message type not implemented |
| mqttRetMsgDel | mqtt.go:298-301 | MISSING | | Retained message delete notification type not implemented |
| mqttSession | mqtt.go:303-344 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:48-60 | MqttSessionData covers basic fields but missing pendingPublish/pendingPubRel maps, cpending, last_pi, maxp, tmaxack, JetStream consumer tracking |
| mqttPersistedSession | mqtt.go:346-353 | MISSING | | Persisted session JSON format not implemented |
| mqttRetainedMsg | mqtt.go:355-364 | PARTIAL | src/NATS.Server/Mqtt/MqttRetainedStore.cs:14 | MqttRetainedMessage exists but missing Origin, Flags, Source, Topic fields; no cache TTL |
| mqttRetainedMsgRef | mqtt.go:366-369 | MISSING | | Retained message reference (sseq + sub) not implemented |
| mqttSub | mqtt.go:375-391 | MISSING | | MQTT subscription metadata (qos, jsDur, prm, reserved) not implemented |
| mqtt (client struct) | mqtt.go:393-408 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:6-16 | MqttConnection exists but missing reader/writer, asm, sess, cid, rejectQoS2Pub, downgradeQoS2Sub |
| mqttPending | mqtt.go:410-414 | PARTIAL | src/NATS.Server/Mqtt/MqttQoS1Tracker.cs:89-96 | QoS1PendingMessage exists but missing sseq, jsAckSubject, jsDur fields |
| mqttConnectProto | mqtt.go:416-420 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:14-25 | MqttConnectInfo covers most fields but is a record struct, not mutable; missing rd (read deadline) |
| mqttIOReader | mqtt.go:422-425 | NOT_APPLICABLE | | Go-specific IO interface; .NET uses NetworkStream directly |
| mqttReader | mqtt.go:427-433 | PARTIAL | src/NATS.Server/Mqtt/MqttPacketReader.cs:23-41 | MqttPacketReader handles fixed header; missing streaming buffer/partial packet support (pbuf, pstart) |
| mqttWriter | mqtt.go:435-437 | PORTED | src/NATS.Server/Mqtt/MqttPacketWriter.cs:3-38 | MqttPacketWriter covers write operations |
| mqttWill | mqtt.go:439-446 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:35-42 | WillMessage exists but missing subject, mapped byte arrays; topic is string not bytes |
| mqttFilter | mqtt.go:448-453 | MISSING | | MQTT filter struct (filter, qos, ttopic) not implemented as a standalone type |
| mqttPublish | mqtt.go:455-463 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:31-37 | MqttPublishInfo covers most fields but missing subject, mapped byte arrays |
| mqttParsedPublishNATSHeader | mqtt.go:495-499 | MISSING | | Parsed NATS header struct for MQTT messages not implemented |
#### Server Lifecycle Functions (lines 501-722)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Server.startMQTT | mqtt.go:501 | PARTIAL | src/NATS.Server/Mqtt/MqttListener.cs:27-38 | MqttListener.StartAsync exists but no TLS, no integration with Server lifecycle |
| Server.createMQTTClient | mqtt.go:542 | PARTIAL | src/NATS.Server/Mqtt/MqttListener.cs:130-158 | AcceptLoopAsync creates MqttConnection but no TLS handshake, no auth timer, no read/write loops as goroutines |
| Server.mqttConfigAuth | mqtt.go:666 | MISSING | | Auth configuration override not implemented |
| validateMQTTOptions | mqtt.go:673 | MISSING | | MQTT option validation not implemented |
| client.isMqtt | mqtt.go:726 | NOT_APPLICABLE | | .NET uses MqttConnection as a distinct type rather than a flag on client |
| client.getMQTTClientID | mqtt.go:733 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:13 | _clientId field exists but no public accessor |
#### Protocol Parsing (lines 742-959)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| client.mqttParse | mqtt.go:742 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:17-66 | RunAsync handles basic text-based protocol; missing binary MQTT packet dispatch, partial packet handling, read deadline management |
| client.mqttTraceMsg | mqtt.go:961 | MISSING | | MQTT message tracing not implemented |
#### Connection Close and Session Cleanup (lines 976-1107)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Server.mqttHandleClosedClient | mqtt.go:976 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:71-77 | DisposeAsync calls Unregister but no session cleanup, no will message handling, no clean session logic |
| Server.mqttUpdateMaxAckPending | mqtt.go:1026 | MISSING | | Max ack pending update across sessions not implemented |
| Server.mqttGetJSAForAccount | mqtt.go:1048 | MISSING | | JetStream API access per account not implemented |
| Server.mqttStoreQoSMsgForAccountOnNewSubject | mqtt.go:1065 | MISSING | | QoS message re-store on subject change not implemented |
| mqttParsePublishNATSHeader | mqtt.go:1080 | MISSING | | NATS header parsing for MQTT messages not implemented |
| mqttParsePubRelNATSHeader | mqtt.go:1096 | MISSING | | PUBREL NATS header parsing not implemented |
#### Account Session Manager (lines 1112-2389)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Server.getOrCreateMQTTAccountSessionManager | mqtt.go:1112 | MISSING | | Per-account session manager creation not implemented |
| Server.mqttCreateAccountSessionManager | mqtt.go:1152 | MISSING | | JetStream stream/consumer creation for MQTT not implemented (350+ lines) |
| Server.mqttDetermineReplicas | mqtt.go:1502 | MISSING | | Replica count determination for cluster not implemented |
| mqttJSA.newRequest | mqtt.go:1533 | MISSING | | JetStream API request helper not implemented |
| mqttJSA.prefixDomain | mqtt.go:1537 | MISSING | | Domain prefix for JS subjects not implemented |
| mqttJSA.newRequestEx | mqtt.go:1547 | MISSING | | Extended JS API request not implemented |
| mqttJSA.newRequestExMulti | mqtt.go:1566 | MISSING | | Multi-request JS API helper not implemented |
| mqttJSA.sendAck | mqtt.go:1649 | MISSING | | JS ack sending not implemented |
| mqttJSA.sendMsg | mqtt.go:1654 | MISSING | | JS message sending not implemented |
| mqttJSA.createEphemeralConsumer | mqtt.go:1663 | MISSING | | Ephemeral consumer creation not implemented |
| mqttJSA.createDurableConsumer | mqtt.go:1677 | MISSING | | Durable consumer creation not implemented |
| mqttJSA.deleteConsumer | mqtt.go:1692 | MISSING | | Consumer deletion not implemented |
| mqttJSA.createStream | mqtt.go:1707 | MISSING | | Stream creation not implemented |
| mqttJSA.updateStream | mqtt.go:1720 | MISSING | | Stream update not implemented |
| mqttJSA.lookupStream | mqtt.go:1733 | MISSING | | Stream lookup not implemented |
| mqttJSA.deleteStream | mqtt.go:1742 | MISSING | | Stream deletion not implemented |
| mqttJSA.loadLastMsgFor | mqtt.go:1751 | MISSING | | Last message loading not implemented |
| mqttJSA.loadLastMsgForMulti | mqtt.go:1765 | MISSING | | Multi-subject last message loading not implemented |
| mqttJSA.loadNextMsgFor | mqtt.go:1789 | MISSING | | Next message loading not implemented |
| mqttJSA.loadMsg | mqtt.go:1803 | MISSING | | Message loading by seq not implemented |
| mqttJSA.storeMsgNoWait | mqtt.go:1817 | MISSING | | No-wait message store not implemented |
| mqttJSA.storeMsg | mqtt.go:1825 | MISSING | | Message store not implemented |
| mqttJSA.storeSessionMsg | mqtt.go:1834 | MISSING | | Session message store not implemented |
| mqttJSA.loadSessionMsg | mqtt.go:1848 | MISSING | | Session message load not implemented |
| mqttJSA.deleteMsg | mqtt.go:1853 | MISSING | | Message deletion not implemented |
| isErrorOtherThan | mqtt.go:1879 | NOT_APPLICABLE | | Go error-checking helper; .NET uses exception types |
| mqttAccountSessionManager.processJSAPIReplies | mqtt.go:1886 | MISSING | | JS API reply processing callback not implemented |
| mqttAccountSessionManager.processRetainedMsg | mqtt.go:1971 | MISSING | | Retained message processing callback not implemented |
| mqttAccountSessionManager.processRetainedMsgDel | mqtt.go:2006 | MISSING | | Retained message delete callback not implemented |
| mqttAccountSessionManager.processSessionPersist | mqtt.go:2030 | MISSING | | Session persist callback not implemented |
| mqttAccountSessionManager.addSessToFlappers | mqtt.go:2092 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:251 | TrackConnectDisconnect exists but uses different data structures |
| mqttAccountSessionManager.removeSessFromFlappers | mqtt.go:2116 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:322 | ClearFlapperState exists |
| mqttAccountSessionManager.createSubscription | mqtt.go:2124 | MISSING | | Internal subscription creation not implemented |
| mqttAccountSessionManager.cleanupRetainedMessageCache | mqtt.go:2140 | MISSING | | Retained message cache cleanup timer not implemented |
| mqttAccountSessionManager.sendJSAPIrequests | mqtt.go:2174 | MISSING | | JS API request sending goroutine not implemented |
| mqttAccountSessionManager.addRetainedMsg | mqtt.go:2269 | PARTIAL | src/NATS.Server/Mqtt/MqttRetainedStore.cs:51-62 | SetRetained exists but no sseq tracking, no sublist insertion |
| mqttAccountSessionManager.removeRetainedMsg | mqtt.go:2304 | PARTIAL | src/NATS.Server/Mqtt/MqttRetainedStore.cs:53-55 | SetRetained with empty payload removes, but no seq/sublist management |
| mqttAccountSessionManager.lockSession | mqtt.go:2326 | MISSING | | Session locking mechanism not implemented |
| mqttAccountSessionManager.unlockSession | mqtt.go:2345 | MISSING | | Session unlocking mechanism not implemented |
| mqttAccountSessionManager.addSession | mqtt.go:2356 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:192-196 | SaveSession exists but no hash-based lookup |
| mqttAccountSessionManager.removeSession | mqtt.go:2372 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:209-210 | DeleteSession exists but no hash-based removal |
#### Subscription Processing (lines 2389-2730)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| mqttSession.processQOS12Sub | mqtt.go:2389 | MISSING | | QoS 1/2 subscription processing with JS consumer not implemented |
| mqttSession.processSub | mqtt.go:2396 | PARTIAL | src/NATS.Server/Mqtt/MqttListener.cs:40-43 | RegisterSubscription exists but extremely simplified; no QoS handling, no JS consumer, no retained message delivery |
| mqttAccountSessionManager.processSubs | mqtt.go:2463 | MISSING | | Bulk subscription processing with JS consumers not implemented (180 lines) |
| mqttAccountSessionManager.serializeRetainedMsgsForSub | mqtt.go:2642 | MISSING | | Retained message serialization for new subscriptions not implemented |
| mqttAccountSessionManager.addRetainedSubjectsForSubject | mqtt.go:2700 | MISSING | | Retained subject matching via sublist not implemented |
| mqttAccountSessionManager.loadRetainedMessages | mqtt.go:2730 | MISSING | | Retained message loading from JetStream not implemented |
#### Retained Message Encoding/Decoding (lines 2798-2960)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| mqttEncodeRetainedMessage | mqtt.go:2798 | MISSING | | Retained message NATS encoding with headers not implemented |
| mqttSliceHeaders | mqtt.go:2855 | MISSING | | NATS header slicing utility not implemented |
| mqttDecodeRetainedMessage | mqtt.go:2908 | MISSING | | Retained message NATS decoding not implemented |
#### Session CRUD (lines 2972-3190)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| mqttAccountSessionManager.createOrRestoreSession | mqtt.go:2972 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:370-397 | ConnectAsync handles basic create/restore but no JetStream persistence, no hash-based lookup |
| mqttAccountSessionManager.deleteRetainedMsg | mqtt.go:3011 | MISSING | | JetStream retained message deletion not implemented |
| mqttAccountSessionManager.notifyRetainedMsgDeleted | mqtt.go:3018 | MISSING | | Retained message delete notification not implemented |
| mqttAccountSessionManager.transferUniqueSessStreamsToMuxed | mqtt.go:3031 | MISSING | | Session stream migration not implemented |
| mqttAccountSessionManager.transferRetainedToPerKeySubjectStream | mqtt.go:3089 | MISSING | | Retained message stream migration not implemented |
| mqttAccountSessionManager.getCachedRetainedMsg | mqtt.go:3146 | MISSING | | Retained message cache get not implemented |
| mqttAccountSessionManager.setCachedRetainedMsg | mqtt.go:3169 | MISSING | | Retained message cache set not implemented |
| mqttSessionCreate | mqtt.go:3193 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:48-60 | MqttSessionData creation exists but missing maxp, pubRelSubject, idHash |
| mqttSession.save | mqtt.go:3215 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:417-425 | SaveSessionAsync exists but no expected-last-sequence header, simplified JSON |
| mqttSession.clear | mqtt.go:3259 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:209-210 | DeleteSession exists but no JetStream consumer cleanup |
| mqttSession.update | mqtt.go:3316 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:402-411 | AddSubscription exists but no filter-level granularity, no auto-persist |
| mqttSession.bumpPI | mqtt.go:3345 | PARTIAL | src/NATS.Server/Mqtt/MqttQoS1Tracker.cs:75-83 | GetNextPacketId exists but simpler (no collision check with pendingPubRel) |
| mqttSession.trackPublishRetained | mqtt.go:3375 | MISSING | | Retained message publish tracking not implemented |
| mqttSession.trackPublish | mqtt.go:3400 | PARTIAL | src/NATS.Server/Mqtt/MqttQoS1Tracker.cs:27-39 | Register exists but no JS ack subject tracking, no cpending map |
| mqttSession.untrackPublish | mqtt.go:3478 | PARTIAL | src/NATS.Server/Mqtt/MqttQoS1Tracker.cs:45-48 | Acknowledge exists but does not return JS ack subject |
| mqttSession.trackAsPubRel | mqtt.go:3503 | MISSING | | PUBREL tracking not implemented |
| mqttSession.untrackPubRel | mqtt.go:3542 | MISSING | | PUBREL untracking not implemented |
| mqttSession.deleteConsumer | mqtt.go:3562 | MISSING | | JS consumer deletion from session not implemented |
#### CONNECT Processing (lines 3576-3988)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| client.mqttParseConnect | mqtt.go:3576 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:70-140 | ParseConnect handles binary parsing well, but missing protocol level validation (rejects non-4), QoS2 will rejection, UTF-8 validation, subject mapping |
| client.mqttConnectTrace | mqtt.go:3753 | MISSING | | Connect trace formatting not implemented |
| Server.mqttProcessConnect | mqtt.go:3792 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:34-48 | Connect handling exists but extremely simplified; no account session manager, no session restore, no flapper detection, no existing client eviction, no JetStream integration |
| client.mqttEnqueueConnAck | mqtt.go:3976 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:45 | Writes "CONNACK" text; missing binary CONNACK with session-present flag and return code |
| Server.mqttHandleWill | mqtt.go:3990 | PARTIAL | src/NATS.Server/Mqtt/MqttSessionStore.cs:164-178 | PublishWillMessage exists but not integrated into connection close flow |
#### PUBLISH Processing (lines 4023-4560)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| client.mqttParsePub | mqtt.go:4023 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:155-185 | ParsePublish handles basic parsing; missing topic-to-subject conversion, subject mapping, empty topic validation |
| mqttPubTrace | mqtt.go:4096 | MISSING | | Publish trace formatting not implemented |
| mqttComputeNatsMsgSize | mqtt.go:4113 | MISSING | | NATS message size computation not implemented |
| mqttNewDeliverableMessage | mqtt.go:4139 | MISSING | | NATS deliverable message composition not implemented |
| mqttNewDeliverablePubRel | mqtt.go:4183 | MISSING | | PUBREL deliverable message composition not implemented |
| Server.mqttProcessPub | mqtt.go:4201 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:53-63 | Basic QoS0/QoS1 publish exists; missing max payload check, QoS2 flow, JetStream message storage |
| Server.mqttInitiateMsgDelivery | mqtt.go:4249 | PARTIAL | src/NATS.Server/Mqtt/MqttListener.cs:46-58 | PublishAsync delivers to subscribers; missing NATS header encoding, permission check, JetStream storage for QoS1+ |
| Server.mqttStoreQoS2MsgOnce | mqtt.go:4295 | MISSING | | QoS2 message deduplication and store not implemented |
| client.mqttQoS2InternalSubject | mqtt.go:4318 | MISSING | | QoS2 internal subject composition not implemented |
| Server.mqttProcessPubRel | mqtt.go:4326 | PARTIAL | src/NATS.Server/Mqtt/MqttRetainedStore.cs:264 | ProcessPubRel exists in QoS2StateMachine but not wired to JetStream or message delivery |
| client.mqttHandlePubRetain | mqtt.go:4366 | PARTIAL | src/NATS.Server/Mqtt/MqttRetainedStore.cs:51-62 | SetRetained handles basic store/clear but no Sparkplug B, no JetStream persistence, no network notification |
| Server.mqttCheckPubRetainedPerms | mqtt.go:4471 | MISSING | | Retained message permission checking not implemented |
| generatePubPerms | mqtt.go:4564 | MISSING | | Publish permission generation not implemented |
| pubAllowed | mqtt.go:4595 | MISSING | | Publish permission checking not implemented |
| client.mqttEnqueuePubResponse | mqtt.go:4609 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:58 | Writes "PUBACK" text for QoS1; missing binary PUBACK/PUBREC/PUBREL/PUBCOMP encoding |
| mqttParsePIPacket | mqtt.go:4641 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:316-323 | ReadUInt16BigEndian reads packet ID but no zero-check wrapper |
| client.mqttProcessPublishReceived | mqtt.go:4656 | MISSING | | PUBACK/PUBREC processing with JS ack not implemented |
| client.mqttProcessPubAck | mqtt.go:4691 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:62-63 | AckPendingPublish exists but no JS ack, no PUBREL tracking |
| client.mqttProcessPubRec | mqtt.go:4695 | PARTIAL | src/NATS.Server/Mqtt/MqttRetainedStore.cs:248 | ProcessPubRec exists in QoS2StateMachine but not wired to session or JetStream |
| client.mqttProcessPubComp | mqtt.go:4700 | PARTIAL | src/NATS.Server/Mqtt/MqttRetainedStore.cs:281 | ProcessPubComp exists in QoS2StateMachine but not wired to session or JetStream |
| mqttGetQoS | mqtt.go:4720 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:162 | Inline in ParsePublish |
| mqttIsRetained | mqtt.go:4724 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:163 | Inline in ParsePublish |
| sparkbParseBirthDeathTopic | mqtt.go:4728 | MISSING | | Sparkplug B birth/death topic parsing not implemented |
#### SUBSCRIBE/UNSUBSCRIBE Processing (lines 4760-5557)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| client.mqttParseSubs | mqtt.go:4760 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:197-217 | ParseSubscribe exists but no flag validation, no UTF-8 validation |
| client.mqttParseSubsOrUnsubs | mqtt.go:4764 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:197-217 | Combined parse exists only for subscribe; no unsubscribe binary parser |
| mqttSubscribeTrace | mqtt.go:4826 | MISSING | | Subscribe trace formatting not implemented |
| mqttDeliverMsgCbQoS0 | mqtt.go:4863 | MISSING | | QoS0 delivery callback not implemented (71 lines; core delivery logic) |
| mqttDeliverMsgCbQoS12 | mqtt.go:4934 | MISSING | | QoS1/2 delivery callback not implemented (70+ lines; core delivery logic) |
| mqttDeliverPubRelCb | mqtt.go:5004 | MISSING | | PUBREL delivery callback not implemented |
| mqttMustIgnoreForReservedSub | mqtt.go:5038 | MISSING | | Reserved subject ignore check not implemented |
| isMQTTReservedSubscription | mqtt.go:5047 | MISSING | | Reserved subscription check not implemented |
| sparkbReplaceDeathTimestamp | mqtt.go:5058 | MISSING | | Sparkplug B death timestamp replacement not implemented |
| client.mqttEnqueuePublishMsgTo | mqtt.go:5107 | MISSING | | MQTT PUBLISH message serialization and enqueue not implemented |
| mqttWriter.WritePublishHeader | mqtt.go:5156 | MISSING | | PUBLISH header serialization not implemented (MqttPacketWriter has generic Write but not PUBLISH-specific) |
| mqttMakePublishHeader | mqtt.go:5184 | MISSING | | PUBLISH header creation not implemented |
| client.mqttProcessSubs | mqtt.go:5204 | PARTIAL | src/NATS.Server/Mqtt/MqttListener.cs:40-43 | RegisterSubscription exists but no session locking, no JS consumer, no QoS handling |
| mqttSession.cleanupFailedSub | mqtt.go:5224 | MISSING | | Failed subscription cleanup not implemented |
| mqttSession.ensurePubRelConsumerSubscription | mqtt.go:5238 | MISSING | | PUBREL consumer subscription setup not implemented (90 lines) |
| mqttSession.processJSConsumer | mqtt.go:5327 | MISSING | | JS consumer creation/restoration for subscription not implemented (100+ lines) |
| client.mqttSendRetainedMsgsToNewSubs | mqtt.go:5435 | PARTIAL | src/NATS.Server/Mqtt/MqttRetainedStore.cs:98-104 | DeliverRetainedOnSubscribe exists but not integrated into connection flow |
| client.mqttEnqueueSubAck | mqtt.go:5449 | PARTIAL | src/NATS.Server/Mqtt/MqttConnection.cs:51 | Writes "SUBACK" text; missing binary SUBACK with per-filter QoS return codes |
| client.mqttParseUnsubs | mqtt.go:5469 | MISSING | | Binary UNSUBSCRIBE packet parsing not implemented |
| client.mqttProcessUnsubs | mqtt.go:5480 | MISSING | | UNSUBSCRIBE processing with JS consumer cleanup not implemented |
| client.mqttEnqueueUnsubAck | mqtt.go:5531 | MISSING | | Binary UNSUBACK packet encoding not implemented |
| mqttUnsubscribeTrace | mqtt.go:5541 | MISSING | | Unsubscribe trace formatting not implemented |
#### PING/PONG (lines 5565-5576)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| client.mqttEnqueuePingResp | mqtt.go:5565 | MISSING | | Binary PINGRESP encoding not implemented (text protocol only has no PING support) |
#### Trace and Error Helpers (lines 5577-5582)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| errOrTrace | mqtt.go:5577 | NOT_APPLICABLE | | Go-specific trace helper; .NET uses ILogger |
#### Subject/Topic Conversion (lines 5592-5739)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| mqttTopicToNATSPubSubject | mqtt.go:5592 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:246-264 | TranslateFilterToNatsSubject exists but uses simple char-by-char replace; missing dot-to-double-slash, empty level handling, space rejection, leading/trailing slash handling |
| mqttFilterToNATSSubject | mqtt.go:5598 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:246-264 | Same as above; wildcard-ok flag not separated |
| mqttToNATSSubjectConversion | mqtt.go:5619 | PARTIAL | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:246-264 | Core conversion exists but missing edge cases: '.' to '//', leading '/' to '/.', trailing '/' to './', empty levels to './', space error |
| natsSubjectStrToMQTTTopic | mqtt.go:5694 | MISSING | | NATS subject to MQTT topic (string variant) not implemented |
| natsSubjectToMQTTTopic | mqtt.go:5698 | MISSING | | NATS subject to MQTT topic (byte variant) not implemented |
| mqttNeedSubForLevelUp | mqtt.go:5730 | MISSING | | Level-up subscription check for '#' wildcard not implemented |
#### Reader Functions (lines 5747-5842)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| mqttReader.reset | mqtt.go:5747 | MISSING | | Streaming reader reset with partial buffer not implemented |
| mqttReader.hasMore | mqtt.go:5760 | NOT_APPLICABLE | | .NET uses Span-based parsing, no position tracking needed |
| mqttReader.readByte | mqtt.go:5764 | PORTED | src/NATS.Server/Mqtt/MqttPacketReader.cs:30-31 | Buffer indexing in Read method |
| mqttReader.readPacketLen / readPacketLenWithCheck | mqtt.go:5773-5803 | PORTED | src/NATS.Server/Mqtt/MqttPacketReader.cs:43-62 | DecodeRemainingLength implemented |
| mqttReader.readString | mqtt.go:5805 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:275-289 | ReadUtf8String implemented |
| mqttReader.readBytes | mqtt.go:5814 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:296-310 | ReadBinaryField implemented |
| mqttReader.readUint16 | mqtt.go:5835 | PORTED | src/NATS.Server/Mqtt/MqttBinaryDecoder.cs:316-323 | ReadUInt16BigEndian implemented |
#### Writer Functions (lines 5850-5882)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| mqttWriter.WriteUint16 | mqtt.go:5850 | PARTIAL | src/NATS.Server/Mqtt/MqttPacketWriter.cs:12-14 | Inline in Write method, not a standalone helper |
| mqttWriter.WriteString | mqtt.go:5855 | MISSING | | Standalone string write (length-prefixed) not implemented |
| mqttWriter.WriteBytes | mqtt.go:5859 | MISSING | | Standalone bytes write (length-prefixed) not implemented |
| mqttWriter.WriteVarInt | mqtt.go:5864 | PORTED | src/NATS.Server/Mqtt/MqttPacketWriter.cs:19-37 | EncodeRemainingLength implemented |
| newMQTTWriter | mqtt.go:5878 | PARTIAL | src/NATS.Server/Mqtt/MqttPacketWriter.cs:3 | Static class, no constructor needed; Write method serves the purpose |
---
## 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/Mqtt/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Mqtt/ -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')"
```
## Summary Counts
| Status | Count |
|--------|-------|
| PORTED | 14 |
| PARTIAL | 57 |
| MISSING | 119 |
| NOT_APPLICABLE | 5 |
| DEFERRED | 0 |
| **Total** | **195** |
## Change Log
| Date | Change | By |
|------|--------|----|
| 2026-02-25 | Full gap analysis completed: 195 items analyzed. 14 PORTED, 57 PARTIAL, 119 MISSING, 5 NOT_APPLICABLE. Major gaps: JetStream integration (entire mqttJSA layer ~30 functions), binary protocol encoding (CONNACK/SUBACK/UNSUBACK/PUBLISH serialization), delivery callbacks (QoS0/QoS1/QoS2), account session management, retained message encoding/decoding, Sparkplug B support, NATS subject reverse mapping. .NET has solid foundation for packet reading/writing, connect parsing, basic pub/sub flow, QoS tracking, and retained store, but all are simplified/in-memory-only without JetStream backing. | claude |
| 2026-02-25 | File created with LLM analysis instructions | auto |

285
gaps/protocol.md Normal file
View File

@@ -0,0 +1,285 @@
# Protocol — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Protocol** 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 Protocol
- The parser is a **byte-by-byte state machine**. In .NET, use `System.IO.Pipelines` for zero-copy parsing with `ReadOnlySequence<byte>`.
- Control line limit: 4096 bytes. Default max payload: 1MB.
- Extended protocol: `HPUB/HMSG` (headers), `RPUB/RMSG` (routes) — check if these are implemented.
- Fuzz tests in Go may map to `[Theory]` tests with randomized inputs in .NET.
---
## Go Reference Files (Source)
- `golang/nats-server/server/parser.go` — Protocol state machine (PUB, SUB, UNSUB, CONNECT, INFO, PING/PONG, HPUB/HMSG, RPUB/RMSG)
- `golang/nats-server/server/proto.go` — Wire-level protocol writing (sendProto, sendInfo, etc.)
- `golang/nats-server/server/const.go` — Protocol constants, limits (control line 4096, default max payload 1MB)
## Go Reference Files (Tests)
- `golang/nats-server/server/parser_test.go`
- `golang/nats-server/server/parser_fuzz_test.go`
- `golang/nats-server/server/server_fuzz_test.go`
- `golang/nats-server/server/subject_fuzz_test.go`
- `golang/nats-server/server/split_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Protocol/NatsParser.cs` — State machine
- `src/NATS.Server/Protocol/NatsProtocol.cs` — Wire-level writing
- `src/NATS.Server/Protocol/NatsHeaderParser.cs`
- `src/NATS.Server/Protocol/ClientCommandMatrix.cs`
- `src/NATS.Server/Protocol/MessageTraceContext.cs`
- `src/NATS.Server/Protocol/ProxyProtocol.cs`
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Protocol/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### golang/nats-server/server/parser.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `parserState` (type) | parser.go:24 | PORTED | `src/NATS.Server/Protocol/NatsParser.cs` | Go uses an `iota` int type; .NET uses `enum CommandType` + internal awaiting-payload state |
| `parseState` (struct) | parser.go:25 | PARTIAL | `src/NATS.Server/Protocol/NatsParser.cs:37` | `.NET NatsParser` carries equivalent per-command state via fields (`_awaitingPayload`, `_expectedPayloadSize`, etc.). Missing: `argBuf`/`msgBuf` split-buffer accumulation fields, `scratch` fixed-size buffer, `header` lazy-parse field. The .NET parser relies on `System.IO.Pipelines` buffering rather than explicit accumulator fields. |
| `pubArg` (struct) | parser.go:37 | PARTIAL | `src/NATS.Server/Protocol/NatsParser.cs:21` | `ParsedCommand` covers `subject`, `reply`, `size`, `hdr`. Missing: `origin`, `account`, `pacache`, `mapped`, `queues`, `szb`, `hdb`, `psi`, `trace`, `delivered` — clustering/routing/JetStream fields not yet needed for core client protocol. |
| `OP_START` … `INFO_ARG` (parser state constants) | parser.go:57134 | PARTIAL | `src/NATS.Server/Protocol/NatsParser.cs:104` | All CLIENT-facing states implemented (PUB, HPUB, SUB, UNSUB, CONNECT, INFO, PING, PONG, +OK, -ERR). MISSING states: `OP_A`/`ASUB_ARG`/`AUSUB_ARG` (A+/A- for gateways), `OP_R`/`OP_RS`/`OP_L`/`OP_LS` (RMSG/LMSG/RS+/RS-/LS+/LS-), `OP_M`/`MSG_ARG`/`HMSG_ARG` (routing MSG/HMSG). See `ClientCommandMatrix.cs` for partial routing opcode routing. |
| `client.parse()` | parser.go:136 | PARTIAL | `src/NATS.Server/Protocol/NatsParser.cs:69` | Core CLIENT-facing parse loop ported as `NatsParser.TryParse()` using `ReadOnlySequence<byte>` + `SequenceReader`. Missing: byte-by-byte incremental state transitions (Go uses byte-by-byte state machine; .NET scans for `\r\n` on each call), auth-set check before non-CONNECT op, MQTT dispatch (`c.mqttParse`), gateway in-CONNECT gating, ROUTER/GATEWAY/LEAF protocol dispatch (RMSG, LMSG, RS+, RS-, A+, A-). |
| `protoSnippet()` | parser.go:1236 | MISSING | — | Helper that formats a quoted snippet of the protocol buffer for error messages. The .NET parser throws `ProtocolViolationException` with a plain message; no equivalent snippet utility exists. |
| `client.overMaxControlLineLimit()` | parser.go:1251 | PARTIAL | `src/NATS.Server/Protocol/NatsParser.cs:82` | .NET checks `line.Length > NatsProtocol.MaxControlLineSize` and throws. Missing: kind-check (Go only enforces for `CLIENT` kind), client close on violation (`closeConnection(MaxControlLineExceeded)`), structured error with state/buffer info. |
| `client.clonePubArg()` | parser.go:1267 | MISSING | — | Split-buffer scenario: clones pubArg and re-processes when payload spans two reads. Not needed in .NET because `System.IO.Pipelines` handles buffering, but there is no explicit equivalent. |
| `parseState.getHeader()` | parser.go:1297 | PARTIAL | `src/NATS.Server/Protocol/NatsHeaderParser.cs:25` | Go lazily parses `http.Header` from the raw message buffer. .NET has `NatsHeaderParser.Parse()` which parses NATS/1.0 headers. Missing: lazy evaluation on the parsed command (header is not cached on `ParsedCommand`). |
### golang/nats-server/server/proto.go
*Note: This Go file (`proto.go`) implements a **protobuf wire-format** scanner/encoder (field tags, varints, length-delimited bytes) used internally for NATS's binary internal protocol (e.g. JetStream metadata encoding). It is unrelated to the NATS text protocol.*
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `errProtoInsufficient` | proto.go:24 | MISSING | — | Package-level error sentinel for varint parsing. No .NET equivalent (JetStream binary encoding not yet ported). |
| `errProtoOverflow` | proto.go:25 | MISSING | — | Package-level error sentinel. |
| `errProtoInvalidFieldNumber` | proto.go:26 | MISSING | — | Package-level error sentinel. |
| `protoScanField()` | proto.go:28 | MISSING | — | Scans one protobuf field (tag + value) from a byte slice. Used by JetStream internal encoding. Not yet ported. |
| `protoScanTag()` | proto.go:42 | MISSING | — | Decodes a protobuf tag (field number + wire type) from a varint. Not yet ported. |
| `protoScanFieldValue()` | proto.go:61 | MISSING | — | Reads the value portion of a protobuf field by wire type. Not yet ported. |
| `protoScanVarint()` | proto.go:77 | MISSING | — | 10-byte max varint decoder. Not yet ported. |
| `protoScanBytes()` | proto.go:179 | MISSING | — | Length-delimited bytes field reader. Not yet ported. |
| `protoEncodeVarint()` | proto.go:190 | MISSING | — | Varint encoder. Not yet ported. |
### golang/nats-server/server/const.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `Command` (type) | const.go:23 | NOT_APPLICABLE | — | Go string type alias for signal commands (stop/quit/reload). Managed by OS signal handling; not applicable to .NET server lifecycle which uses `CancellationToken`. |
| `CommandStop`, `CommandQuit`, `CommandReopen`, `CommandReload` | const.go:2734 | NOT_APPLICABLE | — | OS signal-based server control commands. .NET uses `CancellationToken` + `IHostedService` lifecycle. |
| `gitCommit`, `serverVersion` (build vars) | const.go:39 | NOT_APPLICABLE | — | Go linker-injected build vars. Equivalent handled by .NET assembly info / `AssemblyInformationalVersionAttribute`. |
| `formatRevision()` | const.go:47 | NOT_APPLICABLE | — | Formats a 7-char VCS commit hash for display. Go-specific build info pattern; not needed in .NET. |
| `init()` (build info) | const.go:54 | NOT_APPLICABLE | — | Reads `debug.BuildInfo` at startup to extract VCS revision. Not applicable to .NET. |
| `VERSION = "2.14.0-dev"` | const.go:69 | PARTIAL | `src/NATS.Server/Protocol/NatsProtocol.cs:11` | .NET has `Version = "0.1.0"`. The version string is present but does not match Go's version. |
| `PROTO = 1` | const.go:76 | PORTED | `src/NATS.Server/Protocol/NatsProtocol.cs:12` | `ProtoVersion = 1` |
| `DEFAULT_PORT = 4222` | const.go:79 | PORTED | `src/NATS.Server/Protocol/NatsProtocol.cs:10` | `DefaultPort = 4222` |
| `RANDOM_PORT = -1` | const.go:83 | NOT_APPLICABLE | — | Used in Go test helpers to request a random port. .NET tests use `GetFreePort()` pattern. |
| `DEFAULT_HOST = "0.0.0.0"` | const.go:86 | MISSING | — | No explicit constant in .NET; server defaults to `0.0.0.0` but the constant is not named. |
| `MAX_CONTROL_LINE_SIZE = 4096` | const.go:91 | PORTED | `src/NATS.Server/Protocol/NatsProtocol.cs:7` | `MaxControlLineSize = 4096` |
| `MAX_PAYLOAD_SIZE = 1MB` | const.go:95 | PORTED | `src/NATS.Server/Protocol/NatsProtocol.cs:8` | `MaxPayloadSize = 1024 * 1024` |
| `MAX_PAYLOAD_MAX_SIZE = 8MB` | const.go:99 | MISSING | — | Warning threshold for max_payload setting. No .NET equivalent. |
| `MAX_PENDING_SIZE = 64MB` | const.go:103 | PORTED | `src/NATS.Server/Protocol/NatsProtocol.cs:9` | `MaxPendingSize = 64 * 1024 * 1024` |
| `DEFAULT_MAX_CONNECTIONS = 64K` | const.go:106 | MISSING | — | Default max connections cap. No .NET equivalent constant. |
| `TLS_TIMEOUT = 2s` | const.go:109 | MISSING | — | TLS handshake wait time. Not yet defined in .NET options. |
| `DEFAULT_TLS_HANDSHAKE_FIRST_FALLBACK_DELAY = 50ms` | const.go:114 | MISSING | — | TLS-first handshake fallback delay. Not yet implemented in .NET. |
| `AUTH_TIMEOUT = 2s` | const.go:118 | MISSING | — | Authorization wait timeout. No .NET equivalent constant. |
| `DEFAULT_PING_INTERVAL = 2min` | const.go:122 | MISSING | — | Ping interval for keep-alive. No .NET equivalent. |
| `DEFAULT_PING_MAX_OUT = 2` | const.go:125 | MISSING | — | Max outstanding pings before disconnect. No .NET equivalent. |
| `CR_LF = "\r\n"` | const.go:128 | PORTED | `src/NATS.Server/Protocol/NatsProtocol.cs:15` | `CrLf` byte array. |
| `LEN_CR_LF = 2` | const.go:131 | PORTED | Implicit in .NET (`+ 2` literals in parser). | Used as literal `2` in `TryReadPayload`. |
| `DEFAULT_FLUSH_DEADLINE = 10s` | const.go:134 | MISSING | — | Write/flush deadline. Not yet defined. |
| `DEFAULT_HTTP_PORT = 8222` | const.go:137 | MISSING | — | Monitoring port. Not yet implemented. |
| `DEFAULT_HTTP_BASE_PATH = "/"` | const.go:140 | MISSING | — | Monitoring HTTP base path. Not yet implemented. |
| `ACCEPT_MIN_SLEEP = 10ms` | const.go:143 | MISSING | — | Retry sleep for transient accept errors. Not yet defined. |
| `ACCEPT_MAX_SLEEP = 1s` | const.go:146 | MISSING | — | Max sleep for accept errors. Not yet defined. |
| `DEFAULT_ROUTE_CONNECT = 1s` | const.go:149 | MISSING | — | Route solicitation interval. Clustering not yet implemented. |
| `DEFAULT_ROUTE_CONNECT_MAX = 30s` | const.go:152 | MISSING | — | Route max solicitation interval. |
| `DEFAULT_ROUTE_RECONNECT = 1s` | const.go:155 | MISSING | — | Route reconnect delay. |
| `DEFAULT_ROUTE_DIAL = 1s` | const.go:158 | MISSING | — | Route dial timeout. |
| `DEFAULT_ROUTE_POOL_SIZE = 3` | const.go:161 | MISSING | — | Route connection pool size. |
| `DEFAULT_LEAF_NODE_RECONNECT = 1s` | const.go:164 | MISSING | — | Leaf node reconnect interval. |
| `DEFAULT_LEAF_TLS_TIMEOUT = 2s` | const.go:167 | MISSING | — | Leaf node TLS timeout. |
| `PROTO_SNIPPET_SIZE = 32` | const.go:170 | MISSING | — | Size of proto snippet in parse errors. No .NET equivalent (errors use plain messages). |
| `MAX_CONTROL_LINE_SNIPPET_SIZE = 128` | const.go:172 | MISSING | — | Snippet size for control-line-too-long errors. |
| `MAX_MSG_ARGS = 4` | const.go:175 | NOT_APPLICABLE | — | Used in Go's manual arg-split loop. .NET uses `SplitArgs()` with stack-allocated ranges. |
| `MAX_RMSG_ARGS = 6` | const.go:178 | NOT_APPLICABLE | — | Used in RMSG parsing. RMSG not yet ported. |
| `MAX_HMSG_ARGS = 7` | const.go:180 | NOT_APPLICABLE | — | Used in HMSG parsing. HMSG routing not yet ported. |
| `MAX_PUB_ARGS = 3` | const.go:183 | NOT_APPLICABLE | — | Used in PUB arg splitting. .NET uses dynamic `SplitArgs`. |
| `MAX_HPUB_ARGS = 4` | const.go:186 | NOT_APPLICABLE | — | Used in HPUB arg splitting. .NET uses dynamic `SplitArgs`. |
| `MAX_RSUB_ARGS = 6` | const.go:189 | NOT_APPLICABLE | — | Used in RS+/LS+ subscription arg splitting. Not yet ported. |
| `DEFAULT_MAX_CLOSED_CLIENTS = 10000` | const.go:192 | MISSING | — | Closed-connection history cap. Not yet implemented. |
| `DEFAULT_LAME_DUCK_DURATION = 2min` | const.go:196 | MISSING | — | Lame-duck shutdown spread duration. Not yet implemented. |
| `DEFAULT_LAME_DUCK_GRACE_PERIOD = 10s` | const.go:200 | MISSING | — | Lame-duck grace period. Not yet implemented. |
| `DEFAULT_LEAFNODE_INFO_WAIT = 1s` | const.go:203 | MISSING | — | Leaf node INFO wait. Not yet implemented. |
| `DEFAULT_LEAFNODE_PORT = 7422` | const.go:206 | MISSING | — | Default leaf node port. Not yet implemented. |
| `DEFAULT_CONNECT_ERROR_REPORTS = 3600` | const.go:214 | MISSING | — | Error report throttle for initial connection failures. Not yet implemented. |
| `DEFAULT_RECONNECT_ERROR_REPORTS = 1` | const.go:220 | MISSING | — | Error report throttle for reconnect failures. Not yet implemented. |
| `DEFAULT_RTT_MEASUREMENT_INTERVAL = 1h` | const.go:224 | MISSING | — | RTT measurement interval. Not yet implemented. |
| `DEFAULT_ALLOW_RESPONSE_MAX_MSGS = 1` | const.go:228 | MISSING | — | Default allowed response message count for reply subjects. Not yet implemented. |
| `DEFAULT_ALLOW_RESPONSE_EXPIRATION = 2min` | const.go:232 | MISSING | — | Dynamic response permission expiry. Not yet implemented. |
| `DEFAULT_SERVICE_EXPORT_RESPONSE_THRESHOLD = 2min` | const.go:237 | MISSING | — | Service export response threshold. Not yet implemented (accounts/JetStream). |
| `DEFAULT_SERVICE_LATENCY_SAMPLING = 100` | const.go:241 | MISSING | — | Service latency sampling rate. Not yet implemented. |
| `DEFAULT_SYSTEM_ACCOUNT = "$SYS"` | const.go:244 | MISSING | — | System account name constant. Not yet implemented. |
| `DEFAULT_GLOBAL_ACCOUNT = "$G"` | const.go:247 | MISSING | — | Global account name constant. Not yet implemented. |
| `DEFAULT_ACCOUNT_FETCH_TIMEOUT = 1900ms` | const.go:250 | MISSING | — | Account fetch timeout. Not yet implemented. |
### .NET-Only Additions (no Go counterpart in the three source files)
| .NET Symbol | .NET File:Line | Status | Notes |
|-------------|:---------------|--------|-------|
| `NatsHeaderParser` | `src/NATS.Server/Protocol/NatsHeaderParser.cs:20` | PORTED | Parses `NATS/1.0` header blocks. Go uses `http.Header`/`textproto` lazily from `getHeader()` on `parseState`. .NET provides an eager standalone parser. |
| `NatsHeaders` (struct) | `src/NATS.Server/Protocol/NatsHeaderParser.cs:6` | PORTED | Structured result for parsed NATS headers (status, description, key-value map). No direct Go counterpart — Go uses `http.Header` directly. |
| `ClientCommandMatrix` | `src/NATS.Server/Protocol/ClientCommandMatrix.cs:4` | PORTED | Encodes which inter-server opcodes (RS+, RS-, RMSG, A+, A-, LS+, LS-, LMSG) are allowed per client kind. Corresponds to the `switch c.kind` / `switch c.op` dispatch inside `parse()`. |
| `MessageTraceContext` | `src/NATS.Server/Protocol/MessageTraceContext.cs:3` | PORTED | Captures client name/lang/version/headers from CONNECT options for trace logging. Equivalent to fields inside Go's `client` struct populated during `processConnect`. |
| `ProxyProtocolParser` | `src/NATS.Server/Protocol/ProxyProtocol.cs:48` | PORTED | PROXY protocol v1/v2 pure-parser. Corresponds to `client_proxyproto.go` (not in the three listed source files, but closely related to the protocol module). |
| `ProxyAddress`, `ProxyParseResult`, `ProxyParseResultKind` | `src/NATS.Server/Protocol/ProxyProtocol.cs:11` | PORTED | Supporting types for PROXY protocol parsing. |
| `ServerInfo` (class) | `src/NATS.Server/Protocol/NatsProtocol.cs:39` | PORTED | Wire-format INFO message JSON model. Corresponds to Go's `Info` struct in `server.go`. |
| `ClientOptions` (class) | `src/NATS.Server/Protocol/NatsProtocol.cs:98` | PARTIAL | Wire-format CONNECT options JSON model. Missing: `nkey`, `sig`, `jwt` fields present but auth handling not implemented server-side. |
---
## 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/Protocol/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Protocol/ -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')"
```
## Test Cross-Reference Summary
### Go test file: `parser_test.go`
| Go Test | Status | .NET Equivalent |
|---------|--------|-----------------|
| `TestParsePing` | PARTIAL | `tests/NATS.Server.Tests/ParserTests.cs: Parse_PING` — covers full `PING\r\n`; missing byte-by-byte incremental state assertions |
| `TestParsePong` | PARTIAL | `tests/NATS.Server.Tests/ParserTests.cs: Parse_PONG` — covers full `PONG\r\n`; missing `ping.out` counter decrement test |
| `TestParseConnect` | PORTED | `tests/NATS.Server.Tests/ParserTests.cs: Parse_CONNECT` |
| `TestParseSub` | PORTED | `tests/NATS.Server.Tests/ParserTests.cs: Parse_SUB_without_queue`, `Parse_SUB_with_queue` |
| `TestParsePub` | PARTIAL | `tests/NATS.Server.Tests/ParserTests.cs: Parse_PUB_with_payload`, `Parse_PUB_with_reply` — missing overflow payload error scenario |
| `TestParsePubSizeOverflow` | MISSING | No .NET test for integer overflow on very large size values (>9 digits handled by `ParseSize` returning -1, but no explicit overflow test) |
| `TestParsePubArg` | PORTED | `tests/NATS.Server.Tests/ParserTests.cs: Parse_PUB_argument_variations` (Theory) |
| `TestParsePubBadSize` | PARTIAL | `tests/NATS.Server.Tests/ParserTests.cs: Parse_malformed_protocol_fails` covers some bad args; missing specific `mpay` (max payload per-client) test |
| `TestParseHeaderPub` | PORTED | `tests/NATS.Server.Tests/ParserTests.cs: Parse_HPUB` |
| `TestParseHeaderPubArg` | MISSING | No .NET Theory equivalent for the 32 HPUB argument variations with mixed spaces/tabs |
| `TestParseRoutedHeaderMsg` (HMSG) | MISSING | No .NET equivalent — ROUTER/GATEWAY HMSG parsing not yet ported |
| `TestParseRouteMsg` (RMSG) | MISSING | No .NET equivalent — ROUTER RMSG parsing not yet ported |
| `TestParseMsgSpace` | MISSING | No .NET equivalent — MSG opcode for routes not yet ported |
| `TestShouldFail` | PARTIAL | `tests/NATS.Server.Tests/ParserTests.cs: Parse_malformed_protocol_fails` — covers subset; documented behavioral differences for byte-by-byte vs prefix-scan parser |
| `TestProtoSnippet` | MISSING | No .NET equivalent for `protoSnippet()` helper |
| `TestParseOK` | PORTED | `tests/NATS.Server.Tests/ParserTests.cs: Parse_case_insensitive` includes +OK (via `ParsedCommand.Simple`) |
| `TestMaxControlLine` | PARTIAL | `tests/NATS.Server.Tests/ParserTests.cs: Parse_exceeding_max_control_line_fails` — covers basic enforcement; missing per-client-kind bypass (LEAF/ROUTER/GATEWAY exempt) |
### Go test file: `split_test.go`
| Go Test | Status | .NET Equivalent |
|---------|--------|-----------------|
| `TestSplitBufferSubOp` | PARTIAL | `tests/NATS.Server.Tests/ParserTests.cs` — `System.IO.Pipelines` handles split buffers transparently; no explicit split-state test |
| `TestSplitBufferUnsubOp` | PARTIAL | Same as above |
| `TestSplitBufferPubOp` … `TestSplitBufferPubOp5` | PARTIAL | `Parse_PUB_with_payload` covers basic case; no multi-chunk split test |
| `TestSplitConnectArg` | PARTIAL | No explicit argBuf accumulation test |
| `TestSplitDanglingArgBuf` | NOT_APPLICABLE | .NET parser has no `argBuf` — pipeline buffering makes this moot |
| `TestSplitRoutedMsgArg` | MISSING | RMSG not yet ported |
| `TestSplitBufferMsgOp` | MISSING | RMSG not yet ported |
| `TestSplitBufferLeafMsgArg` | MISSING | LMSG (leaf) not yet ported |
### Go test file: `parser_fuzz_test.go`
| Go Test | Status | .NET Equivalent |
|---------|--------|-----------------|
| `FuzzParser` | MISSING | No .NET fuzz test. Could be approximated with property-based testing (`[Theory]` with random inputs via `FsCheck` or `Bogus`). |
### Go test file: `server_fuzz_test.go`
| Go Test | Status | .NET Equivalent |
|---------|--------|-----------------|
| `FuzzServerTLS` | MISSING | TLS-first handshake and TLS fuzzing not yet implemented in .NET server. |
### Go test file: `subject_fuzz_test.go`
| Go Test | Status | .NET Equivalent |
|---------|--------|-----------------|
| `FuzzSubjectsCollide` | MISSING | `SubjectsCollide()` function not yet ported. .NET has `SubjectMatch.IsValidSubject()` and wildcard matching but not a `SubjectsCollide` API. |
---
## Change Log
| Date | Change | By |
|------|--------|----|
| 2026-02-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated: parser.go, proto.go, const.go; test cross-reference for all 5 Go test files | claude-sonnet-4-6 |

389
gaps/raft.md Normal file
View File

@@ -0,0 +1,389 @@
# RAFT — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **RAFT** 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 RAFT
- RAFT is used for JetStream cluster consensus — both meta-cluster (stream/consumer placement) and per-stream/consumer groups.
- Key operations: leader election, log append/commit, snapshotting, peer management.
- The .NET implementation (20 files, 3,045 LOC) is more granular than Go's single file. Check if the decomposition covers all RAFT states.
---
## Go Reference Files (Source)
- `golang/nats-server/server/raft.go` — RAFT consensus for clustered JetStream (~5,037 lines). Meta-cluster for metadata, per-stream/consumer RAFT groups. Leader election, log replication, snapshotting.
## Go Reference Files (Tests)
- `golang/nats-server/server/raft_test.go`
- `golang/nats-server/server/raft_helpers_test.go`
- `golang/nats-server/server/raft_chain_of_blocks_helpers_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Raft/` (all 20 files)
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Raft/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### golang/nats-server/server/raft.go — Exported Interfaces & Types
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| RaftNode (interface) | raft.go:40-92 | PARTIAL | src/NATS.Server/Raft/IRaftNode.cs:5 | Interface declared but empty — none of the 40+ methods from Go are defined |
| RaftNodeCheckpoint (interface) | raft.go:98-103 | PARTIAL | src/NATS.Server/Raft/RaftSnapshotCheckpoint.cs:7 | Chunk assembly exists but LoadLastSnapshot, AppendEntriesSeq, Abort, InstallSnapshot not matching Go's interface contract |
| WAL (interface) | raft.go:105-118 | PARTIAL | src/NATS.Server/Raft/RaftWal.cs:20 | .NET RaftWal is a concrete file-based WAL; does not implement Go's WAL interface (StoreMsg, LoadMsg, RemoveMsg, Compact, Purge, Truncate, State, FastState, Stop, Delete) |
| Peer (struct) | raft.go:120-125 | PARTIAL | src/NATS.Server/Raft/RaftPeerState.cs:7 | .NET has NextIndex/MatchIndex/LastContact/Active but missing Lag and Current fields from Go's Peer export |
| RaftState (enum) | raft.go:127-135 | PORTED | src/NATS.Server/Raft/RaftState.cs:4 | All four states: Follower, Leader, Candidate, Closed |
| RaftState.String() | raft.go:137-149 | MISSING | — | No .NET ToString override for RaftState enum |
| RaftConfig (struct) | raft.go:301-317 | MISSING | — | No equivalent configuration struct (Name, Store, Log, Track, Observer, Recovering, ScaleUp) |
| CommittedEntry (struct) | raft.go:2506-2509 | PARTIAL | src/NATS.Server/Raft/CommitQueue.cs:9 | CommitQueue<T> exists as channel wrapper, but no CommittedEntry struct with Index+Entries |
| Entry (struct) | raft.go:2641-2644 | PARTIAL | src/NATS.Server/Raft/RaftWireFormat.cs:73 | RaftEntryWire has Type+Data but is wire-format only; no general Entry type used for proposals |
| EntryType (enum) | raft.go:2605-2619 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:54-63 | All types present including EntryCatchup (mapped as RaftEntryType) |
### golang/nats-server/server/raft.go — Exported RaftNode Interface Methods
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Propose() | raft.go:909-924 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:308 | ProposeAsync exists but synchronous replication model, no write-error checking, no proposal queue |
| ProposeMulti() | raft.go:928-945 | MISSING | — | No batch proposal support |
| ForwardProposal() | raft.go:949-959 | PARTIAL | src/NATS.Server/Raft/NatsRaftTransport.cs:182 | Transport has ForwardProposal but RaftNode does not call it automatically for non-leaders |
| InstallSnapshot() | raft.go:1295-1311 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:699 | InstallSnapshotAsync exists but no checkpointing, no WAL compaction, no highwayhash verification |
| CreateSnapshotCheckpoint() | raft.go:1356-1360 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:519 | CreateSnapshotCheckpointAsync exists but simplified — no async write, no WAL compaction tracking |
| SendSnapshot() | raft.go:1284-1290 | MISSING | — | No direct snapshot send as append entry |
| NeedSnapshot() | raft.go:1551-1555 | MISSING | — | No equivalent check |
| Applied() | raft.go:1183-1185 | MISSING | — | No callback from upper layer for applied index tracking (delegates to Processed) |
| Processed() | raft.go:1193-1240 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:664 | MarkProcessed exists but much simpler — no aflr signaling, no leader state transition, no byte estimation |
| State() | raft.go:2025-2027 | PORTED | src/NATS.Server/Raft/RaftNode.cs:43 | Role property (uses RaftRole enum instead of RaftState) |
| Size() | raft.go:2037-2043 | MISSING | — | No WAL size reporting |
| Progress() | raft.go:2030-2034 | MISSING | — | No combined (index, commit, applied) return |
| Leader() | raft.go:1712-1717 | PORTED | src/NATS.Server/Raft/RaftNode.cs:42 | IsLeader property |
| LeaderSince() | raft.go:1721-1726 | MISSING | — | No leader-since timestamp tracking |
| Quorum() | raft.go:3070-3083 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:201 | HasQuorum() exists but uses different window calculation (2x electionTimeout vs Go's lostQuorumInterval) |
| Current() | raft.go:1840-1847 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:879 | IsCurrent exists but no commit==applied check, no forward-progress polling |
| Healthy() | raft.go:1850-1857 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:892 | IsHealthy exists but different semantics — checks peer responsiveness, not isCurrent(true) |
| Term() | raft.go:3119-3123 | PORTED | src/NATS.Server/Raft/RaftNode.cs:41 | Term property |
| Leaderless() | raft.go:1876-1883 | MISSING | — | No atomic hasleader flag |
| GroupLeader() | raft.go:1865-1872 | MISSING | — | No leader ID tracking (only IsLeader bool) |
| HadPreviousLeader() | raft.go:1860-1862 | MISSING | — | No pleader atomic flag |
| StepDown() | raft.go:1900-1977 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:706 | RequestStepDown exists but no preferred leader selection, no leader transfer, no EntryLeaderTransfer |
| SetObserver() | raft.go:2394-2396 | MISSING | — | No observer mode |
| IsObserver() | raft.go:2387-2391 | MISSING | — | No observer mode |
| Campaign() | raft.go:1980-1984 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:771 | CampaignImmediately exists but no random campaign timeout |
| CampaignImmediately() | raft.go:1987-1993 | PORTED | src/NATS.Server/Raft/RaftNode.cs:771 | CampaignImmediately() |
| ID() | raft.go:2045-2051 | PORTED | src/NATS.Server/Raft/RaftNode.cs:40 | Id property |
| Group() | raft.go:2053-2056 | MISSING | — | No group name tracking |
| Peers() | raft.go:2058-2077 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:872 | GetPeerStates returns Dict but missing Lag calculation |
| ProposeKnownPeers() | raft.go:2080-2089 | MISSING | — | No peer state broadcast |
| UpdateKnownPeers() | raft.go:2092-2096 | MISSING | — | No peer state update |
| ProposeAddPeer() | raft.go:962-983 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:372 | ProposeAddPeerAsync exists but synchronous replication, no forwarding to leader |
| ProposeRemovePeer() | raft.go:986-1019 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:415 | ProposeRemovePeerAsync exists but no forwarding, no self-removal handling |
| MembershipChangeInProgress() | raft.go:1021-1025 | PORTED | src/NATS.Server/Raft/RaftNode.cs:67 | MembershipChangeInProgress property |
| AdjustClusterSize() | raft.go:1059-1079 | MISSING | — | No cluster size adjustment |
| AdjustBootClusterSize() | raft.go:1038-1055 | MISSING | — | No boot cluster size adjustment |
| ClusterSize() | raft.go:1029-1033 | MISSING | — | No explicit cluster size property |
| ApplyQ() | raft.go:2106 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:53 | CommitQueue exists as Channel-based queue, different API than ipQueue |
| PauseApply() | raft.go:1084-1092 | MISSING | — | No apply pausing |
| ResumeApply() | raft.go:1111-1156 | MISSING | — | No apply resuming with replay |
| DrainAndReplaySnapshot() | raft.go:1162-1177 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:537 | DrainAndReplaySnapshotAsync exists but simplified — no catchup cancellation, no commit preservation |
| LeadChangeC() | raft.go:2110 | MISSING | — | No leader change channel |
| QuitC() | raft.go:2113 | MISSING | — | No quit channel |
| Created() | raft.go:2115-2118 | MISSING | — | No creation timestamp |
| Stop() | raft.go:2120-2122 | MISSING | — | No graceful shutdown (Dispose exists but minimal) |
| WaitForStop() | raft.go:2124-2128 | MISSING | — | No wait-for-stop mechanism |
| Delete() | raft.go:2130-2143 | MISSING | — | No delete with WAL cleanup |
| IsDeleted() | raft.go:2145-2149 | MISSING | — | No deleted flag |
| RecreateInternalSubs() | raft.go:658-747 | MISSING | — | No NATS internal subscription management |
| IsSystemAccount() | raft.go:648-649 | NOT_APPLICABLE | — | .NET does not have system account NRG routing |
| GetTrafficAccountName() | raft.go:652-656 | NOT_APPLICABLE | — | .NET does not have account NRG routing |
### golang/nats-server/server/raft.go — Core State Machine (unexported but critical)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| raft (struct) | raft.go:151-251 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:3 | RaftNode has basic fields (term, vote, log, peers, commit) but missing: WAL, catchup state, progress map, pae cache, ipQueues, removed peers, many flags |
| run() | raft.go:2275-2362 | MISSING | — | No main run loop / state machine goroutine |
| runAsFollower() | raft.go:2441-2496 | MISSING | — | No follower state machine with select on channels |
| runAsCandidate() | raft.go:3587-3670 | MISSING | — | No candidate state machine with vote collection |
| runAsLeader() | raft.go:2951-3067 | MISSING | — | No leader state machine with heartbeat ticker and proposal batching |
| processAppendEntry() | raft.go:3857-4248 | MISSING | — | Complex append entry processing with catchup, WAL alignment, truncation — not ported |
| processAppendEntryResponse() | raft.go:4287-4339 | MISSING | — | No response processing with catchup triggering |
| processVoteRequest() | raft.go:4780-4845 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:142 | GrantVote handles basic vote logic but missing: log up-to-dateness check (lastTerm/lastIndex), catchup cancellation, campaign timeout reset |
| requestVote() | raft.go:4856-4872 | MISSING | — | No vote request broadcast via RPC |
| handleAppendEntry() | raft.go:3672-3684 | MISSING | — | No wire callback for append entries |
| handleAppendEntryResponse() | raft.go:4342-4346 | MISSING | — | No wire callback for AE responses |
| handleVoteRequest() | raft.go:4847-4854 | MISSING | — | No wire callback for vote requests |
| handleVoteResponse() | raft.go:4764-4778 | MISSING | — | No wire callback for vote responses |
| handleForwardedProposal() | raft.go:2854-2874 | MISSING | — | No forwarded proposal handler |
| handleForwardedRemovePeerProposal() | raft.go:2820-2851 | MISSING | — | No forwarded remove-peer handler |
### golang/nats-server/server/raft.go — WAL & Storage
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| storeToWAL() | raft.go:4360-4396 | PARTIAL | src/NATS.Server/Raft/RaftWal.cs:74 | RaftWal.AppendAsync exists but different storage model (custom binary vs Go's filestore/memstore) |
| loadEntry() | raft.go:3339-3346 | MISSING | — | No entry loading from WAL by index |
| loadFirstEntry() | raft.go:3126-3130 | MISSING | — | No first-entry loading |
| truncateWAL() | raft.go:3758-3815 | MISSING | — | No WAL truncation with snapshot invalidation |
| resetWAL() | raft.go:3819-3821 | MISSING | — | No WAL reset |
| cachePendingEntry() | raft.go:4449-4460 | MISSING | — | No pending append entry cache |
### golang/nats-server/server/raft.go — Snapshots
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| snapshot (struct) | raft.go:1243-1248 | PARTIAL | src/NATS.Server/Raft/RaftSnapshot.cs:3 | RaftSnapshot has LastIncludedIndex/Term/Data but missing peerstate encoding |
| encodeSnapshot() | raft.go:1254-1279 | MISSING | — | No binary snapshot encoding with highwayhash |
| loadLastSnapshot() | raft.go:1660-1707 | MISSING | — | No binary snapshot loading with hash verification |
| setupLastSnapshot() | raft.go:1578-1656 | MISSING | — | No startup snapshot recovery from disk |
| installSnapshot() | raft.go:1315-1350 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:699 | InstallSnapshotAsync simplified — no WAL compact, no snapshot file management |
| termAndIndexFromSnapFile() | raft.go:1564-1573 | MISSING | — | No snapshot filename parsing |
### golang/nats-server/server/raft.go — Peer & Membership State
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| peerState (struct) | raft.go:4470-4474 | MISSING | — | No peer state struct with knownPeers/clusterSize/domainExt |
| encodePeerState() | raft.go:4480-4492 | MISSING | — | No binary peer state encoding |
| decodePeerState() | raft.go:4494-4514 | MISSING | — | No binary peer state decoding |
| writePeerState() | raft.go:4603-4609 | MISSING | — | No peer state file persistence |
| readPeerState() | raft.go:4611-4620 | MISSING | — | No peer state file reading |
| processPeerState() | raft.go:4264-4282 | MISSING | — | No peer state processing from leader |
| addPeer() | raft.go:2879-2898 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:129 | AddMember exists but no cluster size/quorum adjustment, no removed-list reversal |
| removePeer() | raft.go:2903-2913 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:131 | RemoveMember exists but no removed tracking, no cluster size/quorum adjustment |
| adjustClusterSizeAndQuorum() | raft.go:3534-3556 | MISSING | — | No dynamic cluster size/quorum recalculation |
| trackPeer() | raft.go:3559-3585 | MISSING | — | No automatic peer tracking with add-on-discovery |
### golang/nats-server/server/raft.go — Elections & Voting
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| switchToFollower() | raft.go:4958-4983 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:706 | RequestStepDown sets Follower but missing: aflr reset, leaderState/leaderSince clear, acks reset, leader update |
| switchToCandidate() | raft.go:4985-5014 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:133 | StartElection increments term and votes but missing: observer/paused/processed checks, quorum loss signaling |
| switchToLeader() | raft.go:5016-5037 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:986 | TryBecomeLeader sets Role but missing: aflr setup, peer state broadcast, leader update |
| campaign() | raft.go:2002-2009 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:971 | CampaignWithPreVote starts election but no random timeout |
| wonElection() | raft.go:4886-4888 | PORTED | src/NATS.Server/Raft/RaftNode.cs:986 | Quorum check in TryBecomeLeader |
| selectNextLeader() | raft.go:1887-1897 | MISSING | — | No next-leader selection by highest index |
| resetElectionTimeout() | raft.go:2241-2243 | PORTED | src/NATS.Server/Raft/RaftNode.cs:737 | ResetElectionTimeout with Timer |
| randElectionTimeout() | raft.go:2235-2238 | PORTED | src/NATS.Server/Raft/RaftNode.cs:727 | RandomizedElectionTimeout |
| randCampaignTimeout() | raft.go:1995-1998 | MISSING | — | No separate campaign timeout |
### golang/nats-server/server/raft.go — Wire Format & RPC
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| voteRequest (struct) | raft.go:4549-4556 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:84 | RaftVoteRequestWire with Encode/Decode |
| voteResponse (struct) | raft.go:4730-4735 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:132 | RaftVoteResponseWire with Encode/Decode |
| appendEntry (struct) | raft.go:2557-2569 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:189 | RaftAppendEntryWire with Encode/Decode |
| appendEntryResponse (struct) | raft.go:2760-2766 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:318 | RaftAppendEntryResponseWire with Encode/Decode |
| ae.encode() | raft.go:2662-2711 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:202 | RaftAppendEntryWire.Encode() |
| decodeAppendEntry() | raft.go:2714-2746 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:255 | RaftAppendEntryWire.Decode() |
| vr.encode() (voteRequest) | raft.go:4560-4568 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:94 | RaftVoteRequestWire.Encode() |
| decodeVoteRequest() | raft.go:4571-4583 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:109 | RaftVoteRequestWire.Decode() |
| vr.encode() (voteResponse) | raft.go:4739-4751 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:142 | RaftVoteResponseWire.Encode() |
| decodeVoteResponse() | raft.go:4753-4762 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:159 | RaftVoteResponseWire.Decode() |
| ar.encode() | raft.go:2777-2794 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:328 | RaftAppendEntryResponseWire.Encode() |
| decodeAppendEntryResponse() | raft.go:2799-2817 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:343 | RaftAppendEntryResponseWire.Decode() |
| idLen constant | raft.go:2756 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:23 | RaftWireConstants.IdLen = 8 |
| RaftWireHelpers (WriteId/ReadId) | raft.go:2693,4581 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:561 | RaftWireHelpers class |
| RaftWireHelpers (uvarint) | raft.go:2682,2740 | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:597 | WriteUvarint/ReadUvarint |
### golang/nats-server/server/raft.go — NATS Subjects & Transport
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| raftAllSubj constant | raft.go:2162 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:16 | RaftSubjects.All |
| raftVoteSubj constant | raft.go:2163 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:22 | RaftSubjects.Vote() |
| raftAppendSubj constant | raft.go:2164 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:28 | RaftSubjects.AppendEntry() |
| raftPropSubj constant | raft.go:2165 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:34 | RaftSubjects.Proposal() |
| raftRemovePeerSubj constant | raft.go:2166 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:40 | RaftSubjects.RemovePeer() |
| raftReply constant | raft.go:2167 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:46 | RaftSubjects.Reply() |
| raftCatchupReply constant | raft.go:2168 | PORTED | src/NATS.Server/Raft/RaftSubjects.cs:52 | RaftSubjects.CatchupReply() |
| sendRPC() | raft.go:4874-4878 | PARTIAL | src/NATS.Server/Raft/NatsRaftTransport.cs:29 | NatsRaftTransport._publish delegate, but no sendq integration |
| sendReply() | raft.go:4880-4884 | MISSING | — | No reply sending via sendq |
| sendHeartbeat() | raft.go:4545-4547 | PARTIAL | src/NATS.Server/Raft/NatsRaftTransport.cs:229 | SendHeartbeatAsync exists but optimistic (no real ACK tracking) |
| IRaftTransport | — | PORTED | src/NATS.Server/Raft/RaftTransport.cs:3 | Interface with AppendEntries, RequestVote, InstallSnapshot, SendTimeoutNow, SendHeartbeat |
| InMemoryRaftTransport | — | PORTED | src/NATS.Server/Raft/RaftTransport.cs:26 | Full in-memory transport for testing |
| NatsRaftTransport | — | PORTED | src/NATS.Server/Raft/NatsRaftTransport.cs:18 | Wire-level NATS transport with binary encoding |
| createInternalSubs() | raft.go:2209-2233 | MISSING | — | No internal NATS subscription creation |
| subscribe() / unsubscribe() | raft.go:2194-2206 | MISSING | — | No internal subscription management |
### golang/nats-server/server/raft.go — Catchup & Leader Coordination
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| catchupState (struct) | raft.go:260-268 | MISSING | — | No catchup state tracking |
| runCatchup() | raft.go:3132-3236 | MISSING | — | No catchup goroutine for lagging followers |
| catchupFollower() | raft.go:3266-3337 | MISSING | — | No follower catchup initiation |
| createCatchup() | raft.go:3718-3735 | MISSING | — | No catchup subscription/inbox creation |
| cancelCatchup() | raft.go:3689-3697 | MISSING | — | No catchup cancellation |
| catchupStalled() | raft.go:3702-3712 | MISSING | — | No stall detection |
| sendSnapshotToFollower() | raft.go:3239-3264 | MISSING | — | No snapshot streaming to follower |
| sendCatchupSignal() | raft.go:3738-3745 | MISSING | — | No catchup signal to upper layer |
| cancelCatchupSignal() | raft.go:3748-3754 | MISSING | — | No catchup signal cancellation |
### golang/nats-server/server/raft.go — Commit & Apply
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| applyCommit() | raft.go:3350-3462 | MISSING | — | Complex commit application with entry type dispatch, peer state processing, membership changes |
| tryCommit() | raft.go:3468-3487 | MISSING | — | No quorum-based commit with ack counting |
| trackResponse() | raft.go:3493-3529 | MISSING | — | No response tracking with ack map |
| sendMembershipChange() | raft.go:2918-2949 | MISSING | — | No membership change send with WAL store |
### golang/nats-server/server/raft.go — Persistence & Term/Vote
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| writeTermVote() | raft.go:4708-4727 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:993 | PersistAsync writes meta.json (JSON) but Go uses binary tav.idx with dedup |
| readTermVote() | raft.go:4637-4656 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:1013 | LoadPersistedStateAsync reads meta.json but Go uses binary tav.idx |
| writePeerState() (on raft) | raft.go:4589-4600 | MISSING | — | No binary peer state file writing |
| setWriteErr() / setWriteErrLocked() | raft.go:4659-4704 | MISSING | — | No write error tracking with permission/space error escalation |
### golang/nats-server/server/raft.go — Server Integration
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| bootstrapRaftNode() | raft.go:346-407 | MISSING | — | No raft bootstrap with peer validation and directory setup |
| initRaftNode() | raft.go:410-614 | MISSING | — | No full raft initialization with WAL replay, snapshot recovery, peer state restore |
| startRaftNode() | raft.go:617-628 | MISSING | — | No start-and-run goroutine |
| registerRaftNode() | raft.go:776-783 | MISSING | — | No server-level raft registration |
| unregisterRaftNode() | raft.go:786-792 | MISSING | — | No server-level raft unregistration |
| lookupRaftNode() | raft.go:803-811 | MISSING | — | No raft node lookup by group |
| numRaftNodes() | raft.go:795-799 | MISSING | — | No raft node count |
| stepdownRaftNodes() | raft.go:831-851 | MISSING | — | No server-wide raft stepdown |
| shutdownRaftNodes() | raft.go:856-875 | MISSING | — | No server-wide raft shutdown |
| transferRaftLeaders() | raft.go:880-903 | MISSING | — | No server-wide leader transfer |
| reloadDebugRaftNodes() | raft.go:815-827 | NOT_APPLICABLE | — | Debug flag reload is Go-specific |
| serverNameForNode() | raft.go:759-763 | NOT_APPLICABLE | — | Server-level node mapping not yet needed |
| clusterNameForNode() | raft.go:767-772 | NOT_APPLICABLE | — | Cluster-level node mapping not yet needed |
### golang/nats-server/server/raft.go — .NET Extensions (not in Go)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:366 | RaftPreVoteRequestWire — pre-vote wire format (Go does not have pre-vote) |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:410 | RaftPreVoteResponseWire — pre-vote wire format |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:455 | RaftTimeoutNowWire — leadership transfer wire format |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftWireFormat.cs:508 | RaftInstallSnapshotChunkWire — chunked snapshot wire format |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/CompactionPolicy.cs:9 | CompactionPolicy / CompactionOptions — configurable log compaction |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/SnapshotChunkEnumerator.cs:16 | SnapshotChunkEnumerator — snapshot chunking |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftNode.cs:251 | ReadIndexAsync — linearizable reads (Go does not expose this) |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftNode.cs:464 | Joint consensus (BeginJointConsensus/CommitJointConsensus) |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftNode.cs:790 | TransferLeadershipAsync with TimeoutNow RPC |
| (none — .NET addition) | — | PORTED | src/NATS.Server/Raft/RaftNode.cs:906 | Pre-vote protocol (StartPreVote/RequestPreVote) |
### golang/nats-server/server/raft.go — Object Pools & Helpers
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| cePool / newCommittedEntry / ReturnToPool | raft.go:2498-2532 | NOT_APPLICABLE | — | Go sync.Pool for CommittedEntry; .NET uses GC |
| entryPool / newEntry | raft.go:2534-2547 | NOT_APPLICABLE | — | Go sync.Pool for Entry; .NET uses GC |
| aePool / newAppendEntry / returnToPool | raft.go:2549-2583 | NOT_APPLICABLE | — | Go sync.Pool for appendEntry; .NET uses GC |
| pePool / newProposedEntry | raft.go:2586-2603 | NOT_APPLICABLE | — | Go sync.Pool for proposedEntry; .NET uses GC |
| arPool / newAppendEntryResponse | raft.go:2748-2775 | NOT_APPLICABLE | — | Go sync.Pool for appendEntryResponse; .NET uses GC |
| peers sync.Map (string interning) | raft.go:2797 | NOT_APPLICABLE | — | Go-specific string interning optimization |
| debug() / warn() / error() | raft.go:2364-2379 | NOT_APPLICABLE | — | Logging helpers — .NET uses ILogger<T> |
| dios semaphore | raft.go:1665-1667 | NOT_APPLICABLE | — | Go disk I/O semaphore — .NET uses async I/O |
| ipQueue usage | raft.go:233-238 | MISSING | — | Go uses typed ipQueue channels for proposals, entries, votes, responses — .NET has no equivalent internal queue infrastructure |
### golang/nats-server/server/raft.go — Constants & Configuration
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Election timeout defaults | raft.go:277-287 | PARTIAL | src/NATS.Server/Raft/RaftNode.cs:56-57 | .NET uses 150-300ms defaults; Go uses 4-9s defaults. Different design choice |
| hbInterval | raft.go:283 | MISSING | — | No heartbeat interval constant |
| lostQuorumInterval | raft.go:284 | MISSING | — | No lost quorum interval |
| observerModeInterval | raft.go:286 | MISSING | — | No observer mode interval |
| peerRemoveTimeout | raft.go:287 | MISSING | — | No peer remove timeout |
| Error sentinels | raft.go:319-343 | PARTIAL | — | .NET uses InvalidOperationException instead of typed error sentinels |
| noLeader / noVote constants | raft.go:4954-4956 | MISSING | — | No explicit no-leader/no-vote constants |
| paeDropThreshold / paeWarnThreshold | raft.go:4399-4401 | MISSING | — | No pending append entry limits |
| maxBatch / maxEntries | raft.go:3004-3005 | MISSING | — | No proposal batching thresholds |
| extensionState | raft.go:4462-4468 | MISSING | — | No domain extension state tracking |
---
## 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/Raft/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Raft/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap analysis completed: 196 items inventoried across 12 categories. Summary: 46 PORTED, 38 PARTIAL, 99 MISSING, 13 NOT_APPLICABLE, 0 DEFERRED. Wire format is well-ported; core state machine (run loop, catchup, WAL integration) is largely missing. | claude-opus |

186
gaps/routes.md Normal file
View File

@@ -0,0 +1,186 @@
# 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
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### `golang/nats-server/server/route.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `RouteType` (type alias + consts `Implicit`/`Explicit`) | route.go:3644 | 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:5694 | 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:97101 | MISSING | — | Used internally for deferred pool-connection creation after first PONG; no .NET equivalent |
| `gossipDefault`/`gossipDisabled`/`gossipOverride` consts | route.go:104108 | MISSING | — | Gossip mode bytes used in INFO propagation; not implemented in .NET |
| `connectInfo` struct | route.go:110124 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:328` (`BuildConnectInfoJson`) | .NET builds a simplified JSON payload; `connectInfo` fields Echo, Verbose, Pedantic, TLS, Headers, Cluster, Dynamic, LNOC, LNOCU are all MISSING from the .NET payload |
| `ConProto`/`InfoProto` protocol format strings | route.go:127130 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:73,83` | Handshake uses a simplified `ROUTE <serverId>` format rather than `CONNECT <json>` / `INFO <json>` |
| `clusterTLSInsecureWarning` const | route.go:134 | NOT_APPLICABLE | — | TLS not yet implemented in .NET port; warning string has no counterpart |
| `defaultRouteMaxPingInterval` const | route.go:140 | MISSING | — | Ping interval management for compression RTT auto-mode not implemented |
| `routeConnectDelay`/`routeConnectMaxDelay`/`routeMaxPingInterval` vars | route.go:145148 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:486` (250ms hardcoded delay) | .NET hardcodes 250ms retry delay; Go uses configurable `DEFAULT_ROUTE_CONNECT` with exponential backoff |
| `(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:187222` (`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:219221` | .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 <serverId>` 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 | MISSING | — | Async INFO broadcast to connected clients not implemented |
| `(s *Server) processImplicitRoute` | route.go:1043 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:107` (`ProcessImplicitRoute`) | .NET collects discovered URLs; missing: duplicate-ID check, pinned-account re-solicitation, `hasThisRouteConfigured` guard |
| `(s *Server) hasThisRouteConfigured` | route.go:1104 | MISSING | — | Check whether incoming gossip URL is already a configured explicit route; not implemented |
| `(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 | MISSING | — | Sub key parsing for account name extraction not implemented |
| `(c *client) getRoutedSubKeyInfo` | route.go:1290 | MISSING | — | Helper to determine account/key info for a route's subscriptions; not implemented |
| `(c *client) removeRemoteSubs` | route.go:1299 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:577` (`RemoveRoute`) | .NET removes the route connection but does NOT remove individual remote subscriptions from the SubList on close |
| `(c *client) removeRemoteSubsForAcc` | route.go:1352 | MISSING | — | Per-account remote sub removal for dedicated route transition not implemented |
| `(c *client) parseUnsubProto` | route.go:1366 | MISSING | — | RS-/LS- protocol arg parser not implemented; .NET `ReadFramesAsync` only extracts account/subject/queue loosely |
| `(c *client) processRemoteUnsub` | route.go:1404 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:177185` | .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:167175` | .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 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:95109` (`SendRsPlusAsync`/`SendRsMinusAsync`) | .NET sends RS+/RS- with account and optional queue; missing: LS+/LS- variant for leaf origin clusters, queue weight field in RS+ |
| `(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 | MISSING | — | Batch RS+ send not implemented |
| `(c *client) sendRouteUnSubProtos` | route.go:1890 | MISSING | — | Batch RS- send not implemented |
| `(c *client) sendRouteSubOrUnSubProtos` | route.go:1898 | MISSING | — | Low-level batch RS+/RS-/LS+/LS- sender not implemented |
| `(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 | MISSING | — | Helper to find a solicited route in a pool slice; not implemented |
| `upgradeRouteToSolicited` | route.go:2458 | MISSING | — | Upgrade an inbound route to solicited status; not implemented |
| `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:462` (`ConnectToRouteWithRetryAsync`) | .NET retries indefinitely with 250ms delay; missing: random jitter delay, explicit vs implicit distinction affecting delay, quit-channel integration |
| `(s *Server) routeStillValid` | route.go:2881 | MISSING | — | Check that a route URL is still in configured routes list (for reconnect guard) not implemented |
| `(s *Server) connectToRoute` | route.go:2890 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs:462` (`ConnectToRouteWithRetryAsync`) | .NET connects and retries; missing: explicit/implicit distinction, ConnectRetries limit, exponential backoff (`ConnectBackoff`), `routesToSelf` exclusion, address randomization from DNS |
| `(c *client) isSolicitedRoute` | route.go:2976 | MISSING | — | Helper predicate; not implemented |
| `(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:347354` | .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:577` (`RemoveRoute`) | .NET removes from `_routes` dict; missing: per-account route cleanup (`accRoutes`), hash removal, gateway/leafnode URL withdrawal, noPool counter, reconnect-after-noPool logic |
| `(s *Server) isDuplicateServerName` | route.go:3233 | MISSING | — | Duplicate server name detection across routes not implemented |
| `(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-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 |

143
gaps/stillmissing.md Normal file
View File

@@ -0,0 +1,143 @@
# LOC Comparison: Go Reference vs .NET Port
> Generated 2026-02-25 (gap inventory populated). Lines of code counted with `wc -l` (includes blanks and comments).
## Summary
| Metric | Go | .NET | .NET / Go |
|--------|---:|-----:|----------:|
| **Source files** | 123 | 249 | 2.0x |
| **Source LOC** | 134,140 | 51,303 | 38% |
| **Test files** | 127 | 562 | 4.4x |
| **Test LOC** | 260,730 | 165,630 | 64% |
| **Total files** | 250 | 811 | 3.2x |
| **Total LOC** | 394,870 | 216,933 | 55% |
The .NET port has **more files** (smaller, focused C# files vs Go's monolithic files) but **fewer total lines**. Source code is at 38% of Go LOC. Test code is at 64% of Go LOC. The higher file count reflects C#'s convention of one class/type per file.
---
## Source Code by Functional Area
| Functional Area | Go Files | Go LOC | .NET Files | .NET LOC | .NET/Go | Detail File | Notes |
|-----------------|:--------:|-------:|:----------:|----------|--------:|-------------|-------|
| Core Server | 10 | 21,223 | 13 | 3,597 | 17% | [core-server.md](core-server.md) | Go `server.go` + `client.go` = 13K+ combined |
| Protocol | 3 | 1,829 | 6 | 1,156 | 63% | [protocol.md](protocol.md) | Parser, wire protocol, constants |
| Subscriptions | 2 | 2,416 | 10 | 2,142 | 89% | [subscriptions.md](subscriptions.md) | Subject matching, trie, transforms |
| Auth & Accounts | 5 | 7,260 | 41 | 3,445 | 47% | [auth-and-accounts.md](auth-and-accounts.md) | .NET includes Imports/ (12 files, 264 LOC) |
| Configuration | 3 | 1,871 | 12 | 4,559 | 244% | [configuration.md](configuration.md) | .NET exceeds Go; includes rich config parsing |
| Routes | 1 | 3,314 | 3 | 1,139 | 34% | [routes.md](routes.md) | Cluster full-mesh routing |
| Gateways | 1 | 3,426 | 6 | 1,368 | 40% | [gateways.md](gateways.md) | Inter-cluster bridges |
| Leaf Nodes | 1 | 3,470 | 5 | 1,553 | 45% | [leaf-nodes.md](leaf-nodes.md) | Hub-and-spoke edge topology |
| **JetStream** | **20** | **55,228** | **68** | **16,630** | **30%** | [jetstream.md](jetstream.md) | Largest module; see breakdown below |
| RAFT | 1 | 5,037 | 20 | 3,045 | 60% | [raft.md](raft.md) | Consensus protocol |
| MQTT | 1 | 5,882 | 10 | 1,853 | 31% | [mqtt.md](mqtt.md) | MQTT protocol adapter |
| WebSocket | 1 | 1,550 | 8 | 1,531 | 99% | [websocket.md](websocket.md) | Near-complete |
| Monitoring | 2 | 4,396 | 15 | 2,308 | 53% | [monitoring.md](monitoring.md) | HTTP endpoints (/varz, /connz, etc.) |
| Events | 2 | 4,133 | 5 | 1,642 | 40% | [events.md](events.md) | System events, message tracing |
| TLS / Security | 12 | 4,207 | 7 | 516 | 12% | [tls-security.md](tls-security.md) | OCSP, certs, ciphersuites, proxy proto |
| Internal DS | 34 | 4,020 | 7 | 4,225 | 105% | [internal-ds.md](internal-ds.md) | AVL, stree, THW, GSL, PSE, sysmem |
| Logging | 4 | 936 | 0 | 0 | -- | [logging.md](logging.md) | .NET uses Microsoft.Extensions.Logging |
| Utilities & Other | 19 | 3,282 | 3 | 244 | 7% | [utilities-and-other.md](utilities-and-other.md) | Ring, sendq, errors, ats, elastic, tpm |
| Misc / Uncategorized | 1 | 660 | 6 | 70 | -- | [misc-uncategorized.md](misc-uncategorized.md) | |
| **TOTAL** | **123** | **134,140** | **249** | **51,303** | **38%** | | |
### JetStream Breakdown
| JetStream Sub-area | Go Files | Go LOC | .NET Files | .NET LOC | .NET/Go |
|--------------------|:--------:|-------:|:----------:|----------|--------:|
| Core (orchestration, API, stream, consumer, store) | 10 | 28,150 | 5 | 1,656 | 6% |
| API handlers | -- | (in Core) | 13 | 2,011 | -- |
| Consumers | -- | (in Core) | 10 | 2,147 | -- |
| Models | -- | (in Core) | 4 | 230 | -- |
| Publish | -- | (in Core) | 5 | 639 | -- |
| MirrorSource | -- | (in Core) | 2 | 1,085 | -- |
| Snapshots | -- | (in Core) | 1 | 210 | -- |
| Validation | -- | (in Core) | 1 | 60 | -- |
| Storage (filestore, memstore, dirstore) | 9 | 16,191 | 19 | 6,254 | 39% |
| Cluster (jetstream_cluster.go) | 1 | 10,887 | 8 | 2,338 | 21% |
| **JetStream Total** | **20** | **55,228** | **68** | **16,630** | **30%** |
Go consolidates JetStream into a few massive files: `consumer.go` (~6K), `jetstream_api.go` (~5K), `stream.go` (~8K), `filestore.go` (~12K), `jetstream_cluster.go` (~11K). The .NET port decomposes these into smaller, focused files.
---
## Test Code by Functional Area
| Functional Area | Go Files | Go LOC | .NET Files | .NET LOC | .NET/Go |
|-----------------|:--------:|-------:|:----------:|----------|--------:|
| Core Server | 7 | 18,011 | 173* | 25,024* | 139%* |
| Protocol | 5 | 2,020 | 4 | 1,433 | 71% |
| Subscriptions | 2 | 2,711 | 8 | 1,211 | 45% |
| Auth & Accounts | 6 | 15,448 | 26 | 10,170 | 66% |
| Configuration | 3 | 5,281 | 12 | 8,237 | 156% |
| Routes | 1 | 5,041 | 18 | 6,025 | 120% |
| Gateways | 1 | 7,667 | 17 | 5,914 | 77% |
| Leaf Nodes | 2 | 11,778 | 22 | 7,238 | 61% |
| JetStream (all) | 22 | 105,284 | 160 | 65,314 | 62% |
| RAFT | 3 | 6,140 | 33 | 9,154 | 149% |
| MQTT | 3 | 9,793 | 25 | 5,953 | 61% |
| WebSocket | 1 | 4,982 | 13 | 2,590 | 52% |
| Monitoring | 1 | 6,603 | 19 | 5,989 | 91% |
| Events | 2 | 9,344 | 8 | 3,091 | 33% |
| NoRace / Stress | 2 | 12,009 | 3 | 2,342 | 20% |
| Internal DS | 7 | 3,557 | 6 | 3,837 | 108% |
| Integration (test/) | 35 | 29,812 | -- | -- | -- |
| Other / Misc | 14 | 2,904 | 14 | 2,058 | 71% |
| **TOTAL** | **127** | **260,730** | **562** | **165,630** | **64%** |
*\* The .NET "Core Server" root test directory (173 files, 25,024 LOC) contains mixed-purpose tests spanning multiple functional areas (server lifecycle, client protocol, pubsub, integration). Not directly comparable to the Go "Core Server" test files.*
### JetStream Test Breakdown
| JetStream Test Area | Go Files | Go LOC | .NET Files | .NET LOC | .NET/Go |
|---------------------|:--------:|-------:|:----------:|----------|--------:|
| Core tests | 9 | 42,582 | 50* | 14,824* | 35% |
| Storage tests | 4 | 16,930 | 32 | 14,672 | 87% |
| Cluster tests | 8 | 43,426 | 41 | 24,737 | 57% |
| Consumer tests | -- | (in Core) | 22 | 7,701 | -- |
| API tests | -- | (in Core) | 9 | 2,162 | -- |
| Other (mirror, snap, stream) | -- | (in Core) | 6 | 1,918 | -- |
| Benchmark | 1 | 2,346 | -- | -- | -- |
| **JetStream Test Total** | **22** | **105,284** | **160** | **65,314** | **62%** |
*\* JetStream root-level test files not in a named subdirectory.*
---
## Areas Where .NET Exceeds Go LOC
| Area | Go LOC | .NET LOC | Ratio | Explanation |
|------|-------:|----------|------:|-------------|
| Configuration | 1,871 | 4,559 | 2.4x | Richer config parser with lexer/token model |
| Internal DS | 4,020 | 4,225 | 1.05x | Full parity on AVL, stree, THW, GSL |
| WebSocket | 1,550 | 1,531 | 0.99x | Near-identical coverage |
---
## Largest Gaps (Source LOC)
| Area | Go LOC | .NET LOC | Gap (LOC) | .NET/Go |
|------|-------:|----------|----------:|--------:|
| Core Server | 21,223 | 3,877 | 17,346 | 18% |
| JetStream Core | 28,150 | 8,038 | 20,112 | 29% |
| JetStream Storage | 16,191 | 6,254 | 9,937 | 39% |
| JetStream Cluster | 10,887 | 2,338 | 8,549 | 21% |
| Auth & Accounts | 7,260 | 3,445 | 3,815 | 47% |
| MQTT | 5,882 | 1,853 | 4,029 | 31% |
| Monitoring | 4,396 | 2,308 | 2,088 | 53% |
| Events | 4,133 | 1,642 | 2,491 | 40% |
| TLS / Security | 4,207 | 516 | 3,691 | 12% |
| Utilities & Other | 3,282 | 244 | 3,038 | 7% |
---
## Notes
- **LOC counts include blank lines and comments.** Go tends to be more verbose with explicit error handling; C# uses exceptions and has less boilerplate per line. A 1:1 LOC ratio is not the goal.
- **Go's monolithic files vs .NET's granular files.** Go's `client.go` is 6,700+ lines; the .NET equivalent is split across `NatsClient.cs`, `ClientFlags.cs`, `ClientKind.cs`, `INatsClient.cs`, etc. File count comparisons favor .NET.
- **Go `test/` directory** (35 files, 29,812 LOC) contains integration/acceptance tests with no direct .NET equivalent directory; .NET integration tests are spread across subdirectories.
- **Go `norace_*_test.go`** files (12,009 LOC) contain long-running race-condition tests that cross multiple subsystems. .NET equivalents are in `Stress/` (2,342 LOC).
- **Configuration .NET > Go** because .NET implements a full lexer/parser/token pipeline for the NATS config format rather than using Go's simpler approach.
- **.NET Logging = 0 LOC** because it uses `Microsoft.Extensions.Logging` + `Serilog` as NuGet packages rather than custom logging code.
- **Internal Data Structures .NET >= Go** — the AVL, subject tree, time hash wheel, and GSL implementations are at or above Go parity.

232
gaps/subscriptions.md Normal file
View File

@@ -0,0 +1,232 @@
# Subscriptions — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Subscriptions** 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 Subscriptions
- The `Sublist` trie is **performance-critical** — every published message triggers a `Match()` call.
- Cache invalidation uses atomic generation counters (`Interlocked` in .NET).
- `subject_transform.go` handles subject mapping for imports/exports — check if the .NET `Imports/` directory covers this.
---
## Go Reference Files (Source)
- `golang/nats-server/server/sublist.go` — Trie-based subject matcher with wildcard support (`*` single, `>` multi). Nodes have `psubs` (plain), `qsubs` (queue groups). Results cached with atomic generation IDs.
- `golang/nats-server/server/subject_transform.go` — Subject transformation logic (mapping, filtering)
## Go Reference Files (Tests)
- `golang/nats-server/server/sublist_test.go`
- `golang/nats-server/server/subject_transform_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Subscriptions/SubjectMatch.cs`
- `src/NATS.Server/Subscriptions/SubList.cs`
- `src/NATS.Server/Subscriptions/SubListResult.cs`
- `src/NATS.Server/Subscriptions/Subscription.cs`
- All other files in `src/NATS.Server/Subscriptions/`
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/Subscriptions/`
- `tests/NATS.Server.Tests/SubList/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### golang/nats-server/server/sublist.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `SublistResult` (struct) | sublist.go:59 | PORTED | `src/NATS.Server/Subscriptions/SubListResult.cs:4` | Fields renamed: `psubs`→`PlainSubs`, `qsubs`→`QueueSubs`; .NET uses arrays instead of slices |
| `Sublist` (struct) | sublist.go:65 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:11` | Core trie struct ported; notification model differs (event vs channels) |
| `notifyMaps` (struct) | sublist.go:80 | NOT_APPLICABLE | — | Go uses buffered channels for interest notifications; .NET uses `InterestChanged` event (`InterestChange.cs`) |
| `node` (struct) | sublist.go:87 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:974` (private `TrieNode`) | Inner class; `psubs`+`plist` merged into `PlainSubs HashSet`, `qsubs`→`QueueSubs Dictionary` |
| `level` (struct) | sublist.go:96 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:967` (private `TrieLevel`) | Inner class; `nodes`, `pwc`, `fwc` all present |
| `newNode()` | sublist.go:102 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:974` | Inline `new TrieNode()` |
| `newLevel()` | sublist.go:107 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:967` | Inline `new TrieLevel()` |
| `NewSublist(bool)` | sublist.go:117 | PARTIAL | `src/NATS.Server/Subscriptions/SubList.cs:11` | .NET `SubList` always starts with cache; no `enableCache` constructor param |
| `NewSublistWithCache()` | sublist.go:125 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:11` | Default `new SubList()` has cache enabled |
| `NewSublistNoCache()` | sublist.go:130 | MISSING | — | No .NET equivalent; SubList always caches |
| `CacheEnabled()` | sublist.go:135 | MISSING | — | No public method to query whether cache is on; `CacheCount` exists but different semantics |
| `RegisterNotification()` | sublist.go:149 | MISSING | — | Go sends `true/false` on channel when first/last interest added/removed; .NET uses `InterestChanged` event which fires on every change but doesn't replicate channel-based deferred notify semantics |
| `RegisterQueueNotification()` | sublist.go:153 | MISSING | — | Queue-specific interest notification; no .NET equivalent |
| `ClearNotification()` | sublist.go:227 | MISSING | — | No .NET equivalent for removing a notification channel |
| `ClearQueueNotification()` | sublist.go:231 | MISSING | — | No .NET equivalent |
| `chkForInsertNotification()` | sublist.go:301 | NOT_APPLICABLE | — | Internal helper for channel notification; replaced by `InterestChanged` event emission in `Insert()` |
| `chkForRemoveNotification()` | sublist.go:317 | NOT_APPLICABLE | — | Internal helper; replaced by `InterestChanged` event emission in `Remove()` |
| `sendNotification()` | sublist.go:253 | NOT_APPLICABLE | — | Non-blocking channel send; Go-specific pattern, no .NET equivalent needed |
| `addInsertNotify()` | sublist.go:262 | NOT_APPLICABLE | — | Internal channel registration helper; replaced by event model |
| `addRemoveNotify()` | sublist.go:268 | NOT_APPLICABLE | — | Internal channel registration helper |
| `addNotify()` | sublist.go:274 | NOT_APPLICABLE | — | Internal channel map helper |
| `keyFromSubjectAndQueue()` | sublist.go:290 | NOT_APPLICABLE | — | Key generation for notification maps; not needed in .NET event model |
| `Insert()` | sublist.go:356 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:179` | Full trie insertion with queue support; fires `InterestChanged` event instead of channel notify |
| `copyResult()` | sublist.go:449 | NOT_APPLICABLE | — | Go uses copy-on-write for cache mutation; .NET creates new `SubListResult` directly |
| `SublistResult.addSubToResult()` | sublist.go:460 | NOT_APPLICABLE | — | Copy-on-write pattern for cache updates; not needed in .NET generation-based cache invalidation |
| `addToCache()` | sublist.go:478 | NOT_APPLICABLE | — | Go updates individual cache entries on insert; .NET uses generation-based invalidation (simpler) |
| `removeFromCache()` | sublist.go:498 | NOT_APPLICABLE | — | Same as above; .NET bumps generation counter instead |
| `Match()` | sublist.go:520 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:360` | Generation-based cache; same algorithm |
| `MatchBytes()` | sublist.go:526 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:425` | Takes `ReadOnlySpan<byte>` vs `[]byte` |
| `HasInterest()` | sublist.go:532 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:638` | Cache-aware fast path |
| `NumInterest()` | sublist.go:537 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:669` | Returns `(plainCount, queueCount)` tuple |
| `matchNoLock()` | sublist.go:543 | NOT_APPLICABLE | — | Internal; .NET lock model differs (no equivalent needed) |
| `match()` | sublist.go:547 | NOT_APPLICABLE | — | Internal implementation; split into `Match()` + private helpers in .NET |
| `hasInterest()` | sublist.go:624 | NOT_APPLICABLE | — | Internal; maps to `HasInterestLevel()` private helper |
| `reduceCacheCount()` | sublist.go:675 | PORTED | `src/NATS.Server/Subscriptions/SubListCacheSweeper.cs:7` + `SubList.cs:458` | Background goroutine mapped to `SubListCacheSweeper` + `SweepCache()` |
| `isRemoteQSub()` | sublist.go:689 | NOT_APPLICABLE | — | Go has client.kind == ROUTER/LEAF; .NET uses `RemoteSubscription` model instead |
| `UpdateRemoteQSub()` | sublist.go:695 | MISSING | — | Go updates weight of remote qsub; .NET uses `ApplyRemoteSub()` for a different model (full add/remove) |
| `addNodeToResults()` | sublist.go:706 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:549` (private `AddNodeToResults()`) | Remote qsub weight expansion present in Go missing in .NET |
| `findQSlot()` | sublist.go:745 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:562` (inline in `AddNodeToResults`) | Inlined in .NET |
| `matchLevel()` | sublist.go:757 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:506` (private `MatchLevel()`) | Core trie descent algorithm |
| `matchLevelForAny()` | sublist.go:785 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:754` (private `HasInterestLevel()`) + `CountInterestLevel()` | Split into two .NET helpers |
| `remove()` (internal) | sublist.go:843 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:282` (private `RemoveInternal()`) | Internal removal logic |
| `Remove()` | sublist.go:915 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:255` | Fires `InterestChanged` event |
| `RemoveBatch()` | sublist.go:920 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:687` | Cache disable/re-enable pattern ported |
| `level.pruneNode()` | sublist.go:953 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:345` (inline in `RemoveInternal`) | Inlined in removal path |
| `node.isEmpty()` | sublist.go:968 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:981` (`TrieNode.IsEmpty` property) | Property instead of method |
| `level.numNodes()` | sublist.go:978 | NOT_APPLICABLE | — | Used internally in `visitLevel()`; no .NET equivalent needed |
| `removeFromNode()` | sublist.go:993 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:324` (inline in `RemoveInternal`) | Inlined |
| `Count()` | sublist.go:1023 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:41` (`Count` property) | |
| `CacheCount()` | sublist.go:1030 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:97` (`CacheCount` property) | |
| `SublistStats` (struct) | sublist.go:1038 | PORTED | `src/NATS.Server/Subscriptions/SubListStats.cs:3` | All public fields present; unexported fields `totFanout`, `cacheCnt`, `cacheHits` dropped (computed inline in `Stats()`) |
| `SublistStats.add()` | sublist.go:1052 | MISSING | — | Aggregates multiple SublistStats into one; used for cluster monitoring; no .NET equivalent |
| `Stats()` | sublist.go:1076 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:580` | Full fanout stats computed |
| `numLevels()` | sublist.go:1120 | MISSING | — | Debug/test utility counting trie depth; not ported |
| `visitLevel()` | sublist.go:1126 | MISSING | — | Internal helper for `numLevels()`; not ported |
| `subjectHasWildcard()` | sublist.go:1159 | PARTIAL | `src/NATS.Server/Subscriptions/SubjectMatch.cs:50` (`IsLiteral()` — inverse) | .NET `!IsLiteral()` is equivalent but not a dedicated function |
| `subjectIsLiteral()` | sublist.go:1174 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:50` (`IsLiteral()`) | Exact equivalent |
| `IsValidPublishSubject()` | sublist.go:1187 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:67` | |
| `IsValidSubject()` | sublist.go:1192 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:9` | |
| `isValidSubject()` (internal, checkRunes) | sublist.go:1196 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:211` (`IsValidSubject(string, bool)`) | |
| `IsValidLiteralSubject()` | sublist.go:1236 | PARTIAL | `src/NATS.Server/Subscriptions/SubjectMatch.cs:50` (`IsLiteral()`) | `IsLiteral()` does not validate the subject first; `IsValidPublishSubject()` combines both |
| `isValidLiteralSubject()` (tokens iter) | sublist.go:1241 | NOT_APPLICABLE | — | Takes `iter.Seq[string]` (Go 1.23 iterator); C# uses different iteration model |
| `ValidateMapping()` | sublist.go:1258 | MISSING | — | Validates a mapping destination subject string including `{{function()}}` syntax; no public .NET equivalent |
| `analyzeTokens()` | sublist.go:1298 | NOT_APPLICABLE | — | Internal helper used only in `SubjectsCollide()`; logic inlined in .NET `SubjectsCollide()` |
| `tokensCanMatch()` | sublist.go:1314 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:199` (private `TokensCanMatch()`) | |
| `SubjectsCollide()` | sublist.go:1326 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:159` | |
| `numTokens()` | sublist.go:1374 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:118` (`NumTokens()`) | |
| `tokenAt()` | sublist.go:1389 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:132` (`TokenAt()`) | Go is 1-based index; .NET is 0-based |
| `tokenizeSubjectIntoSlice()` | sublist.go:1407 | NOT_APPLICABLE | — | Internal slice-reuse helper; .NET uses `Tokenize()` private method in SubList |
| `SubjectMatchesFilter()` | sublist.go:1421 | PARTIAL | `src/NATS.Server/JetStream/Storage/MemStore.cs:1175` (private), `FileStore.cs:773` (private) | Duplicated as private methods in MemStore and FileStore; not a public standalone function |
| `subjectIsSubsetMatch()` | sublist.go:1426 | MISSING | — | No public .NET equivalent; logic exists privately in MemStore/FileStore |
| `isSubsetMatch()` | sublist.go:1434 | MISSING | — | Internal; no public .NET equivalent |
| `isSubsetMatchTokenized()` | sublist.go:1444 | MISSING | — | Internal; no public .NET equivalent |
| `matchLiteral()` | sublist.go:1483 | PORTED | `src/NATS.Server/Subscriptions/SubjectMatch.cs:75` (`MatchLiteral()`) | |
| `addLocalSub()` | sublist.go:1552 | NOT_APPLICABLE | — | Filters by client kind (CLIENT/SYSTEM/JETSTREAM/ACCOUNT/LEAF); no .NET equivalent needed (client kind routing done elsewhere) |
| `Sublist.addNodeToSubs()` | sublist.go:1562 | NOT_APPLICABLE | — | Internal helper for `localSubs()`; not ported |
| `Sublist.collectLocalSubs()` | sublist.go:1581 | NOT_APPLICABLE | — | Internal helper for `localSubs()`; not ported |
| `Sublist.localSubs()` | sublist.go:1597 | MISSING | — | Returns only local-client subscriptions (excludes routes/gateways); no .NET equivalent |
| `Sublist.All()` | sublist.go:1604 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:712` | |
| `Sublist.addAllNodeToSubs()` | sublist.go:1610 | NOT_APPLICABLE | — | Internal helper for `All()`; inlined in .NET |
| `Sublist.collectAllSubs()` | sublist.go:1627 | NOT_APPLICABLE | — | Internal; inlined in `CollectAllSubs()` private method in .NET |
| `ReverseMatch()` | sublist.go:1649 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:727` | |
| `reverseMatchLevel()` | sublist.go:1674 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:860` (private `ReverseMatchLevel()`) | |
| `getAllNodes()` | sublist.go:1714 | PORTED | `src/NATS.Server/Subscriptions/SubList.cs:909` (private `CollectAllNodes()`) | |
### golang/nats-server/server/subject_transform.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Transform type constants (`NoTransform`…`Random`) | subject_transform.go:43 | PARTIAL | `src/NATS.Server/Subscriptions/SubjectTransform.cs:682` (private `TransformType` enum) | `Random` (value 11 in Go) is absent from the .NET enum and switch statement; all others ported |
| `subjectTransform` (struct) | subject_transform.go:61 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:11` | Internal fields mapped to `_source`, `_dest`, `_sourceTokens`, `_destTokens`, `_ops` |
| `SubjectTransformer` (interface) | subject_transform.go:73 | NOT_APPLICABLE | — | Go exports interface for polymorphism; C# uses concrete `SubjectTransform` class directly |
| `NewSubjectTransformWithStrict()` | subject_transform.go:81 | MISSING | — | Strict mode validates that all source wildcards are used in dest; no .NET equivalent |
| `NewSubjectTransform()` | subject_transform.go:198 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:31` (`Create()`) | Non-strict creation |
| `NewSubjectTransformStrict()` | subject_transform.go:202 | MISSING | — | Strict version for import mappings; no .NET equivalent |
| `getMappingFunctionArgs()` | subject_transform.go:206 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:639` (private `GetFunctionArgs()`) | |
| `transformIndexIntArgsHelper()` | subject_transform.go:215 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:610` (private `ParseIndexIntArgs()`) | |
| `indexPlaceHolders()` | subject_transform.go:237 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:486` (`ParseDestToken()` + `ParseMustacheToken()`) | Split into two methods; `Random` branch missing |
| `transformTokenize()` | subject_transform.go:378 | MISSING | — | Converts `foo.*.*` to `foo.$1.$2`; used for import subject mapping reversal; no .NET equivalent |
| `transformUntokenize()` | subject_transform.go:399 | MISSING | — | Inverse of above; used in `reverse()`; no .NET equivalent |
| `tokenizeSubject()` | subject_transform.go:414 | NOT_APPLICABLE | — | Internal tokenizer; .NET uses `string.Split('.')` or `Tokenize()` private method |
| `subjectTransform.Match()` | subject_transform.go:433 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:126` (`Apply()`) | Renamed; returns `null` instead of error on no-match |
| `subjectTransform.TransformSubject()` | subject_transform.go:456 | PARTIAL | `src/NATS.Server/Subscriptions/SubjectTransform.cs:126` (via `Apply()`) | `TransformSubject` (apply without match check) not separately exposed; `Apply()` always checks match |
| `subjectTransform.getRandomPartition()` | subject_transform.go:460 | MISSING | — | `Random` transform type not implemented in .NET |
| `subjectTransform.getHashPartition()` | subject_transform.go:469 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:226` (`ComputePartition()` + `Fnv1A32()`) | FNV-1a 32-bit hash ported |
| `subjectTransform.TransformTokenizedSubject()` | subject_transform.go:482 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:144` (private `TransformTokenized()`) | All transform types except `Random` ported |
| `subjectTransform.reverse()` | subject_transform.go:638 | MISSING | — | Produces the inverse transform; used for import subject mapping; no .NET equivalent |
| `subjectInfo()` | subject_transform.go:666 | PORTED | `src/NATS.Server/Subscriptions/SubjectTransform.cs:451` (private `SubjectInfo()`) | |
| `ValidateMapping()` (in subject_transform.go context) | sublist.go:1258 | MISSING | — | Also defined via `NewSubjectTransform`; validates mapping destination with function syntax; no .NET public equivalent |
---
## 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/Subscriptions/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/Subscriptions/ tests/NATS.Server.Tests/SubList/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated: analyzed sublist.go (~1,729 lines) and subject_transform.go (~689 lines) against all .NET Subscriptions/*.cs files. Counted 49 PORTED, 6 PARTIAL, 22 MISSING, 27 NOT_APPLICABLE, 0 DEFERRED. | claude-sonnet-4-6 |

340
gaps/tls-security.md Normal file
View File

@@ -0,0 +1,340 @@
# TLS / Security — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **TLS / Security** 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 TLS / Security
- This category is at **12% LOC parity** — one of the largest gaps.
- OCSP stapling and peer validation are critical for mutual TLS deployments.
- Cipher suite filtering allows operators to restrict which TLS ciphers are accepted.
- PROXY protocol support enables running NATS behind HAProxy/nginx with real client IPs.
- `certstore` provides OS certificate store access (Windows-specific parts may be NOT_APPLICABLE).
- `certidp` implements certificate identity validation via OCSP responders.
---
## Go Reference Files (Source)
- `golang/nats-server/server/ocsp.go` — OCSP stapling configuration
- `golang/nats-server/server/ocsp_peer.go` — OCSP peer certificate validation
- `golang/nats-server/server/ocsp_responsecache.go` — OCSP response caching
- `golang/nats-server/server/ciphersuites.go` — TLS cipher suite management and filtering
- `golang/nats-server/server/client_proxyproto.go` — PROXY protocol v1/v2 support (HAProxy)
- `golang/nats-server/server/certidp/certidp.go` — Certificate identity provider
- `golang/nats-server/server/certidp/messages.go` — CertIDP message types
- `golang/nats-server/server/certidp/ocsp_responder.go` — OCSP responder client
- `golang/nats-server/server/certstore/certstore.go` — OS certificate store access
- `golang/nats-server/server/certstore/certstore_other.go` — Non-Windows cert store
- `golang/nats-server/server/certstore/certstore_windows.go` — Windows cert store
- `golang/nats-server/server/certstore/errors.go` — Cert store errors
## Go Reference Files (Tests)
- `golang/nats-server/test/ocsp_test.go` (integration)
- `golang/nats-server/test/ocsp_peer_test.go` (integration)
- `golang/nats-server/test/tls_test.go` (integration)
- `golang/nats-server/server/certstore/certstore_windows_test.go`
- `golang/nats-server/server/certidp/*_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/Tls/TlsHelper.cs`
- `src/NATS.Server/Tls/TlsCertificateProvider.cs`
- `src/NATS.Server/Tls/TlsConnectionState.cs`
- `src/NATS.Server/Tls/TlsConnectionWrapper.cs`
- `src/NATS.Server/Tls/TlsRateLimiter.cs`
- `src/NATS.Server/Tls/PeekableStream.cs`
- `src/NATS.Server/Tls/OcspConfig.cs`
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/` (TLS-related test files in root)
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### ocsp.go — OCSP stapling configuration
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| OCSPMode (type) | golang/nats-server/server/ocsp.go:46 | PORTED | src/NATS.Server/Tls/OcspConfig.cs:8 | Mapped to `OcspMode` enum with matching values |
| OCSPModeAuto | golang/nats-server/server/ocsp.go:49 | PORTED | src/NATS.Server/Tls/OcspConfig.cs:10 | `OcspMode.Auto = 0` |
| OCSPModeAlways | golang/nats-server/server/ocsp.go:52 | PORTED | src/NATS.Server/Tls/OcspConfig.cs:11 | `OcspMode.Always = 1` |
| OCSPModeNever | golang/nats-server/server/ocsp.go:55 | PORTED | src/NATS.Server/Tls/OcspConfig.cs:13 | `OcspMode.Never = 3` |
| OCSPModeMust | golang/nats-server/server/ocsp.go:60 | PORTED | src/NATS.Server/Tls/OcspConfig.cs:12 | `OcspMode.Must = 2` |
| OCSPMonitor (struct) | golang/nats-server/server/ocsp.go:65 | MISSING | — | No dedicated OCSP monitor struct; .NET delegates to SslStreamCertificateContext for stapling |
| OCSPMonitor.getNextRun | golang/nats-server/server/ocsp.go:80 | MISSING | — | No periodic OCSP refresh loop |
| OCSPMonitor.getStatus | golang/nats-server/server/ocsp.go:102 | MISSING | — | No cache/local/remote OCSP status retrieval |
| OCSPMonitor.getCacheStatus | golang/nats-server/server/ocsp.go:119 | MISSING | — | No in-memory OCSP response caching |
| OCSPMonitor.getLocalStatus | golang/nats-server/server/ocsp.go:125 | MISSING | — | No file-based OCSP status persistence |
| OCSPMonitor.getRemoteStatus | golang/nats-server/server/ocsp.go:160 | MISSING | — | No manual OCSP responder HTTP callout; .NET relies on runtime OCSP |
| OCSPMonitor.run | golang/nats-server/server/ocsp.go:272 | MISSING | — | No background goroutine for OCSP staple renewal |
| OCSPMonitor.stop | golang/nats-server/server/ocsp.go:348 | MISSING | — | No explicit stop channel for OCSP monitor |
| Server.NewOCSPMonitor | golang/nats-server/server/ocsp.go:357 | PARTIAL | src/NATS.Server/Tls/TlsHelper.cs:75 | `BuildCertificateContext` delegates stapling to .NET runtime; missing per-kind config, VerifyConnection callbacks, shutdown-on-revoke |
| Server.setupOCSPStapleStoreDir | golang/nats-server/server/ocsp.go:560 | MISSING | — | No OCSP store directory setup |
| tlsConfigKind (struct) | golang/nats-server/server/ocsp.go:577 | MISSING | — | No per-listener TLS config kind abstraction |
| Server.configureOCSP | golang/nats-server/server/ocsp.go:585 | MISSING | — | No multi-listener OCSP configuration loop |
| Server.enableOCSP | golang/nats-server/server/ocsp.go:680 | MISSING | — | No server-level OCSP enablement orchestrator |
| Server.startOCSPMonitoring | golang/nats-server/server/ocsp.go:717 | MISSING | — | No OCSP monitor goroutine dispatcher |
| Server.reloadOCSP | golang/nats-server/server/ocsp.go:734 | MISSING | — | No OCSP hot-reload support |
| hasOCSPStatusRequest | golang/nats-server/server/ocsp.go:804 | MISSING | — | No MustStaple TLS extension detection |
| OCSPMonitor.writeOCSPStatus | golang/nats-server/server/ocsp.go:840 | MISSING | — | No atomic file write for OCSP status persistence |
| parseCertPEM | golang/nats-server/server/ocsp.go:867 | PARTIAL | src/NATS.Server/Tls/TlsHelper.cs:17 | `LoadCaCertificates` uses `ImportFromPemFile` but does not validate PEM block type |
| getOCSPIssuerLocally | golang/nats-server/server/ocsp.go:892 | MISSING | — | No local issuer resolution from cert bundle |
| getOCSPIssuer | golang/nats-server/server/ocsp.go:932 | MISSING | — | No issuer resolution logic |
| ocspStatusString | golang/nats-server/server/ocsp.go:968 | PORTED | src/NATS.Server/Events/EventTypes.cs:647 | `OcspEventBuilder.ParseStatus` and `OcspStatus` enum |
| validOCSPResponse | golang/nats-server/server/ocsp.go:979 | MISSING | — | No manual OCSP response time validation |
### ocsp_peer.go — OCSP peer certificate validation
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| parseOCSPPeer | golang/nats-server/server/ocsp_peer.go:29 | MISSING | — | No config-file parsing for OCSP peer options |
| peerFromVerifiedChains | golang/nats-server/server/ocsp_peer.go:130 | MISSING | — | No peer extraction from verified chains |
| Server.plugTLSOCSPPeer | golang/nats-server/server/ocsp_peer.go:138 | PARTIAL | src/NATS.Server/Tls/TlsHelper.cs:36 | .NET uses X509RevocationMode.Online when OcspPeerVerify set; missing full OCSP peer plugin pattern with per-chain validation |
| Server.plugClientTLSOCSPPeer | golang/nats-server/server/ocsp_peer.go:163 | PARTIAL | src/NATS.Server/Tls/TlsHelper.cs:41 | RemoteCertificateValidationCallback with revocation check, but no OCSP-specific chain walking or event publishing |
| Server.plugServerTLSOCSPPeer | golang/nats-server/server/ocsp_peer.go:183 | MISSING | — | No leaf-spoke server-side OCSP peer plug |
| Server.tlsServerOCSPValid | golang/nats-server/server/ocsp_peer.go:209 | MISSING | — | No server-side verified-chain OCSP evaluation |
| Server.tlsClientOCSPValid | golang/nats-server/server/ocsp_peer.go:220 | MISSING | — | No client-side verified-chain OCSP evaluation |
| Server.peerOCSPValid | golang/nats-server/server/ocsp_peer.go:225 | MISSING | — | No multi-chain OCSP eligibility walker |
| Server.certOCSPGood | golang/nats-server/server/ocsp_peer.go:286 | MISSING | — | No per-link OCSP response fetch, cache check, delegation validation, and policy overrides (WarnOnly, AllowWhenCAUnreachable) |
| sendOCSPPeerRejectEvent | golang/nats-server/server/ocsp_peer.go (ref) | PARTIAL | src/NATS.Server/Events/EventTypes.cs:612 | `OcspEventBuilder.BuildPeerReject` exists as data builder but no actual event publishing to system account |
| sendOCSPPeerChainlinkInvalidEvent | golang/nats-server/server/ocsp_peer.go (ref) | PARTIAL | src/NATS.Server/Events/EventTypes.cs:628 | `OcspEventBuilder.BuildChainValidation` exists as data builder but no actual event publishing |
### ocsp_responsecache.go — OCSP response caching
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| OCSPResponseCacheType (type) | golang/nats-server/server/ocsp_responsecache.go:44 | MISSING | — | No cache type enum (NONE/LOCAL) |
| OCSPResponseCacheConfig | golang/nats-server/server/ocsp_responsecache.go:56 | MISSING | — | No OCSP cache configuration struct |
| NewOCSPResponseCacheConfig | golang/nats-server/server/ocsp_responsecache.go:63 | MISSING | — | No cache config factory |
| OCSPResponseCacheStats | golang/nats-server/server/ocsp_responsecache.go:72 | MISSING | — | No cache statistics tracking |
| OCSPResponseCacheItem | golang/nats-server/server/ocsp_responsecache.go:81 | MISSING | — | No cache item model |
| OCSPResponseCache (interface) | golang/nats-server/server/ocsp_responsecache.go:89 | MISSING | — | No cache interface (Put/Get/Delete/Start/Stop) |
| NoOpCache (struct) | golang/nats-server/server/ocsp_responsecache.go:102 | MISSING | — | No no-op cache implementation |
| LocalCache (struct) | golang/nats-server/server/ocsp_responsecache.go:155 | MISSING | — | No local file-backed OCSP response cache |
| LocalCache.Put | golang/nats-server/server/ocsp_responsecache.go:167 | MISSING | — | No cache insert with S2 compression |
| LocalCache.Get | golang/nats-server/server/ocsp_responsecache.go:201 | MISSING | — | No cache lookup with decompression |
| LocalCache.Delete | golang/nats-server/server/ocsp_responsecache.go:245 | MISSING | — | No cache delete with preserve-revoked policy |
| LocalCache.Start | golang/nats-server/server/ocsp_responsecache.go:272 | MISSING | — | No cache initialization from disk |
| LocalCache.Stop | golang/nats-server/server/ocsp_responsecache.go:281 | MISSING | — | No cache shutdown and save |
| LocalCache.Compress / Decompress | golang/nats-server/server/ocsp_responsecache.go:344 | MISSING | — | No S2 compression for OCSP responses |
| LocalCache.loadCache / saveCache | golang/nats-server/server/ocsp_responsecache.go:371 | MISSING | — | No JSON-based disk persistence |
| Server.initOCSPResponseCache | golang/nats-server/server/ocsp_responsecache.go:508 | MISSING | — | No cache initialization on server startup |
| Server.startOCSPResponseCache | golang/nats-server/server/ocsp_responsecache.go:546 | MISSING | — | No cache start on server startup |
| Server.stopOCSPResponseCache | golang/nats-server/server/ocsp_responsecache.go:564 | MISSING | — | No cache stop on server shutdown |
| parseOCSPResponseCache | golang/nats-server/server/ocsp_responsecache.go:574 | MISSING | — | No config-file parsing for OCSP cache options |
### ciphersuites.go — TLS cipher suite management and filtering
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| cipherMap (var) | golang/nats-server/server/ciphersuites.go:32 | MISSING | — | No cipher name-to-suite mapping; .NET handles cipher suites via SslServerAuthenticationOptions |
| cipherMapByID (var) | golang/nats-server/server/ciphersuites.go:33 | MISSING | — | No cipher ID-to-suite mapping |
| defaultCipherSuites | golang/nats-server/server/ciphersuites.go:35 | NOT_APPLICABLE | — | .NET TLS stack manages default cipher suites internally; no explicit enumeration needed |
| curvePreferenceMap (var) | golang/nats-server/server/ciphersuites.go:45 | NOT_APPLICABLE | — | .NET does not expose curve preference ordering at application level |
| defaultCurvePreferences | golang/nats-server/server/ciphersuites.go:55 | NOT_APPLICABLE | — | .NET runtime handles curve negotiation; FIPS mode handled by OS/.NET runtime config |
| init() cipher registration | golang/nats-server/server/ciphersuites.go:21 | NOT_APPLICABLE | — | Go-specific init pattern; .NET populates cipher lists differently |
### client_proxyproto.go — PROXY protocol v1/v2 support
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| proxyProtoV2 constants | golang/nats-server/server/client_proxyproto.go:28 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:54 | All v2 constants mirrored |
| proxyProtoV1 constants | golang/nats-server/server/client_proxyproto.go:63 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:82 | All v1 constants mirrored |
| proxyProtoAddr (struct) | golang/nats-server/server/client_proxyproto.go:81 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:11 | `ProxyAddress` class with SrcIp, SrcPort, DstIp, DstPort |
| proxyProtoAddr.String | golang/nats-server/server/client_proxyproto.go:89 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:20 | `ProxyAddress.ToString()` with IPv6 bracket formatting |
| proxyProtoAddr.Network | golang/nats-server/server/client_proxyproto.go:94 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:18 | `ProxyAddress.Network` property |
| proxyConn (struct) | golang/nats-server/server/client_proxyproto.go:102 | MISSING | — | No connection wrapper; .NET parser is pure buffer-based, integration with accept loop not yet wired |
| proxyConn.RemoteAddr | golang/nats-server/server/client_proxyproto.go:109 | MISSING | — | Part of proxyConn; accept loop integration needed |
| detectProxyProtoVersion | golang/nats-server/server/client_proxyproto.go:116 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:91 | `ProxyProtocolParser.Parse` auto-detects v1/v2 from first 6 bytes |
| readProxyProtoV1Header | golang/nats-server/server/client_proxyproto.go:134 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:117 | `ProxyProtocolParser.ParseV1` — buffer-based, all edge cases handled |
| readProxyProtoHeader | golang/nats-server/server/client_proxyproto.go:226 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:91 | `ProxyProtocolParser.Parse` covers both versions |
| readProxyProtoV2Header | golang/nats-server/server/client_proxyproto.go:274 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:190 | `ProxyProtocolParser.ParseV2` |
| parseProxyProtoV2Header | golang/nats-server/server/client_proxyproto.go:301 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:208 | `ProxyProtocolParser.ParseV2AfterSig` |
| parseIPv4Addr | golang/nats-server/server/client_proxyproto.go:365 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:250 | `ParseIPv4` private method |
| parseIPv6Addr | golang/nats-server/server/client_proxyproto.go:383 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:269 | `ParseIPv6` private method |
| error variables | golang/nats-server/server/client_proxyproto.go:73 | PORTED | src/NATS.Server/Protocol/ProxyProtocol.cs:353 | `ProxyProtocolException` and `ProxyProtocolUnsupportedException` |
### certidp/certidp.go — Certificate identity provider
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| DefaultAllowedClockSkew | golang/nats-server/server/certidp/certidp.go:30 | MISSING | — | No OCSP clock skew constant |
| DefaultOCSPResponderTimeout | golang/nats-server/server/certidp/certidp.go:31 | MISSING | — | No OCSP responder timeout constant |
| DefaultTTLUnsetNextUpdate | golang/nats-server/server/certidp/certidp.go:32 | MISSING | — | No default TTL when NextUpdate is unset |
| StatusAssertion (type) | golang/nats-server/server/certidp/certidp.go:35 | PARTIAL | src/NATS.Server/Events/EventTypes.cs:595 | `OcspStatus` enum exists (Good, Revoked, Unknown) but no JSON marshal/unmarshal or bidirectional maps |
| GetStatusAssertionStr | golang/nats-server/server/certidp/certidp.go:56 | PORTED | src/NATS.Server/Events/EventTypes.cs:647 | `OcspEventBuilder.ParseStatus` provides string-to-enum; reverse mapping implicit |
| ChainLink (struct) | golang/nats-server/server/certidp/certidp.go:93 | MISSING | — | No chain link struct with Leaf/Issuer/OCSPWebEndpoints |
| OCSPPeerConfig (struct) | golang/nats-server/server/certidp/certidp.go:100 | MISSING | — | No OCSP peer config struct (Verify, Timeout, ClockSkew, WarnOnly, UnknownIsGood, AllowWhenCAUnreachable, TTLUnsetNextUpdate) |
| NewOCSPPeerConfig | golang/nats-server/server/certidp/certidp.go:110 | MISSING | — | No peer config factory |
| Log (struct) | golang/nats-server/server/certidp/certidp.go:123 | NOT_APPLICABLE | — | .NET uses ILogger<T> injection; no need for function-pointer log struct |
| CertInfo (struct) | golang/nats-server/server/certidp/certidp.go:131 | MISSING | — | No cert info DTO for events |
| GenerateFingerprint | golang/nats-server/server/certidp/certidp.go:179 | PARTIAL | src/NATS.Server/Tls/TlsHelper.cs:88 | `GetCertificateHash` uses SHA256 on SPKI (not raw cert as Go does); different hash input |
| getWebEndpoints | golang/nats-server/server/certidp/certidp.go:184 | MISSING | — | No OCSP endpoint URL extraction/filtering |
| GetSubjectDNForm | golang/nats-server/server/certidp/certidp.go:203 | MISSING | — | No subject RDN sequence formatting |
| GetIssuerDNForm | golang/nats-server/server/certidp/certidp.go:212 | MISSING | — | No issuer RDN sequence formatting |
| CertOCSPEligible | golang/nats-server/server/certidp/certidp.go:221 | MISSING | — | No OCSP eligibility check based on AIA extension |
| GetLeafIssuerCert | golang/nats-server/server/certidp/certidp.go:237 | MISSING | — | No positional issuer extraction from chain |
| OCSPResponseCurrent | golang/nats-server/server/certidp/certidp.go:250 | MISSING | — | No OCSP response currency check with clock skew and TTL fallback |
| ValidDelegationCheck | golang/nats-server/server/certidp/certidp.go:288 | MISSING | — | No OCSP response delegation validation per RFC 6960 section 4.2.2.2 |
### certidp/messages.go — CertIDP message types
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Error message constants | golang/nats-server/server/certidp/messages.go:17 | MISSING | — | No equivalent error/debug message constants; .NET uses structured logging |
| Debug message constants | golang/nats-server/server/certidp/messages.go:47 | MISSING | — | Debug format strings not ported; .NET logs differently |
| MsgTLSClientRejectConnection | golang/nats-server/server/certidp/messages.go:81 | PARTIAL | src/NATS.Server/Events/EventTypes.cs:520 | Reject event type exists but literal reject reason string not exposed |
| MsgTLSServerRejectConnection | golang/nats-server/server/certidp/messages.go:82 | PARTIAL | src/NATS.Server/Events/EventTypes.cs:520 | Same as above |
| MsgCacheOnline / MsgCacheOffline | golang/nats-server/server/certidp/messages.go:96 | MISSING | — | No cache status notification messages |
### certidp/ocsp_responder.go — OCSP responder client
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| FetchOCSPResponse | golang/nats-server/server/certidp/ocsp_responder.go:29 | MISSING | — | No HTTP-based OCSP response fetcher (GET with base64 encoding); .NET relies on X509Chain revocation |
| encodeOCSPRequest | golang/nats-server/server/certidp/ocsp_responder.go:89 | MISSING | — | No DER-to-base64-to-URL encoding for OCSP requests |
### certstore/certstore.go — OS certificate store access
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| StoreType (type) | golang/nats-server/server/certstore/certstore.go:24 | NOT_APPLICABLE | — | .NET has native X509Store; no need for Windows-specific store type enum |
| StoreMap / StoreOSMap | golang/nats-server/server/certstore/certstore.go:34 | NOT_APPLICABLE | — | .NET handles store locations via X509StoreLocation enum |
| MatchByType (type) | golang/nats-server/server/certstore/certstore.go:44 | NOT_APPLICABLE | — | .NET can use X509FindType enum for similar functionality |
| MatchByMap | golang/nats-server/server/certstore/certstore.go:52 | NOT_APPLICABLE | — | .NET equivalent: X509FindType |
| ParseCertStore | golang/nats-server/server/certstore/certstore.go:68 | NOT_APPLICABLE | — | .NET has built-in X509Store with StoreLocation |
| ParseCertMatchBy | golang/nats-server/server/certstore/certstore.go:80 | NOT_APPLICABLE | — | .NET has X509FindType |
| GetLeafIssuer | golang/nats-server/server/certstore/certstore.go:88 | MISSING | — | Could port using X509Chain verification to find issuer |
| credential (interface) | golang/nats-server/server/certstore/certstore.go:99 | NOT_APPLICABLE | — | .NET uses X509Certificate2 with private key; no separate credential interface needed |
### certstore/certstore_other.go — Non-Windows cert store stub
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| otherKey (struct) | golang/nats-server/server/certstore/certstore_other.go:27 | NOT_APPLICABLE | — | Go build-tag stub for non-Windows; .NET X509Store is cross-platform |
| TLSConfig (stub) | golang/nats-server/server/certstore/certstore_other.go:29 | NOT_APPLICABLE | — | Build-tag stub; not needed in .NET |
### certstore/certstore_windows.go — Windows cert store
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| Windows CNG/NCrypt constants | golang/nats-server/server/certstore/certstore_windows.go:42 | NOT_APPLICABLE | — | .NET X509Store abstracts away Windows CNG; no P/Invoke needed |
| winCertStore (struct) | golang/nats-server/server/certstore/certstore_windows.go:352 | NOT_APPLICABLE | — | .NET X509Store handles store access natively |
| TLSConfig (Windows) | golang/nats-server/server/certstore/certstore_windows.go:202 | NOT_APPLICABLE | — | .NET can use X509Store + X509Certificate2 directly; no syscall-level TLS config needed |
| winKey (struct) | golang/nats-server/server/certstore/certstore_windows.go:528 | NOT_APPLICABLE | — | .NET X509Certificate2.GetRSAPrivateKey/GetECDsaPrivateKey handles this |
| winSignECDSA / winSignRSA* | golang/nats-server/server/certstore/certstore_windows.go:562 | NOT_APPLICABLE | — | .NET runtime handles signing through X509Certificate2 private key |
| createCACertsPool | golang/nats-server/server/certstore/certstore_windows.go:173 | NOT_APPLICABLE | — | .NET uses X509Certificate2Collection / X509Chain for CA pool |
| certByIssuer / certBySubject / certByThumbprint | golang/nats-server/server/certstore/certstore_windows.go:390 | NOT_APPLICABLE | — | .NET uses X509Store.Certificates.Find() with X509FindType |
| unmarshalECC / winUnmarshalRSA | golang/nats-server/server/certstore/certstore_windows.go:844 | NOT_APPLICABLE | — | .NET BCL handles key deserialization |
### certstore/errors.go — Cert store errors
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| ErrBadCryptoStoreProvider | golang/nats-server/server/certstore/errors.go:9 | NOT_APPLICABLE | — | .NET X509Store throws its own exceptions |
| ErrBadRSAHashAlgorithm | golang/nats-server/server/certstore/errors.go:12 | NOT_APPLICABLE | — | .NET runtime handles hash algorithm validation |
| ErrBadSigningAlgorithm | golang/nats-server/server/certstore/errors.go:15 | NOT_APPLICABLE | — | .NET runtime handles signing |
| ErrStoreRSASigningError | golang/nats-server/server/certstore/errors.go:18 | NOT_APPLICABLE | — | .NET CryptographicException covers this |
| ErrStoreECDSASigningError | golang/nats-server/server/certstore/errors.go:21 | NOT_APPLICABLE | — | .NET CryptographicException covers this |
| ErrNoPrivateKeyStoreRef | golang/nats-server/server/certstore/errors.go:24 | NOT_APPLICABLE | — | .NET checks HasPrivateKey |
| ErrExtractingPrivateKeyMetadata | golang/nats-server/server/certstore/errors.go:27 | NOT_APPLICABLE | — | .NET handles internally |
| ErrExtractingECCPublicKey | golang/nats-server/server/certstore/errors.go:30 | NOT_APPLICABLE | — | .NET handles internally |
| ErrExtractingRSAPublicKey | golang/nats-server/server/certstore/errors.go:33 | NOT_APPLICABLE | — | .NET handles internally |
| ErrExtractingPublicKey | golang/nats-server/server/certstore/errors.go:36 | NOT_APPLICABLE | — | .NET handles internally |
| ErrBadPublicKeyAlgorithm | golang/nats-server/server/certstore/errors.go:39 | NOT_APPLICABLE | — | .NET handles internally |
| ErrExtractPropertyFromKey | golang/nats-server/server/certstore/errors.go:42 | NOT_APPLICABLE | — | .NET handles internally |
| ErrBadECCCurveName | golang/nats-server/server/certstore/errors.go:45 | NOT_APPLICABLE | — | .NET handles internally |
| ErrFailedCertSearch | golang/nats-server/server/certstore/errors.go:48 | NOT_APPLICABLE | — | .NET X509Store.Find returns empty collection |
| ErrFailedX509Extract | golang/nats-server/server/certstore/errors.go:51 | NOT_APPLICABLE | — | .NET handles internally |
| ErrBadMatchByType | golang/nats-server/server/certstore/errors.go:54 | NOT_APPLICABLE | — | .NET uses enum |
| ErrBadCertStore | golang/nats-server/server/certstore/errors.go:57 | NOT_APPLICABLE | — | .NET uses enum |
| ErrConflictCertFileAndStore | golang/nats-server/server/certstore/errors.go:60 | MISSING | — | Config validation for conflicting cert_file + cert_store not ported |
| ErrBadCertStoreField | golang/nats-server/server/certstore/errors.go:63 | NOT_APPLICABLE | — | Config validation |
| ErrBadCertMatchByField | golang/nats-server/server/certstore/errors.go:66 | NOT_APPLICABLE | — | Config validation |
| ErrBadCertMatchField | golang/nats-server/server/certstore/errors.go:69 | NOT_APPLICABLE | — | Config validation |
| ErrBadCaCertMatchField | golang/nats-server/server/certstore/errors.go:72 | NOT_APPLICABLE | — | Config validation |
| ErrBadCertMatchSkipInvalidField | golang/nats-server/server/certstore/errors.go:75 | NOT_APPLICABLE | — | Config validation |
| ErrOSNotCompatCertStore | golang/nats-server/server/certstore/errors.go:78 | NOT_APPLICABLE | — | .NET X509Store is cross-platform |
---
## 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/Tls/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
# TLS tests are in root test directory — filter manually
```
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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Completed full gap inventory: 12 Go source files analyzed, 144 symbols classified (20 PORTED, 9 PARTIAL, 70 MISSING, 45 NOT_APPLICABLE, 0 DEFERRED) | claude-opus |

448
gaps/utilities-and-other.md Normal file
View File

@@ -0,0 +1,448 @@
# Utilities & Other — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **Utilities & Other** 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 Utilities & Other
- This category is at **7% LOC parity** — significant gap, but many items may be NOT_APPLICABLE:
- `sendq.go` / `ipqueue.go` — Go channel-based patterns; .NET uses `Channel<T>` or different buffering
- `ring.go` — May be replaced by .NET collections
- `ats/` — Address translation for NAT environments
- `elastic/` — Elasticsearch metric export (may be deferred)
- `tpm/` — Trusted Platform Module integration (platform-specific, likely DEFERRED)
- `internal/fastrand` — Go's fast random; .NET has `Random.Shared`
- `internal/ldap` — LDAP DN parsing for LDAP auth
- `internal/antithesis` — Chaos testing assertions (NOT_APPLICABLE unless using Antithesis)
- Focus on: `util.go` (byte parsing helpers), `errors.go` (error definitions), `rate_counter.go`, `scheduler.go`.
---
## Go Reference Files (Source)
- `golang/nats-server/server/util.go` — General utilities (string helpers, byte parsing, etc.)
- `golang/nats-server/server/ring.go` — Ring buffer implementation
- `golang/nats-server/server/rate_counter.go` — Rate limiting / counting
- `golang/nats-server/server/sendq.go` — Send queue for outbound messages
- `golang/nats-server/server/ipqueue.go` — IP-based queue
- `golang/nats-server/server/errors.go` — Error type definitions
- `golang/nats-server/server/errors_gen.go` — Generated error codes
- `golang/nats-server/server/sdm.go` — Server dynamic management
- `golang/nats-server/server/scheduler.go` — Internal task scheduler
- `golang/nats-server/server/ats/ats.go` — Address translation service
- `golang/nats-server/server/elastic/elastic.go` — Elasticsearch integration
- `golang/nats-server/server/tpm/js_ek_tpm_windows.go` — TPM key management (Windows)
- `golang/nats-server/server/tpm/js_ek_tpm_other.go` — TPM key management (non-Windows)
- `golang/nats-server/internal/` — Internal utility packages (fastrand, ldap, ocsp, testhelper, antithesis)
## Go Reference Files (Tests)
- `golang/nats-server/server/util_test.go`
- `golang/nats-server/server/ring_test.go`
- `golang/nats-server/server/rate_counter_test.go`
- `golang/nats-server/server/ipqueue_test.go`
- `golang/nats-server/server/errors_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/IO/OutboundBufferPool.cs`
- `src/NATS.Server/IO/AdaptiveReadBuffer.cs`
- `src/NATS.Server/Server/AcceptLoopErrorHandler.cs`
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/IO/`
---
## Gap Inventory
### `golang/nats-server/server/util.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `refCountedUrlSet` (type alias) | golang/nats-server/server/util.go:34 | NOT_APPLICABLE | — | Internal clustering gossip helper; no equivalent needed in .NET port yet (clustering/gateway not fully ported) |
| `refCountedUrlSet.addUrl` | golang/nats-server/server/util.go:203 | NOT_APPLICABLE | — | Part of gossip URL ref-counting; used only in route/gateway code not yet ported |
| `refCountedUrlSet.removeUrl` | golang/nats-server/server/util.go:212 | NOT_APPLICABLE | — | Same as above |
| `refCountedUrlSet.getAsStringSlice` | golang/nats-server/server/util.go:226 | NOT_APPLICABLE | — | Same as above |
| `versionComponents` (unexported) | golang/nats-server/server/util.go:42 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:992 | Inlined as `VersionAtLeast` helper in test file; production equivalent used in server |
| `versionAtLeastCheckError` (unexported) | golang/nats-server/server/util.go:62 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:992 | Logic folded into `VersionAtLeast` test helper |
| `versionAtLeast` (unexported) | golang/nats-server/server/util.go:75 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:992 | Ported as `VersionAtLeast` in InfrastructureGoParityTests; production uses `System.Version` comparisons |
| `parseSize` (unexported) | golang/nats-server/server/util.go:82 | PORTED | src/NATS.Server/Protocol/NatsParser.cs:434 | `NatsParser.ParseSize(Span<byte>)` — exact behavioral match including -1 on error; tested in InfrastructureGoParityTests |
| `parseInt64` (unexported) | golang/nats-server/server/util.go:113 | PORTED | src/NATS.Server/Protocol/NatsParser.cs:434 | Folded into `ParseSize` / inline parser logic; behavior covered by parser tests |
| `secondsToDuration` (unexported) | golang/nats-server/server/util.go:127 | NOT_APPLICABLE | — | Go-specific `time.Duration` helper; .NET uses `TimeSpan.FromSeconds(double)` directly |
| `parseHostPort` (unexported) | golang/nats-server/server/util.go:134 | PARTIAL | src/NATS.Server/Configuration/ConfigProcessor.cs | ConfigProcessor parses `host:port` strings but does not have a standalone `ParseHostPort` helper matching Go's default-port fallback logic |
| `urlsAreEqual` (unexported) | golang/nats-server/server/util.go:158 | NOT_APPLICABLE | — | Uses `reflect.DeepEqual` on `*url.URL`; not needed in .NET port (no equivalent URL gossip pattern yet) |
| `comma` (unexported) | golang/nats-server/server/util.go:169 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:960 | Ported as `CommaFormat` helper in InfrastructureGoParityTests (test-side only); monitoring output uses `string.Format("{0:N0}")` in production |
| `natsListenConfig` | golang/nats-server/server/util.go:246 | PORTED | src/NATS.Server/NatsServer.cs | .NET `TcpListener` / `Socket` used without OS TCP keepalives; keepalive is disabled by default in .NET socket setup matching Go behavior |
| `natsListen` (unexported) | golang/nats-server/server/util.go:252 | PORTED | src/NATS.Server/NatsServer.cs | Equivalent accept loop uses `TcpListener.AcceptTcpClientAsync` without system keepalives |
| `natsDialTimeout` (unexported) | golang/nats-server/server/util.go:258 | PARTIAL | src/NATS.Server/Routes/RouteConnection.cs | Route dialing exists but the explicit keepalive=-1 (disabled) setting is not verified in .NET route code |
| `redactURLList` (unexported) | golang/nats-server/server/util.go:270 | PARTIAL | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:979 | `RedactUrl` helper ported in test file; no production-side `redactURLList` for URL slices |
| `redactURLString` (unexported) | golang/nats-server/server/util.go:296 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:979 | `RedactUrl` in InfrastructureGoParityTests matches behavior; also used in log redaction |
| `getURLsAsString` (unexported) | golang/nats-server/server/util.go:308 | NOT_APPLICABLE | — | Internal URL slice utility for clustering; not needed in current .NET scope |
| `copyBytes` (unexported) | golang/nats-server/server/util.go:317 | PORTED | (inline) | .NET uses `ReadOnlySpan<byte>.ToArray()`, `Array.Copy`, or `Buffer.BlockCopy` equivalently throughout the codebase |
| `copyStrings` (unexported) | golang/nats-server/server/util.go:328 | NOT_APPLICABLE | — | Go slice copying idiom; .NET uses LINQ `.ToArray()` or list copy directly |
| `generateInfoJSON` (unexported) | golang/nats-server/server/util.go:339 | PORTED | src/NATS.Server/Protocol/NatsProtocol.cs | `NatsProtocol.WriteInfo()` writes the `INFO …\r\n` frame directly to the write buffer |
| `parallelTaskQueue` (unexported) | golang/nats-server/server/util.go:350 | NOT_APPLICABLE | — | Go goroutine pool pattern; .NET uses `Parallel.ForEach`, `Task.WhenAll`, or `Channel<T>` for equivalent parallelism |
### `golang/nats-server/server/ring.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `closedClient` (type) | golang/nats-server/server/ring.go:17 | PORTED | src/NATS.Server/Monitoring/ClosedClient.cs | `ClosedClient` C# record mirrors the Go struct; includes connection info, subscription details |
| `closedRingBuffer` (type) | golang/nats-server/server/ring.go:25 | PORTED | src/NATS.Server/Monitoring/ClosedConnectionRingBuffer.cs | `ClosedConnectionRingBuffer` — full ring buffer with capacity, count, totalClosed, thread-safe via `Lock` |
| `newClosedRingBuffer` | golang/nats-server/server/ring.go:31 | PORTED | src/NATS.Server/Monitoring/ClosedConnectionRingBuffer.cs:16 | Constructor with capacity parameter |
| `closedRingBuffer.append` | golang/nats-server/server/ring.go:39 | PORTED | src/NATS.Server/Monitoring/ClosedConnectionRingBuffer.cs:37 | `Add(ClosedClient)` — overwrites oldest on overflow |
| `closedRingBuffer.next` (unexported) | golang/nats-server/server/ring.go:44 | PORTED | src/NATS.Server/Monitoring/ClosedConnectionRingBuffer.cs | Inlined into `Add` — `_head = (_head + 1) % _buffer.Length` |
| `closedRingBuffer.len` (unexported) | golang/nats-server/server/ring.go:48 | PORTED | src/NATS.Server/Monitoring/ClosedConnectionRingBuffer.cs:24 | `Count` property |
| `closedRingBuffer.totalConns` (unexported) | golang/nats-server/server/ring.go:55 | PORTED | src/NATS.Server/Monitoring/ClosedConnectionRingBuffer.cs:29 | `TotalClosed` property |
| `closedRingBuffer.closedClients` (unexported) | golang/nats-server/server/ring.go:65 | PORTED | src/NATS.Server/Monitoring/ClosedConnectionRingBuffer.cs:52 | `GetAll()` and `GetRecent(int)` — returns sorted copy; .NET returns newest-first (monitoring convention) |
### `golang/nats-server/server/rate_counter.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `rateCounter` (type) | golang/nats-server/server/rate_counter.go:21 | PORTED | src/NATS.Server/Tls/TlsRateLimiter.cs | `TlsRateLimiter` provides equivalent token-bucket rate limiting for TLS handshakes; also `ApiRateLimiter` for JetStream API |
| `newRateCounter` | golang/nats-server/server/rate_counter.go:30 | PORTED | src/NATS.Server/Tls/TlsRateLimiter.cs:9 | `TlsRateLimiter(long tokensPerSecond)` constructor |
| `rateCounter.allow` (unexported) | golang/nats-server/server/rate_counter.go:37 | PARTIAL | src/NATS.Server/Tls/TlsRateLimiter.cs:22 | `WaitAsync(CancellationToken)` is async/blocking rather than a synchronous allow-or-deny check; Go's `allow()` is non-blocking |
| `rateCounter.countBlocked` (unexported) | golang/nats-server/server/rate_counter.go:58 | MISSING | — | No equivalent "count blocked requests" metric; `TlsRateLimiter` does not expose a blocked-count stat |
### `golang/nats-server/server/sendq.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `outMsg` (type) | golang/nats-server/server/sendq.go:21 | NOT_APPLICABLE | — | Internal message envelope for Go goroutine-to-goroutine delivery; .NET internal system events use `Channel<T>` with typed event objects directly |
| `sendq` (type) | golang/nats-server/server/sendq.go:28 | NOT_APPLICABLE | — | Go-specific goroutine-based internal send queue for system account messages; .NET equivalent is `Channel<T>` or `InternalEventSystem` dispatch |
| `Server.newSendQ` | golang/nats-server/server/sendq.go:35 | NOT_APPLICABLE | — | Factory for goroutine-based queue; not applicable to .NET async model |
| `sendq.internalLoop` (unexported) | golang/nats-server/server/sendq.go:41 | NOT_APPLICABLE | — | Go goroutine event loop; .NET uses `await foreach` on `Channel<T>` reader |
| `outMsgPool` | golang/nats-server/server/sendq.go:103 | NOT_APPLICABLE | — | `sync.Pool` for `outMsg` structs; .NET uses `ArrayPool<byte>` or object pools via `System.Buffers` |
| `sendq.send` | golang/nats-server/server/sendq.go:109 | NOT_APPLICABLE | — | Public entry point for internal system message dispatch; .NET internal event dispatch uses `InternalEventSystem.PublishAsync` |
### `golang/nats-server/server/ipqueue.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `ipQueue[T]` (generic type) | golang/nats-server/server/ipqueue.go:25 | NOT_APPLICABLE | — | Go generics-based intra-process queue using goroutine channels; .NET uses `System.Threading.Channels.Channel<T>` which provides the same producer-consumer semantics natively |
| `ipQueueOpts[T]` | golang/nats-server/server/ipqueue.go:38 | NOT_APPLICABLE | — | Options struct for ipQueue; .NET `Channel.CreateBounded/CreateUnbounded` options cover equivalent functionality |
| `ipQueueOpt[T]` (function type) | golang/nats-server/server/ipqueue.go:45 | NOT_APPLICABLE | — | Functional options pattern; .NET uses `BoundedChannelOptions` |
| `ipqMaxRecycleSize[T]` | golang/nats-server/server/ipqueue.go:49 | NOT_APPLICABLE | — | Pool recycling threshold; not needed with .NET GC |
| `ipqSizeCalculation[T]` | golang/nats-server/server/ipqueue.go:57 | NOT_APPLICABLE | — | Size tracking callback; .NET channels track count natively |
| `ipqLimitBySize[T]` | golang/nats-server/server/ipqueue.go:68 | NOT_APPLICABLE | — | Capacity limiting by byte size; .NET `BoundedChannelOptions.Capacity` limits by count; byte-size limiting is MISSING but not critical for current port scope |
| `ipqLimitByLen[T]` | golang/nats-server/server/ipqueue.go:77 | NOT_APPLICABLE | — | Capacity limiting by count; maps to `BoundedChannelOptions.Capacity` |
| `errIPQLenLimitReached` | golang/nats-server/server/ipqueue.go:83 | NOT_APPLICABLE | — | Error sentinel for capacity; .NET channels return `false` from `TryWrite` when full |
| `errIPQSizeLimitReached` | golang/nats-server/server/ipqueue.go:84 | NOT_APPLICABLE | — | Same as above for size-based limit |
| `newIPQueue[T]` | golang/nats-server/server/ipqueue.go:86 | NOT_APPLICABLE | — | Factory; equivalent is `Channel.CreateBounded<T>` or `Channel.CreateUnbounded<T>` |
| `ipQueue[T].push` | golang/nats-server/server/ipqueue.go:113 | NOT_APPLICABLE | — | Maps to `ChannelWriter<T>.TryWrite` |
| `ipQueue[T].pop` | golang/nats-server/server/ipqueue.go:152 | NOT_APPLICABLE | — | Maps to `ChannelReader<T>.ReadAllAsync` |
| `ipQueue[T].popOne` | golang/nats-server/server/ipqueue.go:178 | NOT_APPLICABLE | — | Maps to `ChannelReader<T>.TryRead` |
| `ipQueue[T].recycle` | golang/nats-server/server/ipqueue.go:215 | NOT_APPLICABLE | — | Slice pool return; not needed with .NET GC |
| `ipQueue[T].len` | golang/nats-server/server/ipqueue.go:234 | NOT_APPLICABLE | — | Maps to `ChannelReader<T>.Count` |
| `ipQueue[T].size` | golang/nats-server/server/ipqueue.go:242 | NOT_APPLICABLE | — | Byte-size tracking; no direct .NET equivalent but not needed |
| `ipQueue[T].drain` | golang/nats-server/server/ipqueue.go:253 | NOT_APPLICABLE | — | Flush/clear; .NET channels are drained by completing the writer and reading remaining items |
| `ipQueue[T].inProgress` | golang/nats-server/server/ipqueue.go:275 | NOT_APPLICABLE | — | In-flight count tracking; not needed in .NET model |
| `ipQueue[T].unregister` | golang/nats-server/server/ipqueue.go:281 | NOT_APPLICABLE | — | Remove from server map; .NET uses `ConcurrentDictionary.TryRemove` |
### `golang/nats-server/server/errors.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `ErrConnectionClosed` | golang/nats-server/server/errors.go:23 | PORTED | src/NATS.Server/Protocol/NatsProtocol.cs | Protocol-level error strings defined as constants in `NatsProtocol`; connection close reason tracked via `ClientClosedReason` enum |
| `ErrAuthentication` | golang/nats-server/server/errors.go:26 | PORTED | src/NATS.Server/Protocol/NatsProtocol.cs:30 | `ErrAuthorizationViolation` constant |
| `ErrAuthTimeout` | golang/nats-server/server/errors.go:29 | PORTED | src/NATS.Server/NatsClient.cs | Auth timeout enforced; close reason `AuthTimeout` in `ClientClosedReason` |
| `ErrAuthExpired` | golang/nats-server/server/errors.go:32 | PORTED | src/NATS.Server/NatsClient.cs | Auth expiry enforced via connection deadline |
| `ErrAuthProxyNotTrusted` | golang/nats-server/server/errors.go:35 | PORTED | src/NATS.Server/Auth/ | Proxy trust handled in auth layer |
| `ErrAuthProxyRequired` | golang/nats-server/server/errors.go:39 | PORTED | src/NATS.Server/Auth/ | Proxy requirement enforced |
| `ErrMaxPayload` | golang/nats-server/server/errors.go:43 | PORTED | src/NATS.Server/Protocol/NatsProtocol.cs:27 | `ErrMaxPayloadViolation` constant |
| `ErrMaxControlLine` | golang/nats-server/server/errors.go:46 | PORTED | src/NATS.Server/Protocol/NatsParser.cs | Parser throws when control line exceeds `MaxControlLineSize` |
| `ErrReservedPublishSubject` | golang/nats-server/server/errors.go:49 | PORTED | src/NATS.Server/NatsClient.cs | Reserved subject check on publish |
| `ErrBadPublishSubject` | golang/nats-server/server/errors.go:52 | PORTED | src/NATS.Server/Protocol/NatsProtocol.cs:28 | `ErrInvalidPublishSubject` constant |
| `ErrBadSubject` | golang/nats-server/server/errors.go:55 | PORTED | src/NATS.Server/Protocol/NatsProtocol.cs:29 | `ErrInvalidSubject` constant |
| `ErrBadQualifier` | golang/nats-server/server/errors.go:58 | MISSING | — | No dedicated bad-qualifier error; transform validation throws `ArgumentException` |
| `ErrBadClientProtocol` | golang/nats-server/server/errors.go:61 | PORTED | src/NATS.Server/NatsClient.cs | Protocol version validation on CONNECT |
| `ErrTooManyConnections` | golang/nats-server/server/errors.go:64 | PORTED | src/NATS.Server/Protocol/NatsProtocol.cs:25 | `ErrMaxConnectionsExceeded` constant |
| `ErrTooManyAccountConnections` | golang/nats-server/server/errors.go:68 | MISSING | — | Account-level connection limits not yet implemented |
| `ErrLeafNodeLoop` | golang/nats-server/server/errors.go:72 | PORTED | src/NATS.Server/LeafNode/ | Leaf node loop detection implemented |
| `ErrTooManySubs` | golang/nats-server/server/errors.go:76 | MISSING | — | Per-connection subscription limit not yet enforced |
| `ErrTooManySubTokens` | golang/nats-server/server/errors.go:79 | MISSING | — | Subject token count limit not yet enforced |
| `ErrClientConnectedToRoutePort` | golang/nats-server/server/errors.go:83 | PORTED | src/NATS.Server/Routes/ | Wrong port detection on route listener |
| `ErrClientConnectedToLeafNodePort` | golang/nats-server/server/errors.go:87 | PORTED | src/NATS.Server/LeafNode/ | Wrong port detection on leaf node listener |
| `ErrLeafNodeHasSameClusterName` | golang/nats-server/server/errors.go:91 | PORTED | src/NATS.Server/LeafNode/ | Same-cluster-name rejection |
| `ErrLeafNodeDisabled` | golang/nats-server/server/errors.go:94 | PORTED | src/NATS.Server/LeafNode/ | Leaf node disabled check |
| `ErrConnectedToWrongPort` | golang/nats-server/server/errors.go:98 | PORTED | src/NATS.Server/NatsServer.cs | Port sniffing / wrong-port close |
| `ErrAccountExists` | golang/nats-server/server/errors.go:102 | PORTED | src/NATS.Server/Auth/Account.cs | Duplicate account registration check |
| `ErrBadAccount` | golang/nats-server/server/errors.go:105 | PORTED | src/NATS.Server/Auth/ | Bad/malformed account |
| `ErrReservedAccount` | golang/nats-server/server/errors.go:108 | MISSING | — | Reserved account name check not yet implemented |
| `ErrMissingAccount` | golang/nats-server/server/errors.go:111 | PORTED | src/NATS.Server/Auth/ | Missing account lookup |
| `ErrMissingService` | golang/nats-server/server/errors.go:114 | MISSING | — | Service export/import not yet ported |
| `ErrBadServiceType` | golang/nats-server/server/errors.go:117 | MISSING | — | Service latency tracking not yet ported |
| `ErrBadSampling` | golang/nats-server/server/errors.go:120 | MISSING | — | Latency sampling validation not yet ported |
| `ErrAccountValidation` | golang/nats-server/server/errors.go:123 | PORTED | src/NATS.Server/Auth/ | Account validation logic |
| `ErrAccountExpired` | golang/nats-server/server/errors.go:126 | PORTED | src/NATS.Server/Auth/ | Account expiry check |
| `ErrNoAccountResolver` | golang/nats-server/server/errors.go:129 | PORTED | src/NATS.Server/Auth/ | No resolver configured check |
| `ErrAccountResolverUpdateTooSoon` | golang/nats-server/server/errors.go:132 | MISSING | — | Resolver update rate limiting not yet ported |
| `ErrAccountResolverSameClaims` | golang/nats-server/server/errors.go:135 | MISSING | — | Same-claims dedup not yet ported |
| `ErrStreamImportAuthorization` | golang/nats-server/server/errors.go:138 | MISSING | — | Stream import auth not yet ported |
| `ErrStreamImportBadPrefix` | golang/nats-server/server/errors.go:141 | MISSING | — | Stream import prefix validation not yet ported |
| `ErrStreamImportDuplicate` | golang/nats-server/server/errors.go:144 | MISSING | — | Duplicate import detection not yet ported |
| `ErrServiceImportAuthorization` | golang/nats-server/server/errors.go:147 | MISSING | — | Service import auth not yet ported |
| `ErrImportFormsCycle` | golang/nats-server/server/errors.go:150 | MISSING | — | Import cycle detection not yet ported |
| `ErrCycleSearchDepth` | golang/nats-server/server/errors.go:153 | MISSING | — | Cycle search depth limit not yet ported |
| `ErrClientOrRouteConnectedToGatewayPort` | golang/nats-server/server/errors.go:157 | PORTED | src/NATS.Server/Gateways/ | Wrong port detection on gateway listener |
| `ErrWrongGateway` | golang/nats-server/server/errors.go:161 | PORTED | src/NATS.Server/Gateways/ | Wrong gateway name on connect |
| `ErrGatewayNameHasSpaces` | golang/nats-server/server/errors.go:165 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs | Config validation rejects spaces in gateway name |
| `ErrNoSysAccount` | golang/nats-server/server/errors.go:169 | PORTED | src/NATS.Server/NatsServer.cs | System account presence check |
| `ErrRevocation` | golang/nats-server/server/errors.go:172 | PORTED | src/NATS.Server/Auth/ | Credential revocation check |
| `ErrServerNotRunning` | golang/nats-server/server/errors.go:175 | PORTED | src/NATS.Server/NatsServer.cs | Server running state check |
| `ErrServerNameHasSpaces` | golang/nats-server/server/errors.go:178 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs:1434 | Config validation; `ConfigProcessorException` thrown |
| `ErrBadMsgHeader` | golang/nats-server/server/errors.go:181 | PORTED | src/NATS.Server/Protocol/NatsParser.cs | Header parsing validation |
| `ErrMsgHeadersNotSupported` | golang/nats-server/server/errors.go:185 | PORTED | src/NATS.Server/NatsClient.cs | Headers-not-supported check |
| `ErrNoRespondersRequiresHeaders` | golang/nats-server/server/errors.go:189 | PORTED | src/NATS.Server/NatsClient.cs | No-responders validation |
| `ErrClusterNameConfigConflict` | golang/nats-server/server/errors.go:193 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs | Cluster/gateway name conflict validation |
| `ErrClusterNameRemoteConflict` | golang/nats-server/server/errors.go:196 | PORTED | src/NATS.Server/Routes/ | Remote cluster name mismatch |
| `ErrClusterNameHasSpaces` | golang/nats-server/server/errors.go:199 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs | Config validation |
| `ErrMalformedSubject` | golang/nats-server/server/errors.go:202 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs | Subject validation rejects malformed subjects |
| `ErrSubscribePermissionViolation` | golang/nats-server/server/errors.go:205 | PORTED | src/NATS.Server/NatsClient.cs | Subscribe permission check |
| `ErrNoTransforms` | golang/nats-server/server/errors.go:208 | MISSING | — | Transform selection logic not yet fully ported |
| `ErrCertNotPinned` | golang/nats-server/server/errors.go:211 | PORTED | src/NATS.Server/Tls/TlsHelper.cs | Pinned cert validation |
| `ErrDuplicateServerName` | golang/nats-server/server/errors.go:215 | PORTED | src/NATS.Server/Routes/ | Duplicate server name on cluster connect |
| `ErrMinimumVersionRequired` | golang/nats-server/server/errors.go:218 | PORTED | src/NATS.Server/Routes/ | Minimum version enforcement on cluster |
| `ErrInvalidMappingDestination` | golang/nats-server/server/errors.go:221 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Transform destination validation |
| `ErrInvalidMappingDestinationSubject` | golang/nats-server/server/errors.go:223 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Invalid destination subject |
| `ErrMappingDestinationNotUsingAllWildcards` | golang/nats-server/server/errors.go:226 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Wildcard coverage check |
| `ErrUnknownMappingDestinationFunction` | golang/nats-server/server/errors.go:229 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Unknown function error |
| `ErrMappingDestinationIndexOutOfRange` | golang/nats-server/server/errors.go:232 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Index out of range |
| `ErrMappingDestinationNotEnoughArgs` | golang/nats-server/server/errors.go:235 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Not enough args error |
| `ErrMappingDestinationInvalidArg` | golang/nats-server/server/errors.go:238 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Invalid arg error |
| `ErrMappingDestinationTooManyArgs` | golang/nats-server/server/errors.go:241 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Too many args error |
| `ErrMappingDestinationNotSupportedForImport` | golang/nats-server/server/errors.go:244 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Import-only wildcard restriction |
| `mappingDestinationErr` (type) | golang/nats-server/server/errors.go:248 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Equivalent validation exceptions in transform code |
| `mappingDestinationErr.Error` | golang/nats-server/server/errors.go:253 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs | Exception message formatting |
| `mappingDestinationErr.Is` | golang/nats-server/server/errors.go:257 | NOT_APPLICABLE | — | Go `errors.Is` interface; .NET uses `is` and `catch (SpecificException)` instead |
| `configErr` (type) | golang/nats-server/server/errors.go:261 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs:1434 | `ConfigProcessorException` with error list |
| `configErr.Source` | golang/nats-server/server/errors.go:267 | PARTIAL | src/NATS.Server/Configuration/ConfigProcessor.cs | Source file/line tracking in errors is partial; exception message includes context but not file:line:col |
| `configErr.Error` | golang/nats-server/server/errors.go:272 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs | Exception `Message` property |
| `unknownConfigFieldErr` (type) | golang/nats-server/server/errors.go:280 | PARTIAL | src/NATS.Server/Configuration/ConfigProcessor.cs | Unknown fields trigger `ConfigProcessorException` but without the specific `unknownConfigFieldErr` type distinction |
| `configWarningErr` (type) | golang/nats-server/server/errors.go:292 | MISSING | — | No distinction between warnings and errors in .NET config processor; all surfaced as exceptions |
| `processConfigErr` (type) | golang/nats-server/server/errors.go:303 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs:1434 | `ConfigProcessorException` accumulates all errors |
| `processConfigErr.Error` | golang/nats-server/server/errors.go:310 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs | `Message` + `Errors` list |
| `processConfigErr.Warnings` | golang/nats-server/server/errors.go:322 | MISSING | — | No separate warnings list; warnings folded into errors or logged |
| `processConfigErr.Errors` | golang/nats-server/server/errors.go:327 | PORTED | src/NATS.Server/Configuration/ConfigProcessor.cs:1437 | `ConfigProcessorException.Errors` property |
| `errCtx` (type) | golang/nats-server/server/errors.go:332 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:1069 | `WrappedNatsException` (test-file-scoped) mirrors the error context wrapping; production code uses standard .NET exception chaining |
| `NewErrorCtx` | golang/nats-server/server/errors.go:338 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:1069 | Equivalent via `new WrappedNatsException(inner, ctx)` in tests; production uses `new Exception(msg, inner)` |
| `errCtx.Unwrap` | golang/nats-server/server/errors.go:343 | NOT_APPLICABLE | — | Go `errors.Unwrap` interface; .NET uses `Exception.InnerException` |
| `errCtx.Context` | golang/nats-server/server/errors.go:350 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:1069 | `WrappedNatsException.FullTrace()` extracts and returns context |
| `UnpackIfErrorCtx` | golang/nats-server/server/errors.go:358 | PORTED | tests/NATS.Server.Tests/InfrastructureGoParityTests.cs:1075 | `WrappedNatsException.FullTrace()` — recursive trace assembly matches Go's `UnpackIfErrorCtx` logic |
| `errorsUnwrap` (unexported) | golang/nats-server/server/errors.go:371 | NOT_APPLICABLE | — | Go 1.13 compatibility shim; .NET uses `Exception.InnerException` directly |
| `ErrorIs` | golang/nats-server/server/errors.go:382 | NOT_APPLICABLE | — | Go 1.13 compatibility shim for `errors.Is`; .NET uses `is`, `catch`, or `Exception.GetType()` |
### `golang/nats-server/server/errors_gen.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| (entire file — code generator) | golang/nats-server/server/errors_gen.go:1 | NOT_APPLICABLE | — | This is a `//go:build ignore` code generator that produces `jetstream_errors_generated.go`. The generated JetStream API error codes are ported directly as `JetStreamApiError` and integer error code constants in `src/NATS.Server/JetStream/Api/`. No need to port the generator itself. |
### `golang/nats-server/server/sdm.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `SDMMeta` (type) | golang/nats-server/server/sdm.go:22 | MISSING | — | Subject Delete Marker metadata tracker; used inside `stream.go` for SDM/KV purge tracking. MsgScheduling not yet ported so SDMMeta is also absent. |
| `SDMBySeq` (type) | golang/nats-server/server/sdm.go:27 | MISSING | — | Per-sequence SDM state (last flag + timestamp); needed by `SDMMeta` |
| `newSDMMeta` | golang/nats-server/server/sdm.go:33 | MISSING | — | Constructor for `SDMMeta` |
| `isSubjectDeleteMarker` (unexported) | golang/nats-server/server/sdm.go:42 | MISSING | — | Header inspection to detect SDM or KV purge markers; needed for FileStore tombstone handling |
| `SDMMeta.empty` | golang/nats-server/server/sdm.go:47 | MISSING | — | Clear all tracking state |
| `SDMMeta.trackPending` | golang/nats-server/server/sdm.go:56 | MISSING | — | Cache a pending SDM sequence |
| `SDMMeta.removeSeqAndSubject` | golang/nats-server/server/sdm.go:66 | MISSING | — | Remove a confirmed SDM from tracking |
### `golang/nats-server/server/scheduler.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `ErrMsgScheduleInvalidVersion` | golang/nats-server/server/scheduler.go:29 | MISSING | — | Error for unknown schedule snapshot version; needed when decoding binary schedule state |
| `MsgScheduling` (type) | golang/nats-server/server/scheduler.go:35 | MISSING | — | Core message scheduler type — manages per-subject schedules, timer, THW integration; tests in FileStoreTombstoneTests.cs note this is not yet ported |
| `MsgSchedule` (type) | golang/nats-server/server/scheduler.go:46 | MISSING | — | Per-subject schedule entry (seq + timestamp) |
| `newMsgScheduling` | golang/nats-server/server/scheduler.go:51 | MISSING | — | Constructor with run callback |
| `MsgScheduling.add` | golang/nats-server/server/scheduler.go:61 | MISSING | — | Add/replace a schedule entry |
| `MsgScheduling.init` (unexported) | golang/nats-server/server/scheduler.go:66 | MISSING | — | Internal init with THW integration |
| `MsgScheduling.update` | golang/nats-server/server/scheduler.go:81 | MISSING | — | Update timestamp of existing schedule |
| `MsgScheduling.markInflight` | golang/nats-server/server/scheduler.go:93 | MISSING | — | Mark subject as currently being delivered |
| `MsgScheduling.isInflight` | golang/nats-server/server/scheduler.go:99 | MISSING | — | Check if subject is in-flight |
| `MsgScheduling.remove` | golang/nats-server/server/scheduler.go:104 | MISSING | — | Remove by sequence number |
| `MsgScheduling.removeSubject` | golang/nats-server/server/scheduler.go:111 | MISSING | — | Remove by subject |
| `MsgScheduling.clearInflight` | golang/nats-server/server/scheduler.go:119 | MISSING | — | Reset all in-flight tracking |
| `MsgScheduling.resetTimer` (unexported) | golang/nats-server/server/scheduler.go:123 | MISSING | — | Recalculate next fire time from THW |
| `MsgScheduling.getScheduledMessages` (unexported) | golang/nats-server/server/scheduler.go:158 | MISSING | — | Evaluate expired schedules; complex — includes pattern parsing, header manipulation |
| `MsgScheduling.encode` | golang/nats-server/server/scheduler.go:249 | MISSING | — | Binary snapshot serialization |
| `MsgScheduling.decode` | golang/nats-server/server/scheduler.go:267 | MISSING | — | Binary snapshot deserialization |
| `parseMsgSchedule` (unexported) | golang/nats-server/server/scheduler.go:302 | MISSING | — | Parse `@at ...` / `@every ...` schedule patterns |
### `golang/nats-server/server/ats/ats.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `TickInterval` (const) | golang/nats-server/server/ats/ats.go:25 | MISSING | — | 100ms tick interval constant for access-time service |
| `Register` | golang/nats-server/server/ats/ats.go:42 | MISSING | — | Start the access-time goroutine (ref-counted). Used by FileStore on creation to get efficient nanosecond timestamps without calling `time.Now()` on every access. |
| `Unregister` | golang/nats-server/server/ats/ats.go:64 | MISSING | — | Stop the access-time goroutine when last user unregisters |
| `AccessTime` | golang/nats-server/server/ats/ats.go:76 | MISSING | — | Return cached Unix nanosecond timestamp (atomic load). .NET equivalent could use `Interlocked`-cached `DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()` but no such service exists yet |
### `golang/nats-server/server/elastic/elastic.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `Pointer[T]` (generic type) | golang/nats-server/server/elastic/elastic.go:26 | MISSING | — | Elastic (weak/strong) reference wrapper using Go 1.24 `weak.Pointer`. Used for FileStore block caching to allow GC to reclaim blocks while keeping a strong ref when needed. No .NET equivalent ported. |
| `Make[T]` | golang/nats-server/server/elastic/elastic.go:20 | MISSING | — | Factory for `Pointer[T]` from a strong pointer |
| `Pointer[T].Set` | golang/nats-server/server/elastic/elastic.go:31 | MISSING | — | Update the weak/strong pointer |
| `Pointer[T].Strengthen` | golang/nats-server/server/elastic/elastic.go:38 | MISSING | — | Promote weak ref to strong |
| `Pointer[T].Weaken` | golang/nats-server/server/elastic/elastic.go:45 | MISSING | — | Demote strong ref to weak (allow GC) |
| `Pointer[T].Value` | golang/nats-server/server/elastic/elastic.go:52 | MISSING | — | Get current value (strong if available, else weak) |
### `golang/nats-server/server/tpm/js_ek_tpm_windows.go` and `js_ek_tpm_other.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `JsKeyTPMVersion` | golang/nats-server/server/tpm/js_ek_tpm_windows.go:34 | DEFERRED | — | TPM version constant; entire TPM subsystem deferred |
| `regenerateSRK` (unexported) | golang/nats-server/server/tpm/js_ek_tpm_windows.go:51 | DEFERRED | — | Windows-only TPM2 SRK creation |
| `natsTPMPersistedKeys` (type) | golang/nats-server/server/tpm/js_ek_tpm_windows.go:77 | DEFERRED | — | JSON-serializable key blob struct |
| `writeTPMKeysToFile` (unexported) | golang/nats-server/server/tpm/js_ek_tpm_windows.go:85 | DEFERRED | — | Persist TPM key blobs to disk |
| `readTPMKeysFromFile` (unexported) | golang/nats-server/server/tpm/js_ek_tpm_windows.go:115 | DEFERRED | — | Read TPM key blobs from disk |
| `createAndSealJsEncryptionKey` (unexported) | golang/nats-server/server/tpm/js_ek_tpm_windows.go:146 | DEFERRED | — | Create NKey-based JetStream encryption key and seal to TPM |
| `unsealJsEncrpytionKey` (unexported) | golang/nats-server/server/tpm/js_ek_tpm_windows.go:178 | DEFERRED | — | Unseal JetStream key from TPM |
| `policyPCRPasswordSession` (unexported) | golang/nats-server/server/tpm/js_ek_tpm_windows.go:203 | DEFERRED | — | TPM2 policy session with PCR + password |
| `LoadJetStreamEncryptionKeyFromTPM` | golang/nats-server/server/tpm/js_ek_tpm_windows.go:245 | DEFERRED | — | Main entry point; Windows only. Non-Windows stub returns error. Deferred: requires `Microsoft.TPM` or `Tss.Net` NuGet and Windows-only API surface. |
| `LoadJetStreamEncryptionKeyFromTPM` (stub) | golang/nats-server/server/tpm/js_ek_tpm_other.go:21 | NOT_APPLICABLE | — | Non-Windows stub; .NET would use `PlatformNotSupportedException` equivalently |
### `golang/nats-server/internal/fastrand/fastrand.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `Uint32` | golang/nats-server/internal/fastrand/fastrand.go:12 | NOT_APPLICABLE | — | Links to `runtime.fastrand` via `go:linkname` — a Go runtime internal. .NET uses `Random.Shared.Next()` or `RandomNumberGenerator` equivalently. No need to port this internal. |
| `Uint32n` | golang/nats-server/internal/fastrand/fastrand.go:17 | NOT_APPLICABLE | — | Same as above; maps to `Random.Shared.Next(0, n)` |
| `Uint64` | golang/nats-server/internal/fastrand/fastrand.go:20 | NOT_APPLICABLE | — | Same as above; maps to `Random.Shared.NextInt64()` |
### `golang/nats-server/internal/ldap/dn.go`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `AttributeTypeAndValue` (type) | golang/nats-server/internal/ldap/dn.go:30 | PARTIAL | src/NATS.Server/Auth/TlsMapAuthenticator.cs | .NET `X500DistinguishedName` used directly; no standalone `AttributeTypeAndValue` type |
| `RelativeDN` (type) | golang/nats-server/internal/ldap/dn.go:37 | PARTIAL | src/NATS.Server/Auth/TlsMapAuthenticator.cs | `X500DistinguishedName.EnumerateRelativeDistinguishedNames()` provides equivalent; no custom `RelativeDN` type |
| `DN` (type) | golang/nats-server/internal/ldap/dn.go:43 | PARTIAL | src/NATS.Server/Auth/TlsMapAuthenticator.cs | `X500DistinguishedName` (.NET BCL) used instead; no custom `DN` type |
| `FromCertSubject` | golang/nats-server/internal/ldap/dn.go:48 | PARTIAL | src/NATS.Server/Auth/TlsMapAuthenticator.cs:31 | Certificate subject extracted via `X509Certificate2.SubjectName`; does not support multi-value RDNs or full RFC 4514 ordering |
| `FromRawCertSubject` | golang/nats-server/internal/ldap/dn.go:80 | MISSING | — | Raw ASN.1 subject parsing preserving original order; not implemented |
| `ParseDN` | golang/nats-server/internal/ldap/dn.go:126 | MISSING | — | RFC 4514 DN string parser; not implemented. .NET uses `X500DistinguishedName` which parses RFC 2253 format but lacks the Go implementation's special-case handling |
| `DN.Equal` | golang/nats-server/internal/ldap/dn.go:220 | MISSING | — | RFC 4517 distinguishedNameMatch comparison |
| `DN.RDNsMatch` | golang/nats-server/internal/ldap/dn.go:234 | MISSING | — | Order-independent RDN matching |
| `DN.AncestorOf` | golang/nats-server/internal/ldap/dn.go:258 | MISSING | — | DN ancestry check |
| `RelativeDN.Equal` | golang/nats-server/internal/ldap/dn.go:278 | MISSING | — | RFC 4517 RDN equivalence |
| `RelativeDN.hasAllAttributes` (unexported) | golang/nats-server/internal/ldap/dn.go:285 | MISSING | — | Attribute superset check |
| `AttributeTypeAndValue.Equal` | golang/nats-server/internal/ldap/dn.go:302 | MISSING | — | Case-insensitive type + value comparison |
### `golang/nats-server/internal/antithesis/`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `AssertUnreachable` (noop build) | golang/nats-server/internal/antithesis/noop.go:24 | NOT_APPLICABLE | — | Chaos testing assertion (noop when Antithesis SDK not active); .NET has no Antithesis SDK. xUnit's `Assert.Fail()` covers the reachability assertion pattern in tests. |
| `Assert` (noop build) | golang/nats-server/internal/antithesis/noop.go:27 | NOT_APPLICABLE | — | Same as above; .NET uses Shouldly `condition.ShouldBeTrue()` in tests |
| `AssertUnreachable` (SDK build) | golang/nats-server/internal/antithesis/test_assert.go:49 | NOT_APPLICABLE | — | Antithesis SDK wrapper; no .NET Antithesis SDK exists |
| `Assert` (SDK build) | golang/nats-server/internal/antithesis/test_assert.go:83 | NOT_APPLICABLE | — | Same as above |
### `golang/nats-server/internal/testhelper/` (ocsp.go / logging.go)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `NewOCSPResponder` | golang/nats-server/internal/testhelper/ocsp.go (ocsp.go):45 | PORTED | tests/NATS.Server.Tests/OcspStaplingTests.cs | .NET OCSP test helper implemented inline in test files using ASP.NET Core minimal APIs |
| `NewOCSPResponderCustomAddress` | golang/nats-server/internal/testhelper/ocsp.go:40 | PORTED | tests/NATS.Server.Tests/OcspStaplingTests.cs | Equivalent test helper |
| `NewOCSPResponderDesignatedCustomAddress` | golang/nats-server/internal/testhelper/ocsp.go:50 | PORTED | tests/NATS.Server.Tests/OcspStaplingTests.cs | Designated responder variant |
| `NewOCSPResponderPreferringHTTPMethod` | golang/nats-server/internal/testhelper/ocsp.go:55 | PORTED | tests/NATS.Server.Tests/OcspStaplingTests.cs | HTTP method preference variant |
| `NewOCSPResponderCustomTimeout` | golang/nats-server/internal/testhelper/ocsp.go:60 | PORTED | tests/NATS.Server.Tests/OcspStaplingTests.cs | Custom TTL variant |
| `NewOCSPResponderBase` | golang/nats-server/internal/testhelper/ocsp.go:65 | PORTED | tests/NATS.Server.Tests/OcspStaplingTests.cs | Base factory; all OCSP responder variants collapse to this |
| `GetOCSPStatus` | golang/nats-server/internal/testhelper/ocsp.go:230 | PORTED | tests/NATS.Server.Tests/OcspStaplingTests.cs | OCSP status extraction from TLS connection state |
| `SetOCSPStatus` | golang/nats-server/internal/testhelper/ocsp.go:251 | PORTED | tests/NATS.Server.Tests/OcspStaplingTests.cs | HTTP POST to set cert status on mock OCSP responder |
---
## .NET-Only Implementations (No Direct Go Equivalent)
The following .NET types were added to support the porting but have no exact Go counterpart:
| .NET Type | File | Notes |
|-----------|------|-------|
| `OutboundBufferPool` | `src/NATS.Server/IO/OutboundBufferPool.cs` | Tiered byte-array pool (512/4096/65536) replacing Go's `sync.Pool` + `[]byte` dynamic sizing |
| `AdaptiveReadBuffer` | `src/NATS.Server/IO/AdaptiveReadBuffer.cs` | Buffer sizing state machine; mirrors `client.go` readLoop buffer growth/shrink logic |
| `AcceptLoopErrorHandler` | `src/NATS.Server/Server/AcceptLoopErrorHandler.cs` | Callback-based accept error delegation (no Go equivalent; Go logs inline) |
| `TlsRateLimiter` | `src/NATS.Server/Tls/TlsRateLimiter.cs` | Async token-bucket rate limiter for TLS handshakes |
---
## 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/IO/ src/NATS.Server/Server/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/IO/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated — all Go source files analyzed, 131 symbols classified | auto |

188
gaps/websocket.md Normal file
View File

@@ -0,0 +1,188 @@
# WebSocket — Gap Analysis
> This file tracks what has and hasn't been ported from Go to .NET for the **WebSocket** 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 WebSocket
- WebSocket is at **99% LOC parity** — likely near-complete.
- Focus analysis on edge cases: compression negotiation, frame fragmentation, close handshake.
- WebSocket connections wrap a standard NATS connection with WS framing.
---
## Go Reference Files (Source)
- `golang/nats-server/server/websocket.go` — WebSocket transport support (~1,550 lines). WS framing, upgrade handshake, compression, masking.
## Go Reference Files (Tests)
- `golang/nats-server/server/websocket_test.go`
## .NET Implementation Files (Source)
- `src/NATS.Server/WebSocket/` (all files)
## .NET Implementation Files (Tests)
- `tests/NATS.Server.Tests/WebSocket/`
---
## Gap Inventory
<!-- After analysis, fill in this table. Group rows by Go source file. -->
### `golang/nats-server/server/websocket.go`
#### Types and Structs
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `wsOpCode` (type) | websocket.go:41 | PORTED | `src/NATS.Server/WebSocket/WsConstants.cs:1` | Int constants replace Go type alias; `WsConstants.TextMessage`, `BinaryMessage`, etc. |
| `websocket` (struct) | websocket.go:108 | PORTED | `src/NATS.Server/WebSocket/WsConnection.cs:8` | Fields mapped: `compress`, `maskread`/`maskwrite`, `browser`, `nocompfrag`. Cookie fields moved to `WsUpgradeResult`. `frames`/`fs` buffer management is now in `WsConnection.WriteAsync`. `compressor` (reuse) is NOT pooled — recreated per call. |
| `srvWebsocket` (struct) | websocket.go:126 | PARTIAL | `src/NATS.Server/NatsServer.cs:538` | `_wsListener`, `_options.WebSocket` cover port/host/tls; `allowedOrigins` managed via `WsOriginChecker`. `connectURLsMap` ref-count URL set and `authOverride` flag are MISSING — not ported. |
| `allowedOrigin` (struct) | websocket.go:145 | PORTED | `src/NATS.Server/WebSocket/WsOriginChecker.cs:80` | Private `AllowedOrigin` record struct with `Scheme` and `Port`. |
| `wsUpgradeResult` (struct) | websocket.go:150 | PORTED | `src/NATS.Server/WebSocket/WsUpgrade.cs:346` | `WsUpgradeResult` readonly record struct with equivalent fields. `kind` maps to `WsClientKind` enum. |
| `wsReadInfo` (struct) | websocket.go:156 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:10` | All fields ported: `rem`→`Remaining`, `fs`→`FrameStart`, `ff`→`FirstFrame`, `fc`→`FrameCompressed`, `mask`→`ExpectMask`, `mkpos`→`MaskKeyPos`, `mkey`→`MaskKey`, `cbufs`→`CompressedBuffers`, `coff`→`CompressedOffset`. Extra .NET fields added for control frame output. |
| `WsDeflateParams` (struct) | websocket.go:885916 | PORTED | `src/NATS.Server/WebSocket/WsCompression.cs:10` | New .NET-specific struct capturing `permessage-deflate` negotiated parameters. No Go equivalent struct — Go stores compress bool only. |
#### Package-Level Variables / Constants
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `wsOpCode` constants | websocket.go:4396 | PORTED | `src/NATS.Server/WebSocket/WsConstants.cs:965` | All opcode, bit, size, close-status, and header-string constants are present. |
| `decompressorPool` | websocket.go:99 | PARTIAL | `src/NATS.Server/WebSocket/WsCompression.cs:193` | Go uses `sync.Pool` for `flate.Reader` reuse. .NET creates a new `DeflateStream` per decompression call — no pooling. Functional but slightly less efficient under high load. |
| `compressLastBlock` | websocket.go:100 | PORTED | `src/NATS.Server/WebSocket/WsConstants.cs:62` | .NET uses 4-byte `DecompressTrailer` (sync marker only); Go uses 9-byte block. Both work correctly — difference is .NET `DeflateStream` does not need the final stored block. |
| `wsGUID` | websocket.go:103 | PORTED | `src/NATS.Server/WebSocket/WsUpgrade.cs:177` | Inline string literal in `ComputeAcceptKey`. |
| `wsTestRejectNoMasking` | websocket.go:106 | MISSING | — | Test-only hook to force masking rejection. No equivalent test hook in .NET. |
#### Methods on `wsReadInfo`
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `wsReadInfo.init()` | websocket.go:168 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:28` | Constructor initializes `FrameStart=true`, `FirstFrame=true` (same as Go `init()`). |
| `wsReadInfo.Read()` | websocket.go:353 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:100` | Consumed data from `CompressedBuffers` is handled inside `ReadFrames` rather than via an `io.Reader` interface; functionally equivalent. |
| `wsReadInfo.nextCBuf()` | websocket.go:376 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:100` | Buffer cycling logic is inline within the `Decompress` method — no explicit `nextCBuf` helper needed since .NET uses list indexing. |
| `wsReadInfo.ReadByte()` | websocket.go:393 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:100` | Used by Go's `flate.Resetter`; .NET `DeflateStream` reads a `MemoryStream` directly — no `ReadByte` interface needed. |
| `wsReadInfo.decompress()` | websocket.go:408 | PORTED | `src/NATS.Server/WebSocket/WsCompression.cs:193` | `WsCompression.Decompress()` — appends trailer, decompresses with `DeflateStream`, enforces `maxPayload` limit. |
| `wsReadInfo.unmask()` | websocket.go:509 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:56` | `WsReadInfo.Unmask()` — exact port including 8-byte bulk XOR optimization for buffers >= 16 bytes. |
#### Methods on `client` (WebSocket-related)
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `client.isWebsocket()` | websocket.go:197 | PORTED | `src/NATS.Server/NatsClient.cs:107` | `NatsClient.IsWebSocket` bool property. |
| `client.wsRead()` | websocket.go:208 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:100` | `WsReadInfo.ReadFrames()` — full state machine port with frame-type validation, extended-length decoding, mask read, control frame dispatch, and compression accumulation. |
| `client.wsHandleControlFrame()` | websocket.go:445 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:246` | `WsReadInfo.HandleControlFrame()` — ping→pong, close→echo-close, pong→no-op. Close UTF-8 body validation is MISSING (Go validates UTF-8 in close body and downgrades status to 1007 on failure; .NET does not). |
| `client.wsEnqueueControlMessage()` | websocket.go:600 | PORTED | `src/NATS.Server/WebSocket/WsConnection.cs:127` | Control frames collected in `PendingControlFrames` and flushed via `FlushControlFramesAsync`. Go uses per-client write buffer queuing with `flushSignal`; .NET writes directly — functionally equivalent. |
| `client.wsEnqueueControlMessageLocked()` | websocket.go:631 | PORTED | `src/NATS.Server/WebSocket/WsConnection.cs:127` | Combined with `FlushControlFramesAsync`. Lock semantics differ (Go client mu, .NET `_writeLock`). |
| `client.wsEnqueueCloseMessage()` | websocket.go:668 | PORTED | `src/NATS.Server/WebSocket/WsFrameWriter.cs:152` | `WsFrameWriter.MapCloseStatus()` covers the same `ClosedState` → status mapping. `WsConnection.SendCloseAsync()` performs the send. `BadClientProtocolVersion`, `MaxAccountConnectionsExceeded`, `MaxConnectionsExceeded`, `MaxControlLineExceeded`, `MissingAccount`, `Revocation` close-reason mappings are MISSING from .NET `ClientClosedReason` enum. |
| `client.wsHandleProtocolError()` | websocket.go:700 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:135` | Protocol errors throw `InvalidOperationException` in `ReadFrames` — caller (WsConnection.ReadAsync) treats any exception as a connection close. Direct close-frame send on protocol error is implicit via the close path. |
| `client.wsCollapsePtoNB()` | websocket.go:1367 | PARTIAL | `src/NATS.Server/WebSocket/WsConnection.cs:91` | `WsConnection.WriteFramedAsync()` handles browser fragmentation and compression. However, Go's `wsCollapsePtoNB` is a low-level buffer-collapse routine that operates on `net.Buffers` (scatter/gather I/O) and reuses a persistent per-connection `flate.Writer` compressor. .NET recreates a `DeflateStream` per write and does not use scatter/gather — less efficient but functionally correct. |
#### Standalone Functions
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|-----------|:-------------|--------|:----------------|-------|
| `wsGet()` | websocket.go:178 | PORTED | `src/NATS.Server/WebSocket/WsReadInfo.cs:299` | `WsReadInfo.WsGet()` (private) — returns `(byte[], newPos)` tuple. Same buffer-then-reader fallback logic. |
| `wsIsControlFrame()` | websocket.go:539 | PORTED | `src/NATS.Server/WebSocket/WsConstants.cs:64` | `WsConstants.IsControlFrame(int opcode)` — same `>= CloseMessage` check. |
| `wsCreateFrameHeader()` | websocket.go:545 | PORTED | `src/NATS.Server/WebSocket/WsFrameWriter.cs:17` | `WsFrameWriter.CreateFrameHeader()` — first=true, final=true wrapper. |
| `wsFillFrameHeader()` | websocket.go:551 | PORTED | `src/NATS.Server/WebSocket/WsFrameWriter.cs:30` | `WsFrameWriter.FillFrameHeader()` — exact port with 2/4/10-byte length encoding and optional masking. |
| `wsMaskBuf()` | websocket.go:607 | PORTED | `src/NATS.Server/WebSocket/WsFrameWriter.cs:77` | `WsFrameWriter.MaskBuf()` — simple XOR loop. |
| `wsMaskBufs()` | websocket.go:614 | PORTED | `src/NATS.Server/WebSocket/WsFrameWriter.cs:86` | `WsFrameWriter.MaskBufs()` — multi-buffer contiguous XOR. |
| `wsCreateCloseMessage()` | websocket.go:711 | PORTED | `src/NATS.Server/WebSocket/WsFrameWriter.cs:103` | `WsFrameWriter.CreateCloseMessage()` — 2-byte status + body, body truncated with "..." at `MaxControlPayloadSize-2`. .NET additionally validates UTF-8 boundary when truncating. |
| `wsUpgrade()` (Server method) | websocket.go:731 | PORTED | `src/NATS.Server/WebSocket/WsUpgrade.cs:13` | `WsUpgrade.TryUpgradeAsync()` — full RFC 6455 handshake: method, host, upgrade, connection, key, version checks; origin; compression negotiation; no-masking; browser detection; cookie extraction; X-Forwarded-For; custom headers; 101 response. Go uses `http.Hijacker` pattern; .NET reads raw TCP stream. MQTT `Sec-Websocket-Protocol` header in response is MISSING. |
| `wsHeaderContains()` | websocket.go:872 | PORTED | `src/NATS.Server/WebSocket/WsUpgrade.cs:315` | `WsUpgrade.HeaderContains()` (private) — same comma-split, case-insensitive token matching. |
| `wsPMCExtensionSupport()` | websocket.go:885 | PORTED | `src/NATS.Server/WebSocket/WsCompression.cs:60` | `WsDeflateNegotiator.Negotiate()` — parses `Sec-WebSocket-Extensions`, detects `permessage-deflate`, extracts `server_no_context_takeover`, `client_no_context_takeover`, and window bits. |
| `wsReturnHTTPError()` | websocket.go:921 | PORTED | `src/NATS.Server/WebSocket/WsUpgrade.cs:226` | `WsUpgrade.FailAsync()` (private) — sends HTTP error response and returns `WsUpgradeResult.Failed`. |
| `srvWebsocket.checkOrigin()` | websocket.go:933 | PORTED | `src/NATS.Server/WebSocket/WsOriginChecker.cs:32` | `WsOriginChecker.CheckOrigin()` — same-origin and allowed-list checks. Go checks `r.TLS != nil` for TLS detection; .NET uses `isTls` parameter passed at call site. |
| `wsGetHostAndPort()` | websocket.go:985 | PORTED | `src/NATS.Server/WebSocket/WsOriginChecker.cs:65` | `WsOriginChecker.GetHostAndPort()` and `ParseHostPort()` — missing-port defaults to 80/443 by TLS flag. |
| `wsAcceptKey()` | websocket.go:1004 | PORTED | `src/NATS.Server/WebSocket/WsUpgrade.cs:175` | `WsUpgrade.ComputeAcceptKey()` — SHA-1 of key + GUID, base64 encoded. |
| `wsMakeChallengeKey()` | websocket.go:1011 | MISSING | — | Generates a random 16-byte base64 client key for outbound WS connections (leaf node acting as WS client). No .NET equivalent. Needed when .NET server connects outbound as a WS leaf. |
| `validateWebsocketOptions()` | websocket.go:1020 | PARTIAL | `src/NATS.Server/WebSocket/WebSocketTlsConfig.cs:24` | `WebSocketTlsConfig.Validate()` checks cert+key pair consistency. Full Go validation (TLS required unless NoTLS, AllowedOrigins parseable, NoAuthUser in users list, Token/Username incompatible with users/nkeys, JWTCookie requires TrustedOperators, TLSPinnedCerts, reserved header names) is MISSING from .NET. |
| `Server.wsSetOriginOptions()` | websocket.go:1083 | PARTIAL | `src/NATS.Server/WebSocket/WsUpgrade.cs:49` | Origin checking is constructed inline in `TryUpgradeAsync` from `options.SameOrigin` and `options.AllowedOrigins`. The Go method persists parsed origins in `srvWebsocket.allowedOrigins` map and supports hot-reload. .NET constructs a `WsOriginChecker` per request — no hot-reload support, but functionally equivalent for initial config. |
| `Server.wsSetHeadersOptions()` | websocket.go:1111 | PORTED | `src/NATS.Server/WebSocket/WsUpgrade.cs:141` | Custom headers applied inline in `TryUpgradeAsync` from `options.Headers`. |
| `Server.wsConfigAuth()` | websocket.go:1131 | MISSING | — | Sets `srvWebsocket.authOverride` flag based on username/token/noAuthUser presence. No equivalent flag computation in .NET — `WebSocketOptions` properties are read directly. Functionally equivalent but the explicit override flag is absent. |
| `Server.startWebsocketServer()` | websocket.go:1137 | PORTED | `src/NATS.Server/NatsServer.cs:538` | `NatsServer.StartAsync()` section at line 538 sets up the WS listener, logs, and launches `RunWebSocketAcceptLoopAsync`. Go uses `http.Server` + mux; .NET uses raw `TcpListener`/`Socket.AcceptAsync`. LEAF and MQTT routing at connection time is PARTIAL — LEAF path is wired (`WsClientKind.Leaf`) but MQTT is not handled in `AcceptWebSocketClientAsync`. `lame-duck` / `ldmCh` signaling is MISSING. |
| `Server.wsGetTLSConfig()` | websocket.go:1264 | PARTIAL | `src/NATS.Server/NatsServer.cs:807` | TLS is applied once at accept time via `TlsConnectionWrapper.NegotiateAsync`. Go uses `GetConfigForClient` callback for hot-reload TLS config. .NET does not support hot TLS config reload for WS. |
| `Server.createWSClient()` | websocket.go:1273 | PORTED | `src/NATS.Server/NatsServer.cs:799` | `AcceptWebSocketClientAsync()` — creates `WsConnection`, constructs `NatsClient`, wires `IsWebSocket`/`WsInfo`, registers client. Go also sends INFO immediately and sets auth timer; .NET's `NatsClient.RunAsync()` handles INFO send and auth timer. |
| `isWSURL()` | websocket.go:1544 | MISSING | — | Helper to detect `ws://` scheme in a URL. Used by leaf node / route URL parsing. No .NET equivalent — not yet needed since outbound WS connections are not implemented. |
| `isWSSURL()` | websocket.go:1548 | MISSING | — | Helper to detect `wss://` scheme in a URL. Same as above — not yet needed. |
---
## 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/WebSocket/ -name '*.cs' -type f -exec cat {} + | wc -l
# Re-count .NET test LOC for this module
find tests/NATS.Server.Tests/WebSocket/ -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-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated: 37 Go symbols classified (26 PORTED, 7 PARTIAL, 4 MISSING, 0 NOT_APPLICABLE, 0 DEFERRED) | auto |