diff --git a/gaps/auth-and-accounts.md b/gaps/auth-and-accounts.md new file mode 100644 index 0000000..62546b7 --- /dev/null +++ b/gaps/auth-and-accounts.md @@ -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 + + + +### 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 | diff --git a/gaps/configuration.md b/gaps/configuration.md new file mode 100644 index 0000000..ffe8503 --- /dev/null +++ b/gaps/configuration.md @@ -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 + + + +### 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` — 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` | +| `(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:"` 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=` 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:42–74` | 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 | diff --git a/gaps/core-server.md b/gaps/core-server.md new file mode 100644 index 0000000..9217860 --- /dev/null +++ b/gaps/core-server.md @@ -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 + + + +| 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 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 | +| 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 | diff --git a/gaps/events.md b/gaps/events.md new file mode 100644 index 0000000..24c8652 --- /dev/null +++ b/gaps/events.md @@ -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..CONNECT`, `$SYS.ACCOUNT..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 + + + +### 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 | diff --git a/gaps/gateways.md b/gaps/gateways.md new file mode 100644 index 0000000..8b2f81c --- /dev/null +++ b/gaps/gateways.md @@ -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_...` 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 + + + +### `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:94–111 | 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:49–58 | 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:43–46 | 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 | diff --git a/gaps/instructions.md b/gaps/instructions.md new file mode 100644 index 0000000..675b697 --- /dev/null +++ b/gaps/instructions.md @@ -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 `.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: +description: "Analyze 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: + +``` + +| JetStream.enableJetStream | jetstream.go:245 | PORTED | ... | ... | +... + +| 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/` diff --git a/gaps/internal-ds.md b/gaps/internal-ds.md new file mode 100644 index 0000000..8c18ef1 --- /dev/null +++ b/gaps/internal-ds.md @@ -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 + + + +### 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()` 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` | +| `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` class | +| `newLeaf[T]` | `golang/nats-server/server/stree/leaf.go:30` | PORTED | `src/NATS.Server/Internal/SubjectTree/Nodes.cs:63` | `new Leaf(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` | +| `node[T]` (struct) | `golang/nats-server/server/gsl/gsl.go:64` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:52` | `Node` | +| `level[T]` (struct) | `golang/nats-server/server/gsl/gsl.go:71` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:19` | `Level` | +| `newNode[T]` | `golang/nats-server/server/gsl/gsl.go:77` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:52` | Inline `new Node()` | +| `newLevel[T]` | `golang/nats-server/server/gsl/gsl.go:82` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:19` | Inline `new Level()` | +| `NewSublist[T]` | `golang/nats-server/server/gsl/gsl.go:87` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:76` | `new GenericSubjectList()` 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` 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.PruneNode` | +| `node.isEmpty` | `golang/nats-server/server/gsl/gsl.go:418` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:61` | `Node.IsEmpty()` | +| `level.numNodes` | `golang/nats-server/server/gsl/gsl.go:423` | PORTED | `src/NATS.Server/Internal/Gsl/GenericSubjectList.cs:25` | `Level.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 | diff --git a/gaps/jetstream.md b/gaps/jetstream.md new file mode 100644 index 0000000..9dde20a --- /dev/null +++ b/gaps/jetstream.md @@ -0,0 +1,1935 @@ +# JetStream — Gap Analysis + +> This file tracks what has and hasn't been ported from Go to .NET for the **JetStream** 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 JetStream + +- **This is the largest module** (55,228 Go source LOC, 30% ported). Break analysis into sub-areas: + 1. Core orchestration (`jetstream.go`, `jetstream_api.go`) + 2. Stream lifecycle (`stream.go`) — retention policies: Limits, Interest, WorkQueue + 3. Consumer state machine (`consumer.go`) — push vs pull, ack policies (None, All, Explicit) + 4. Storage engine (`filestore.go`, `memstore.go`) — S2 compression (IronSnappy NuGet), AEAD encryption (ChaCha20Poly1305/AesGcm) + 5. Cluster coordination (`jetstream_cluster.go`) — depends on RAFT module +- Go's `filestore.go` alone is 12,600 lines — the single largest implementation file. +- The storage interface (`store.go`) defines the contract; analyze this first to understand the abstraction. + +--- + +## Go Reference Files (Source) + +- `golang/nats-server/server/jetstream.go` — Orchestration, API subject handlers (`$JS.API.*`) +- `golang/nats-server/server/jetstream_api.go` — JetStream API handlers (~5,300 lines) +- `golang/nats-server/server/jetstream_events.go` — JetStream event publishing +- `golang/nats-server/server/jetstream_errors.go` — Error definitions +- `golang/nats-server/server/jetstream_errors_generated.go` — Generated error codes +- `golang/nats-server/server/jetstream_batching.go` — Batch processing +- `golang/nats-server/server/jetstream_versioning.go` — Version compatibility +- `golang/nats-server/server/stream.go` — Stream lifecycle, retention policies (~8,000 lines) +- `golang/nats-server/server/consumer.go` — Consumer state machine (~6,000 lines). Push vs pull, ack policies. +- `golang/nats-server/server/store.go` — Storage abstraction interface +- `golang/nats-server/server/filestore.go` — Block-based persistent storage (~12,600 lines). S2 compression, encryption. +- `golang/nats-server/server/memstore.go` — In-memory storage with TTL +- `golang/nats-server/server/dirstore.go` — Directory-based storage +- `golang/nats-server/server/disk_avail.go` — Disk space checking +- `golang/nats-server/server/jetstream_cluster.go` — Clustered JetStream (~10,900 lines) + +## Go Reference Files (Tests) + +- `golang/nats-server/server/jetstream_test.go` +- `golang/nats-server/server/jetstream_consumer_test.go` +- `golang/nats-server/server/jetstream_errors_test.go` +- `golang/nats-server/server/jetstream_batching_test.go` +- `golang/nats-server/server/jetstream_versioning_test.go` +- `golang/nats-server/server/jetstream_helpers_test.go` +- `golang/nats-server/server/jetstream_jwt_test.go` +- `golang/nats-server/server/jetstream_tpm_test.go` +- `golang/nats-server/server/jetstream_sourcing_scaling_test.go` +- `golang/nats-server/server/jetstream_benchmark_test.go` +- `golang/nats-server/server/filestore_test.go` +- `golang/nats-server/server/memstore_test.go` +- `golang/nats-server/server/dirstore_test.go` +- `golang/nats-server/server/store_test.go` +- `golang/nats-server/server/jetstream_cluster_1_test.go` through `_4_test.go` +- `golang/nats-server/server/jetstream_super_cluster_test.go` +- `golang/nats-server/server/jetstream_leafnode_test.go` +- `golang/nats-server/server/jetstream_cluster_long_test.go` + +## .NET Implementation Files (Source) + +- `src/NATS.Server/JetStream/` — Root files (orchestration) +- `src/NATS.Server/JetStream/Api/` — API handlers +- `src/NATS.Server/JetStream/Consumers/` — Consumer state machine +- `src/NATS.Server/JetStream/Models/` — StreamConfig, ConsumerConfig, etc. +- `src/NATS.Server/JetStream/MirrorSource/` — Stream mirroring & sourcing +- `src/NATS.Server/JetStream/Publish/` — Publish handling +- `src/NATS.Server/JetStream/Snapshots/` — State snapshots +- `src/NATS.Server/JetStream/Validation/` — Config validation +- `src/NATS.Server/JetStream/Storage/` — FileStore, MemStore, encryption +- `src/NATS.Server/JetStream/Cluster/` — Clustered JetStream + +## .NET Implementation Files (Tests) + +- `tests/NATS.Server.Tests/JetStream/` — Root tests +- `tests/NATS.Server.Tests/JetStream/Api/` +- `tests/NATS.Server.Tests/JetStream/Cluster/` +- `tests/NATS.Server.Tests/JetStream/Consumers/` +- `tests/NATS.Server.Tests/JetStream/MirrorSource/` +- `tests/NATS.Server.Tests/JetStream/Snapshots/` +- `tests/NATS.Server.Tests/JetStream/Storage/` +- `tests/NATS.Server.Tests/JetStream/Streams/` + +--- + +## Gap Inventory + + + + + +### jetstream.go — Core Orchestration (~2866 lines) + +| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | +|-----------|:-------------|--------|:----------------|-------| +| JetStreamConfig (struct) | golang/nats-server/server/jetstream.go:42 | PARTIAL | src/NATS.Server/Configuration/JetStreamOptions.cs:5 | .NET has MaxMemoryStore, MaxFileStore, MaxStreams, MaxConsumers, Domain. Missing: SyncInterval, SyncAlways, CompressOK, UniqueTag, Strict fields | +| JetStreamStats (struct) | golang/nats-server/server/jetstream.go:55 | MISSING | — | Server-level usage stats (Memory, Store, ReservedMemory, ReservedStore, Accounts, HAAssets, API) not modeled | +| JetStreamAccountLimits (struct) | golang/nats-server/server/jetstream.go:65 | PARTIAL | src/NATS.Server/Configuration/JetStreamOptions.cs:5 | MaxStreams/MaxConsumers present. Missing: MaxAckPending, MemoryMaxStreamBytes, StoreMaxStreamBytes, MaxBytesRequired, tiered limits | +| JetStreamTier (struct) | golang/nats-server/server/jetstream.go:76 | MISSING | — | Tiered accounting not implemented | +| JetStreamAccountStats (struct) | golang/nats-server/server/jetstream.go:87 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:105 | JetStreamAccountInfo has Streams/Consumers counts only. Missing: memory/store usage, tiers, domain, API stats | +| JetStreamAPIStats (struct) | golang/nats-server/server/jetstream.go:95 | MISSING | — | API level/total/errors/inflight stats not tracked | +| jetStream (internal struct) | golang/nats-server/server/jetstream.go:103 | PARTIAL | src/NATS.Server/JetStream/JetStreamService.cs:11 | JetStreamService covers lifecycle. Missing: apiInflight/apiTotal/apiErrors atomics, memUsed/storeUsed tracking, accounts map, apiSubs, cluster, oos/shuttingDown state | +| jsAccount (internal struct) | golang/nats-server/server/jetstream.go:151 | MISSING | — | Per-account JetStream state (streams map, usage tracking, cluster usage updates) not modeled | +| jsaUsage (internal struct) | golang/nats-server/server/jetstream.go:181 | MISSING | — | Per-account mem/store usage tracking | +| EnableJetStream (Server method) | golang/nats-server/server/jetstream.go:188 | PARTIAL | src/NATS.Server/JetStream/JetStreamService.cs:95 | StartAsync handles StoreDir creation and API subject registration. Missing: dynamic config, system memory detection, encryption init, cluster init | +| jsKeyGen (Server method) | golang/nats-server/server/jetstream.go:240 | MISSING | — | HMAC-SHA256 key generation for JetStream encryption | +| decryptMeta (Server method) | golang/nats-server/server/jetstream.go:257 | MISSING | — | Encrypted metafile decryption (AEAD with cipher fallback) | +| checkStoreDir (Server method) | golang/nats-server/server/jetstream.go:320 | MISSING | — | Legacy store directory migration logic | +| initJetStreamEncryption (Server method) | golang/nats-server/server/jetstream.go:382 | MISSING | — | TPM-backed encryption key initialization | +| enableJetStream (Server method) | golang/nats-server/server/jetstream.go:414 | PARTIAL | src/NATS.Server/JetStream/JetStreamService.cs:95 | Core startup covered. Missing: gcbOutMax, system account setup, JS banner, encryption notice, internal subscriptions setup, cluster mode init | +| canExtendOtherDomain (Server method) | golang/nats-server/server/jetstream.go:533 | MISSING | — | Leaf node domain extension check | +| restartJetStream (Server method) | golang/nats-server/server/jetstream.go:556 | MISSING | — | Re-enable JetStream during config reload | +| setupJetStreamExports (Server method) | golang/nats-server/server/jetstream.go:590 | MISSING | — | System account service export setup | +| handleOutOfSpace (Server method) | golang/nats-server/server/jetstream.go:613 | MISSING | — | OOS handling: disable JS + advisory | +| DisableJetStream (Server method) | golang/nats-server/server/jetstream.go:643 | PARTIAL | src/NATS.Server/JetStream/JetStreamService.cs:141 | DisposeAsync clears subjects. Missing: cluster meta leader transfer, RAFT node cleanup | +| enableJetStreamAccounts (Server method) | golang/nats-server/server/jetstream.go:690 | MISSING | — | Multi-account JS enablement with parallel task workers | +| enableAllJetStreamServiceImportsAndMappings (Account method) | golang/nats-server/server/jetstream.go:714 | MISSING | — | Per-account service imports and domain mappings | +| configJetStream (Server method) | golang/nats-server/server/jetstream.go:771 | MISSING | — | Per-account JS config (enable/update/disable) | +| configAllJetStreamAccounts (Server method) | golang/nats-server/server/jetstream.go:809 | MISSING | — | Walk all accounts and restore JetStream state | +| JetStreamEnabled (Server method) | golang/nats-server/server/jetstream.go:904 | PARTIAL | src/NATS.Server/JetStream/JetStreamService.cs:48 | IsRunning property equivalent | +| JetStreamEnabledForDomain (Server method) | golang/nats-server/server/jetstream.go:909 | MISSING | — | Domain-wide JS availability check | +| signalPullConsumers (Server method) | golang/nats-server/server/jetstream.go:930 | MISSING | — | Shutdown signal to R1 pull consumers | +| shutdownJetStream (Server method) | golang/nats-server/server/jetstream.go:977 | PARTIAL | src/NATS.Server/JetStream/JetStreamService.cs:141 | Basic cleanup in DisposeAsync. Missing: account removal, cluster qch signaling | +| JetStreamConfig (Server method) | golang/nats-server/server/jetstream.go:1055 | MISSING | — | Returns copy of current config | +| StoreDir (Server method) | golang/nats-server/server/jetstream.go:1065 | MISSING | — | Returns current StoreDir | +| JetStreamNumAccounts (Server method) | golang/nats-server/server/jetstream.go:1074 | MISSING | — | Enabled account count | +| JetStreamReservedResources (Server method) | golang/nats-server/server/jetstream.go:1085 | MISSING | — | Reserved mem/store bytes | +| Account.EnableJetStream | golang/nats-server/server/jetstream.go:1107 | MISSING | — | Per-account JS enablement with limits, store dir, cluster usage | +| Account.UpdateJetStreamLimits | golang/nats-server/server/jetstream.go:1736 | MISSING | — | Update account limits with delta checking | +| Account.JetStreamUsage | golang/nats-server/server/jetstream.go:1834 | MISSING | — | Full account usage stats with tier breakdown | +| Account.DisableJetStream | golang/nats-server/server/jetstream.go:1961 | MISSING | — | Per-account JS disable | +| Account.lookupStream | golang/nats-server/server/jetstream.go:1717 | MISSING | — | Stream lookup by name (via jsa) | +| Account.streams / filteredStreams | golang/nats-server/server/jetstream.go:1681 | MISSING | — | All/filtered stream enumeration | +| jsAccount.updateUsage | golang/nats-server/server/jetstream.go:2187 | MISSING | — | Storage usage delta tracking | +| jsAccount.checkAndSyncUsage | golang/nats-server/server/jetstream.go:2108 | MISSING | — | Usage drift detection and correction | +| jsAccount.sendClusterUsageUpdate | golang/nats-server/server/jetstream.go:2250 | MISSING | — | Binary-encoded cluster usage publication | +| jsAccount.remoteUpdateUsage | golang/nats-server/server/jetstream.go:2021 | MISSING | — | Process incoming remote usage updates | +| jetStream.wouldExceedLimits | golang/nats-server/server/jetstream.go:2299 | MISSING | — | Server-level resource limit check | +| jetStream.checkLimits | golang/nats-server/server/jetstream.go:2434 | MISSING | — | Account+server limit checking for stream config | +| jetStream.checkBytesLimits | golang/nats-server/server/jetstream.go:2446 | MISSING | — | Byte-level limits check for mem/file storage | +| jetStream.sufficientResources | golang/nats-server/server/jetstream.go:2549 | MISSING | — | System resource sufficiency check for new accounts | +| jetStream.reserveStreamResources | golang/nats-server/server/jetstream.go:2606 | MISSING | — | Reserve MaxBytes for a stream | +| jetStream.releaseStreamResources | golang/nats-server/server/jetstream.go:2627 | MISSING | — | Release reserved MaxBytes | +| jetStream.usageStats | golang/nats-server/server/jetstream.go:2520 | MISSING | — | Server-wide JS usage stats | +| jsAccount.selectLimits | golang/nats-server/server/jetstream.go:2337 | MISSING | — | Tier-based limit selection | +| jsAccount.storageTotals | golang/nats-server/server/jetstream.go:2360 | MISSING | — | Aggregate mem/store totals | +| jsAccount.reservedStorage | golang/nats-server/server/jetstream.go:1801 | MISSING | — | Reserved bytes by tier | +| jsAccount.delete | golang/nats-server/server/jetstream.go:2481 | MISSING | — | Delete all JS resources for account | +| dynJetStreamConfig (Server method) | golang/nats-server/server/jetstream.go:2659 | MISSING | — | Dynamic config: 75% sysmem, disk available | +| isValidName | golang/nats-server/server/jetstream.go:2735 | MISSING | — | Name validation (no spaces, wildcards) | +| friendlyBytes | golang/nats-server/server/jetstream.go:2723 | NOT_APPLICABLE | — | Logging helper; .NET has built-in formatting | +| tierName | golang/nats-server/server/jetstream.go:2316 | MISSING | — | Compute tier name from replica count | +| validateJetStreamOptions | golang/nats-server/server/jetstream.go:2767 | MISSING | — | Validates JS options (domain, cluster, etc.) | +| fixCfgMirrorWithDedupWindow | golang/nats-server/server/jetstream.go:2848 | NOT_APPLICABLE | — | Bug fix for legacy config; not needed in new port | +| JetStreamStoreDir (const) | golang/nats-server/server/jetstream.go:2649 | MISSING | — | "jetstream" directory name constant | +| JetStreamMaxStoreDefault (const) | golang/nats-server/server/jetstream.go:2651 | MISSING | — | Default 1TB disk limit | +| JetStreamMaxMemDefault (const) | golang/nats-server/server/jetstream.go:2653 | MISSING | — | Default 256MB mem limit | +| Stream recovery logic (doStream/doConsumers) | golang/nats-server/server/jetstream.go:1223-1636 | MISSING | — | Full stream/consumer recovery from disk: metafile reading, checksum, encryption, versioning, subject repair | +| keyGen (type) | golang/nats-server/server/jetstream.go:237 | MISSING | — | Key generation function signature for encryption | +| resourcesExceededError (Server method) | golang/nats-server/server/jetstream.go:2743 | MISSING | — | Throttled error logging + meta leader stepdown | +| handleWritePermissionError (Server method) | golang/nats-server/server/jetstream.go:2857 | MISSING | — | FS permission error handling | + +### jetstream_api.go — API Handlers (~5165 lines) + +| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | +|-----------|:-------------|--------|:----------------|-------| +| JSApi* subject constants (50+) | golang/nats-server/server/jetstream_api.go:36-312 | PORTED | src/NATS.Server/JetStream/Api/JetStreamApiSubjects.cs:1 | All major API subjects defined. Minor: some template variants (T-suffixed) not needed in .NET | +| JSAdvisory* prefix constants (25+) | golang/nats-server/server/jetstream_api.go:229-311 | PARTIAL | src/NATS.Server/JetStream/Api/AdvisoryPublisher.cs:1 | Stream create/delete/update, consumer create/delete covered. Missing: snapshot, restore, leader elected, quorum lost, batch abandoned, out-of-storage, server removed, API limit, pause, pinned, unpinned advisory prefixes | +| JSMaxDescriptionLen (const) | golang/nats-server/server/jetstream_api.go:352 | MISSING | — | 4096 byte limit for descriptions | +| JSMaxMetadataLen (const) | golang/nats-server/server/jetstream_api.go:356 | MISSING | — | 128KB metadata map size limit | +| JSMaxNameLen (const) | golang/nats-server/server/jetstream_api.go:360 | MISSING | — | 255 char name length limit | +| JSDefaultRequestQueueLimit (const) | golang/nats-server/server/jetstream_api.go:364 | MISSING | — | 10,000 request queue limit | +| ApiResponse (struct) | golang/nats-server/server/jetstream_api.go:369 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:5 | Type field missing; error structure simplified | +| ApiPaged / ApiPagedRequest (structs) | golang/nats-server/server/jetstream_api.go:395-404 | MISSING | — | Paged API request/response not implemented | +| JSApiAccountInfoResponse | golang/nats-server/server/jetstream_api.go:407 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:105 | Basic streams/consumers count. Missing: full JetStreamAccountStats embedding | +| JSApiStreamCreateResponse | golang/nats-server/server/jetstream_api.go:415 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:94 | StreamInfo returned. Missing: DidCreate field | +| JSApiStreamDeleteResponse | golang/nats-server/server/jetstream_api.go:423 | PORTED | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:17 | Success field present | +| JSApiStreamInfoRequest / Response | golang/nats-server/server/jetstream_api.go:434-446 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:94 | Basic info works. Missing: DeletedDetails, SubjectsFilter, paged response | +| JSApiStreamNamesRequest / Response | golang/nats-server/server/jetstream_api.go:453-467 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:11 | StreamNames returned. Missing: paging, subject filter | +| JSApiStreamListRequest / Response | golang/nats-server/server/jetstream_api.go:469-485 | MISSING | — | Detailed stream list with paging, missing/offline | +| JSApiStreamPurgeRequest / Response | golang/nats-server/server/jetstream_api.go:492-507 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:76 | Purge response with count. Missing: filter/sequence/keep options in request | +| JSApiStreamUpdateResponse | golang/nats-server/server/jetstream_api.go:519 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:94 | Returns StreamInfo. Response type constant missing | +| JSApiMsgDeleteRequest / Response | golang/nats-server/server/jetstream_api.go:528-538 | PORTED | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleMessageDelete implemented | +| JSApiStreamSnapshotRequest / Response | golang/nats-server/server/jetstream_api.go:540-569 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:125 | Basic snapshot modeled. Missing: DeliverSubject, NoConsumers, ChunkSize, WindowSize, CheckMsgs options | +| JSApiStreamRestoreRequest / Response | golang/nats-server/server/jetstream_api.go:572-586 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleRestore exists. Missing: chunked delivery protocol | +| JSApiStreamRemovePeerRequest / Response | golang/nats-server/server/jetstream_api.go:589-600 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ClusterControlApiHandlers.cs | HandleStreamPeerRemove exists as stub | +| JSApiStreamLeaderStepDownResponse | golang/nats-server/server/jetstream_api.go:603-608 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ClusterControlApiHandlers.cs | HandleStreamLeaderStepdown exists | +| JSApiConsumerLeaderStepDownResponse | golang/nats-server/server/jetstream_api.go:611-616 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ClusterControlApiHandlers.cs | HandleConsumerLeaderStepdown exists | +| JSApiLeaderStepdownRequest / Response | golang/nats-server/server/jetstream_api.go:619-629 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ClusterControlApiHandlers.cs | HandleMetaLeaderStepdown exists | +| JSApiMetaServerRemoveRequest / Response | golang/nats-server/server/jetstream_api.go:632-646 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/AccountControlApiHandlers.cs | HandleServerRemove exists as stub | +| JSApiMetaServerStreamMoveRequest | golang/nats-server/server/jetstream_api.go:650-659 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/AccountControlApiHandlers.cs | HandleAccountStreamMove exists as stub | +| JSApiAccountPurgeResponse | golang/nats-server/server/jetstream_api.go:663-667 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/AccountControlApiHandlers.cs | HandleAccountPurge exists as stub | +| JSApiMsgGetRequest / Response | golang/nats-server/server/jetstream_api.go:670-699 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleMessageGet exists. Missing: Batch, MaxBytes, StartTime, MultiLastFor, UpToSeq/Time, NoHeaders | +| JSApiConsumerCreateResponse | golang/nats-server/server/jetstream_api.go:704-709 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:100 | ConsumerInfo returned. Response type constant missing | +| JSApiConsumerDeleteResponse | golang/nats-server/server/jetstream_api.go:711-716 | PORTED | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleDelete implemented | +| JSApiConsumerPauseRequest / Response | golang/nats-server/server/jetstream_api.go:718-729 | PORTED | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:86 | PauseResponse with Paused/PauseUntil | +| JSApiConsumerInfoResponse | golang/nats-server/server/jetstream_api.go:731-736 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:100 | ConsumerInfo returned. Missing: type field | +| JSApiConsumerNamesResponse | golang/nats-server/server/jetstream_api.go:742-748 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiResponse.cs:12 | ConsumerNames returned. Missing: paging | +| JSApiConsumerListResponse | golang/nats-server/server/jetstream_api.go:750-758 | MISSING | — | Detailed consumer list with paging, missing/offline | +| JSApiConsumerGetNextRequest | golang/nats-server/server/jetstream_api.go:761-768 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleNext exists. Missing: Expires, MaxBytes, Heartbeat, PriorityGroup | +| JSApiConsumerResetRequest / Response | golang/nats-server/server/jetstream_api.go:771-781 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleReset exists as stub | +| JSApiConsumerUnpinRequest / Response | golang/nats-server/server/jetstream_api.go:509-517 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleUnpin exists as stub | +| generateJSMappingTable | golang/nats-server/server/jetstream_api.go:323-349 | MISSING | — | Domain prefix mapping table generation | +| apiDispatch (jetStream method) | golang/nats-server/server/jetstream_api.go:795 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs:222 | Router.Route matches subjects to handlers. Missing: ClientInfoHdr check, inflight tracking, queue-based dispatch pool | +| processJSAPIRoutedRequests | golang/nats-server/server/jetstream_api.go:882 | MISSING | — | Worker pool goroutine for processing routed API requests | +| setJetStreamExportSubs | golang/nats-server/server/jetstream_api.go:913 | PARTIAL | src/NATS.Server/JetStream/JetStreamService.cs:16 | AllApiSubjects list covers subject registration. Missing: worker pool, Sublist-based routing | +| sendAPIResponse / sendAPIErrResponse | golang/nats-server/server/jetstream_api.go:986-999 | MISSING | — | API response sending with audit advisory | +| sendJetStreamAPIAuditAdvisory | golang/nats-server/server/jetstream_api.go:999+ | MISSING | — | JS API audit advisory publication | +| jsAccountInfoRequest | golang/nats-server/server/jetstream_api.go:1238 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/AccountApiHandlers.cs | HandleInfo returns basic counts. Missing: full JetStreamAccountStats | +| jsStreamCreateRequest | golang/nats-server/server/jetstream_api.go:1335 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleCreate parses config and creates. Missing: clustered forwarding, permission checks, JSRequiredApiLevel header check | +| jsStreamUpdateRequest | golang/nats-server/server/jetstream_api.go:1452 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleUpdate exists. Missing: config diff validation, clustered proposal | +| jsStreamNamesRequest | golang/nats-server/server/jetstream_api.go:1557 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleNames returns list. Missing: paging, subject filter | +| jsStreamListRequest | golang/nats-server/server/jetstream_api.go:1690 | MISSING | — | Detailed stream list with full StreamInfo per entry | +| jsStreamInfoRequest | golang/nats-server/server/jetstream_api.go:1809 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleInfo returns config+state. Missing: subject detail paging, deleted details | +| jsStreamDeleteRequest | golang/nats-server/server/jetstream_api.go:3073 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleDelete works. Missing: clustered forwarding | +| jsStreamPurgeRequest | golang/nats-server/server/jetstream_api.go:3572 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandlePurge works. Missing: filter/sequence/keep options | +| jsMsgDeleteRequest | golang/nats-server/server/jetstream_api.go:3147 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleMessageDelete works. Missing: NoErase option, clustered forwarding | +| jsMsgGetRequest | golang/nats-server/server/jetstream_api.go:3272 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleMessageGet works for basic seq/subject. Missing: batch, multi-last, time-based queries | +| jsStreamSnapshotRequest | golang/nats-server/server/jetstream_api.go:4007 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleSnapshot exists. Missing: chunk delivery, ack flow, window sizing | +| jsStreamRestoreRequest | golang/nats-server/server/jetstream_api.go:3722 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/StreamApiHandlers.cs | HandleRestore exists. Missing: chunk receive protocol | +| jsStreamLeaderStepDownRequest | golang/nats-server/server/jetstream_api.go:2034 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ClusterControlApiHandlers.cs | Stub exists | +| jsStreamRemovePeerRequest | golang/nats-server/server/jetstream_api.go:2275 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ClusterControlApiHandlers.cs | Stub exists | +| jsConsumerCreateRequest | golang/nats-server/server/jetstream_api.go:4244 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleCreate works for basic cases. Missing: JSApiConsumerCreateEx subject parsing, action (Create/Update), config validation depth | +| jsConsumerNamesRequest | golang/nats-server/server/jetstream_api.go:4470 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleNames works. Missing: paging | +| jsConsumerListRequest | golang/nats-server/server/jetstream_api.go:4597 | MISSING | — | Detailed consumer list with paging | +| jsConsumerInfoRequest | golang/nats-server/server/jetstream_api.go:4707 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleInfo returns config. Missing: full ConsumerInfo (delivered, ack_floor, num_ack_pending, etc.) | +| jsConsumerDeleteRequest | golang/nats-server/server/jetstream_api.go:4913 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleDelete works | +| jsConsumerPauseRequest | golang/nats-server/server/jetstream_api.go:4991 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandlePause works. Missing: PauseRemaining duration in response | +| jsConsumerUnpinRequest | golang/nats-server/server/jetstream_api.go:3429 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ConsumerApiHandlers.cs | HandleUnpin exists as stub | +| jsLeaderServerRemoveRequest | golang/nats-server/server/jetstream_api.go:2383 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/AccountControlApiHandlers.cs | Stub exists | +| jsLeaderServerStreamMoveRequest | golang/nats-server/server/jetstream_api.go:2514 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/AccountControlApiHandlers.cs | Stub exists | +| jsLeaderServerStreamCancelMoveRequest | golang/nats-server/server/jetstream_api.go:2678 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/AccountControlApiHandlers.cs | Stub exists | +| jsLeaderAccountPurgeRequest | golang/nats-server/server/jetstream_api.go:2793 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/AccountControlApiHandlers.cs | Stub exists | +| jsLeaderStepDownRequest | golang/nats-server/server/jetstream_api.go:2893 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/ClusterControlApiHandlers.cs | Stub exists | +| delayedAPIResponder | golang/nats-server/server/jetstream_api.go:1047 | MISSING | — | Delayed API response delivery with ordered linked list | +| JSDirectMsgGet handler | golang/nats-server/server/jetstream_api.go:105-111 | PARTIAL | src/NATS.Server/JetStream/Api/Handlers/DirectApiHandlers.cs | HandleGet exists. Missing: batch support, multi-last, header stripping | +| ILeaderForwarder interface | — | PORTED | src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs:10 | .NET-native abstraction for leader forwarding | +| JetStreamApiRouter | — | PORTED | src/NATS.Server/JetStream/Api/JetStreamApiRouter.cs:68 | Subject-to-handler routing with leader check | +| ApiRateLimiter | — | PORTED | src/NATS.Server/JetStream/Api/ApiRateLimiter.cs:11 | Concurrency limiter + dedup cache | +| ClusteredRequestProcessor | — | PORTED | src/NATS.Server/JetStream/Api/ClusteredRequestProcessor.cs:13 | Pending request correlation with TCS pattern | + +### jetstream_events.go — Advisory Event Types (~366 lines) + +| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | +|-----------|:-------------|--------|:----------------|-------| +| publishAdvisory (Server method) | golang/nats-server/server/jetstream_events.go:23 | PARTIAL | src/NATS.Server/JetStream/Api/AdvisoryPublisher.cs:9 | AdvisoryPublisher publishes via delegate. Missing: interest check (SubList.HasInterest), gateway interest check, system account fallback | +| JSAPIAudit (struct) | golang/nats-server/server/jetstream_events.go:50 | MISSING | — | API audit advisory type with Server, Client, Subject, Request, Response, Domain | +| JSAPIAuditType (const) | golang/nats-server/server/jetstream_events.go:60 | MISSING | — | "io.nats.jetstream.advisory.v1.api_audit" | +| ActionAdvisoryType (type + consts) | golang/nats-server/server/jetstream_events.go:63-69 | MISSING | — | CreateEvent/DeleteEvent/ModifyEvent enum | +| JSStreamActionAdvisory (struct) | golang/nats-server/server/jetstream_events.go:72 | PARTIAL | src/NATS.Server/JetStream/Api/AdvisoryPublisher.cs:28 | StreamCreated/Deleted/Updated covered. Missing: Action field, Domain field, TypedEvent base | +| JSConsumerActionAdvisory (struct) | golang/nats-server/server/jetstream_events.go:82 | PARTIAL | src/NATS.Server/JetStream/Api/AdvisoryPublisher.cs:75 | ConsumerCreated/Deleted covered. Missing: Action field, Domain field | +| JSConsumerPauseAdvisory (struct) | golang/nats-server/server/jetstream_events.go:93 | MISSING | — | Consumer pause/unpause advisory | +| JSConsumerAckMetric (struct) | golang/nats-server/server/jetstream_events.go:106 | MISSING | — | Ack latency metric | +| JSConsumerDeliveryExceededAdvisory (struct) | golang/nats-server/server/jetstream_events.go:122 | MISSING | — | Max delivery exceeded advisory | +| JSConsumerDeliveryNakAdvisory (struct) | golang/nats-server/server/jetstream_events.go:135 | MISSING | — | NAK advisory | +| JSConsumerDeliveryTerminatedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:149 | MISSING | — | Terminated message advisory | +| JSSnapshotCreateAdvisory (struct) | golang/nats-server/server/jetstream_events.go:166 | MISSING | — | Snapshot start advisory | +| JSSnapshotCompleteAdvisory (struct) | golang/nats-server/server/jetstream_events.go:177 | MISSING | — | Snapshot complete advisory | +| JSRestoreCreateAdvisory (struct) | golang/nats-server/server/jetstream_events.go:191 | MISSING | — | Restore start advisory | +| JSRestoreCompleteAdvisory (struct) | golang/nats-server/server/jetstream_events.go:201 | MISSING | — | Restore complete advisory | +| JSDomainLeaderElectedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:221 | MISSING | — | Domain leader election advisory | +| JSStreamLeaderElectedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:233 | MISSING | — | Stream leader election advisory | +| JSStreamQuorumLostAdvisory (struct) | golang/nats-server/server/jetstream_events.go:247 | MISSING | — | Stream quorum lost advisory | +| JSStreamBatchAbandonedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:259 | MISSING | — | Batch abandoned advisory | +| BatchAbandonReason (type + consts) | golang/nats-server/server/jetstream_events.go:268-274 | MISSING | — | timeout/large/incomplete reasons | +| JSConsumerLeaderElectedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:280 | MISSING | — | Consumer leader election advisory | +| JSConsumerQuorumLostAdvisory (struct) | golang/nats-server/server/jetstream_events.go:295 | MISSING | — | Consumer quorum lost advisory | +| JSConsumerGroupPinnedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:307 | MISSING | — | Priority group pin advisory | +| JSConsumerGroupUnpinnedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:320 | MISSING | — | Priority group unpin advisory | +| JSServerOutOfSpaceAdvisory (struct) | golang/nats-server/server/jetstream_events.go:335 | MISSING | — | Server out of space advisory | +| JSServerRemovedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:348 | MISSING | — | Server removed advisory | +| JSAPILimitReachedAdvisory (struct) | golang/nats-server/server/jetstream_events.go:360 | MISSING | — | API queue limit reached advisory | + +### jetstream_errors.go — Error Framework (~103 lines) + +| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | +|-----------|:-------------|--------|:----------------|-------| +| ErrorIdentifier (type) | golang/nats-server/server/jetstream_errors.go:29 | MISSING | — | uint16 error identifier type | +| ErrorOption / Unless | golang/nats-server/server/jetstream_errors.go:12-19 | MISSING | — | Functional options for error creation | +| IsNatsErr | golang/nats-server/server/jetstream_errors.go:32 | MISSING | — | Error identity checker by ErrorIdentifier | +| ApiError (struct) | golang/nats-server/server/jetstream_errors.go:57 | PARTIAL | src/NATS.Server/JetStream/Api/JetStreamApiError.cs:3 | Code + Description present. Missing: ErrCode (uint16 error code), Error() method | +| ErrorsData (struct) | golang/nats-server/server/jetstream_errors.go:64 | MISSING | — | Source data for generated errors | +| ApiError.toReplacerArgs | golang/nats-server/server/jetstream_errors.go:79 | MISSING | — | Template replacement helper | + +### jetstream_errors_generated.go — Error Codes (~3176 lines) + +| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | +|-----------|:-------------|--------|:----------------|-------| +| ErrorIdentifier constants (200+) | golang/nats-server/server/jetstream_errors_generated.go:7-400+ | PARTIAL | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:14 | Only 9 atomic-batch error codes ported. Missing: ~190 other error identifiers (stream, consumer, cluster, etc.) | +| ApiErrors map | golang/nats-server/server/jetstream_errors_generated.go:400+ | MISSING | — | Global map[ErrorIdentifier]*ApiError with HTTP codes and descriptions | +| NewJS*Error factory functions (200+) | golang/nats-server/server/jetstream_errors_generated.go:varies | MISSING | — | Factory functions for each error type with template substitution | + +### jetstream_versioning.go — Version Negotiation (~237 lines) + +| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | +|-----------|:-------------|--------|:----------------|-------| +| JSApiLevel (const) | golang/nats-server/server/jetstream_versioning.go:20 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:19 | Value 3 matches | +| JSRequiredLevelMetadataKey (const) | golang/nats-server/server/jetstream_versioning.go:22 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:25 | "_nats.req.level" | +| JSServerVersionMetadataKey (const) | golang/nats-server/server/jetstream_versioning.go:23 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:28 | "_nats.ver" | +| JSServerLevelMetadataKey (const) | golang/nats-server/server/jetstream_versioning.go:24 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:31 | "_nats.level" | +| getRequiredApiLevel | golang/nats-server/server/jetstream_versioning.go:28 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:37 | Exact behavior match | +| supportsRequiredApiLevel | golang/nats-server/server/jetstream_versioning.go:36 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:48 | Exact behavior match | +| setStaticStreamMetadata | golang/nats-server/server/jetstream_versioning.go:44 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:63 | All feature level checks match (TTL, counter, batch, schedule, persist) | +| setDynamicStreamMetadata | golang/nats-server/server/jetstream_versioning.go:88 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:101 | Copy + add version/level | +| copyStreamMetadata | golang/nats-server/server/jetstream_versioning.go:110 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:121 | Copy versioning fields | +| setOrDeleteInStreamMetadata | golang/nats-server/server/jetstream_versioning.go:118 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:197 | Set or delete helper | +| setStaticConsumerMetadata | golang/nats-server/server/jetstream_versioning.go:136 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:132 | PauseUntil + priority checks match | +| setDynamicConsumerMetadata | golang/nats-server/server/jetstream_versioning.go:164 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:157 | Copy + add version/level | +| setDynamicConsumerInfoMetadata | golang/nats-server/server/jetstream_versioning.go:181 | MISSING | — | Wraps setDynamicConsumerMetadata for ConsumerInfo | +| copyConsumerMetadata | golang/nats-server/server/jetstream_versioning.go:198 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:176 | Copy versioning fields | +| setOrDeleteInConsumerMetadata | golang/nats-server/server/jetstream_versioning.go:206 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:213 | Set or delete helper | +| deleteDynamicMetadata | golang/nats-server/server/jetstream_versioning.go:222 | PORTED | src/NATS.Server/JetStream/JsVersioning.cs:187 | Remove version/level keys | +| errorOnRequiredApiLevel | golang/nats-server/server/jetstream_versioning.go:229 | MISSING | — | Header-based API level rejection check | + +### jetstream_batching.go — Batch Operations (~663 lines) + +| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | +|-----------|:-------------|--------|:----------------|-------| +| globalInflightBatches (var) | golang/nats-server/server/jetstream_batching.go:31 | MISSING | — | Global atomic counter across all streams | +| batching (struct) | golang/nats-server/server/jetstream_batching.go:35 | PARTIAL | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:91 | AtomicBatchPublishEngine uses ConcurrentDictionary. Missing: per-group store, timer-based cleanup | +| batchGroup (struct) | golang/nats-server/server/jetstream_batching.go:40 | PARTIAL | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:61 | InFlightBatch tracks messages. Missing: lseq, store (StreamStore), timer | +| batchGroup.newBatchGroup | golang/nats-server/server/jetstream_batching.go:47 | PARTIAL | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:150 | Batch creation in Process(). Missing: per-batch file store, server-configured timeout | +| getBatchStoreDir | golang/nats-server/server/jetstream_batching.go:66 | MISSING | — | Compute batch store directory path | +| newBatchStore | golang/nats-server/server/jetstream_batching.go:79 | MISSING | — | Create file or memory store for batch staging | +| batchGroup.readyForCommit | golang/nats-server/server/jetstream_batching.go:104 | PARTIAL | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:258 | Commit path exists. Missing: timer.Stop() check, FlushAllPending | +| batchGroup.cleanup / cleanupLocked | golang/nats-server/server/jetstream_batching.go:113-125 | PARTIAL | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:317 | EvictExpiredBatches. Missing: store.Delete, globalInflightBatches decrement | +| batchGroup.stopLocked | golang/nats-server/server/jetstream_batching.go:128 | MISSING | — | Stop batch without cleanup | +| batchStagedDiff (struct) | golang/nats-server/server/jetstream_batching.go:135 | MISSING | — | Staged diff for consistency checks: msgIds, counter, inflight, expectedPerSubject | +| batchStagedDiff.commit | golang/nats-server/server/jetstream_batching.go:147 | MISSING | — | Commit staged diff to stream state | +| batchApply (struct) | golang/nats-server/server/jetstream_batching.go:198 | MISSING | — | Cluster-level batch apply state | +| batchApply.clearBatchStateLocked | golang/nats-server/server/jetstream_batching.go:209 | MISSING | — | Clear in-memory batch state | +| batchApply.rejectBatchStateLocked | golang/nats-server/server/jetstream_batching.go:220 | MISSING | — | Reject batch and adjust CLFS | +| checkMsgHeadersPreClusteredProposal | golang/nats-server/server/jetstream_batching.go:240 | MISSING | — | Pre-proposal header validation (~420 lines): expected-seq, per-subject-seq, msg ID dedup, counter CRDT, TTL, scheduling, rollup, discard-new checks | +| AtomicBatchPublishEngine (.NET-only) | — | PORTED | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:91 | .NET-native batch engine with stage/commit/error flow | +| AtomicBatchPublishErrorCodes (.NET-only) | — | PORTED | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:14 | 9 error codes matching Go generated codes | +| BatchPublishRequest (.NET-only) | — | PORTED | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:331 | Request model for batch messages | +| AtomicBatchResult (.NET-only) | — | PORTED | src/NATS.Server/JetStream/Publish/AtomicBatchPublishEngine.cs:358 | Staged/Committed/Error result type | + +--- + +## 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/JetStream/ -name '*.cs' -type f -exec cat {} + | wc -l + # Re-count .NET test LOC for this module + find tests/NATS.Server.Tests/JetStream/ -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')" + ``` + + + +| `StorageType` (enum: FileStorage, MemoryStorage) | store.go:30-38 | PORTED | `Models/StreamConfig.cs` `StorageType` enum + `Models/JetStreamPolicies.cs` | Values mapped as `Memory`/`File` | +| `ErrStoreClosed` | store.go:42 | MISSING | — | No sentinel error constants defined in .NET storage layer | +| `ErrStoreMsgNotFound` | store.go:44 | MISSING | — | Same | +| `ErrStoreEOF` | store.go:46 | MISSING | — | Same | +| `ErrMaxMsgs` | store.go:48 | MISSING | — | Same | +| `ErrMaxBytes` | store.go:50 | MISSING | — | Same | +| `ErrMaxMsgsPerSubject` | store.go:52 | MISSING | — | Same | +| `ErrStoreSnapshotInProgress` | store.go:55 | MISSING | — | Same | +| `ErrMsgTooLarge` | store.go:57 | MISSING | — | Same | +| `ErrStoreWrongType` | store.go:59 | MISSING | — | Same | +| `ErrNoAckPolicy` | store.go:61 | MISSING | — | Same | +| `ErrSequenceMismatch` | store.go:63 | MISSING | — | Same | +| `ErrCorruptStreamState` | store.go:65 | MISSING | — | Same | +| `ErrTooManyResults` | store.go:67 | MISSING | — | Same | +| `StoreMsg` (struct) | store.go:71-78 | PORTED | `Storage/StoreMsg.cs` | Class with Subject, Header, Data, Sequence, Timestamp; Clear() matches Go's clear() | +| `StoreMsg.copy()` | store.go:740-748 | MISSING | — | No buffer-reuse copy semantics; .NET class doesn't pool backing buffer | +| `StoreMsg.clear()` | store.go:751-759 | PORTED | `StoreMsg.Clear()` | Simplified — no backing buffer pool | +| `StorageUpdateHandler` (callback) | store.go:82-83 | MISSING | — | No callback registration model in .NET IStreamStore | +| `StorageRemoveMsgHandler` (callback) | store.go:85 | MISSING | — | Same | +| `ProcessJetStreamMsgHandler` (callback) | store.go:89 | MISSING | — | Same | +| `StreamStore` (interface) | store.go:91-135 | PORTED | `Storage/IStreamStore.cs` | All 34 Go methods have signatures; default implementations throw NotSupportedException | +| `StreamStore.StoreMsg` | store.go:92 | PORTED | `IStreamStore.StoreMsg` | Signature matches | +| `StreamStore.StoreRawMsg` | store.go:93 | PORTED | `IStreamStore.StoreRawMsg` | Signature matches | +| `StreamStore.SkipMsg` | store.go:94 | PORTED | `IStreamStore.SkipMsg` | Returns ulong only (Go returns (uint64, error)) | +| `StreamStore.SkipMsgs` | store.go:95 | PORTED | `IStreamStore.SkipMsgs` | Signature matches | +| `StreamStore.FlushAllPending` | store.go:96 | PORTED | `IStreamStore.FlushAllPending` | Returns Task (async adaptation) | +| `StreamStore.LoadMsg` | store.go:97 | PORTED | `IStreamStore.LoadMsg` | Signature matches | +| `StreamStore.LoadNextMsg` | store.go:98 | PORTED | `IStreamStore.LoadNextMsg` | Returns tuple `(StoreMsg, ulong Skip)` | +| `StreamStore.LoadNextMsgMulti` | store.go:99 | MISSING | — | gsl.SimpleSublist not ported; no multi-filter load | +| `StreamStore.LoadLastMsg` | store.go:100 | PORTED | `IStreamStore.LoadLastMsg` | Signature matches | +| `StreamStore.LoadPrevMsg` | store.go:101 | PORTED | `IStreamStore.LoadPrevMsg` | Signature matches | +| `StreamStore.LoadPrevMsgMulti` | store.go:102 | MISSING | — | gsl.SimpleSublist not ported | +| `StreamStore.RemoveMsg` | store.go:103 | PORTED | `IStreamStore.RemoveMsg` | Returns bool only (Go returns (bool, error)) | +| `StreamStore.EraseMsg` | store.go:104 | PORTED | `IStreamStore.EraseMsg` | Same note | +| `StreamStore.Purge` | store.go:105 | PORTED | `IStreamStore.Purge` | Returns ulong only | +| `StreamStore.PurgeEx` | store.go:106 | PORTED | `IStreamStore.PurgeEx` | Signature matches | +| `StreamStore.Compact` | store.go:107 | PORTED | `IStreamStore.Compact` | Signature matches | +| `StreamStore.Truncate` | store.go:108 | PORTED | `IStreamStore.Truncate` | Void (Go returns error) | +| `StreamStore.GetSeqFromTime` | store.go:109 | PORTED | `IStreamStore.GetSeqFromTime` | Uses DateTime instead of time.Time | +| `StreamStore.FilteredState` | store.go:110 | PORTED | `IStreamStore.FilteredState` | Signature matches | +| `StreamStore.SubjectsState` | store.go:111 | PORTED | `IStreamStore.SubjectsState` | Returns Dictionary | +| `StreamStore.SubjectsTotals` | store.go:112 | PORTED | `IStreamStore.SubjectsTotals` | Returns Dictionary | +| `StreamStore.AllLastSeqs` | store.go:113 | PORTED | `IStreamStore.AllLastSeqs` | Returns ulong[] | +| `StreamStore.MultiLastSeqs` | store.go:114 | PORTED | `IStreamStore.MultiLastSeqs` | Signature matches | +| `StreamStore.SubjectForSeq` | store.go:115 | PORTED | `IStreamStore.SubjectForSeq` | Signature matches | +| `StreamStore.NumPending` | store.go:116 | PORTED | `IStreamStore.NumPending` | Returns tuple `(ulong Total, ulong ValidThrough)` | +| `StreamStore.NumPendingMulti` | store.go:117 | MISSING | — | gsl.SimpleSublist not ported | +| `StreamStore.State` | store.go:118 | PORTED | `IStreamStore.State` | Returns `Storage.StreamState` | +| `StreamStore.FastState` | store.go:119 | PORTED | `IStreamStore.FastState(ref)` | Uses `ref` parameter | +| `StreamStore.EncodedStreamState` | store.go:120 | PORTED | `IStreamStore.EncodedStreamState` | Signature matches | +| `StreamStore.SyncDeleted` | store.go:121 | MISSING | — | DeleteBlocks interface not ported | +| `StreamStore.Type` | store.go:122 | PORTED | `IStreamStore.Type` | Signature matches | +| `StreamStore.RegisterStorageUpdates` | store.go:123 | MISSING | — | No callback registration | +| `StreamStore.RegisterStorageRemoveMsg` | store.go:124 | MISSING | — | No callback registration | +| `StreamStore.RegisterProcessJetStreamMsg` | store.go:125 | MISSING | — | No callback registration | +| `StreamStore.UpdateConfig` | store.go:126 | PORTED | `IStreamStore.UpdateConfig` | Signature matches | +| `StreamStore.Delete` | store.go:127 | PORTED | `IStreamStore.Delete` | Signature matches | +| `StreamStore.Stop` | store.go:128 | PORTED | `IStreamStore.Stop` | Signature matches | +| `StreamStore.ConsumerStore` | store.go:129 | PORTED | `IStreamStore.ConsumerStore` | Returns IConsumerStore | +| `StreamStore.AddConsumer` | store.go:130 | MISSING | — | Not in IStreamStore | +| `StreamStore.RemoveConsumer` | store.go:131 | MISSING | — | Not in IStreamStore | +| `StreamStore.Snapshot` | store.go:132 | PARTIAL | `Snapshots/StreamSnapshotService.cs` | TAR+S2 snapshot exists but not on IStreamStore interface; different signature | +| `StreamStore.Utilization` | store.go:133 | MISSING | — | Not in IStreamStore | +| `StreamStore.ResetState` | store.go:134 | PORTED | `IStreamStore.ResetState` | Signature matches | +| `RetentionPolicy` (enum) | store.go:137-148 | PORTED | `Models/JetStreamPolicies.cs` `RetentionPolicy` | Limits, Interest, WorkQueue | +| `RetentionPolicy.String()` | store.go:478-489 | NOT_APPLICABLE | — | .NET enum has ToString() by default | +| `RetentionPolicy.MarshalJSON` | store.go:491-502 | NOT_APPLICABLE | — | System.Text.Json handles enum serialization | +| `RetentionPolicy.UnmarshalJSON` | store.go:504-516 | NOT_APPLICABLE | — | Same | +| `DiscardPolicy` (enum) | store.go:152-159 | PORTED | `Models/JetStreamPolicies.cs` `DiscardPolicy` | Old, New | +| `DiscardPolicy.String/MarshalJSON/UnmarshalJSON` | store.go:518-550 | NOT_APPLICABLE | — | Handled by System.Text.Json | +| `StorageType.String/MarshalJSON/UnmarshalJSON` | store.go:562-594 | NOT_APPLICABLE | — | Handled by System.Text.Json | +| `AckPolicy` (enum: AckNone, AckAll, AckExplicit) | store.go:596-633 | PORTED | `Models/ConsumerConfig.cs` `AckPolicy` | None, Explicit, All | +| `AckPolicy.MarshalJSON/UnmarshalJSON` | store.go:608-633 | NOT_APPLICABLE | — | Handled by System.Text.Json | +| `ReplayPolicy` (enum) | store.go:635-666 | PORTED | `Models/JetStreamPolicies.cs` `ReplayPolicy` | Instant, Original | +| `ReplayPolicy.MarshalJSON/UnmarshalJSON` | store.go:645-666 | NOT_APPLICABLE | — | Same | +| `DeliverPolicy` (enum) | store.go:668-726 | PORTED | `Models/JetStreamPolicies.cs` `DeliverPolicy` | All, Last, New, ByStartSequence, ByStartTime, LastPerSubject | +| `DeliverPolicy.MarshalJSON/UnmarshalJSON` | store.go:688-726 | NOT_APPLICABLE | — | Same | +| `StreamState` (struct) | store.go:162-175 | PORTED | `Storage/StreamState.cs` | record struct with all fields | +| `SimpleState` (struct) | store.go:178-187 | PORTED | `Storage/StreamState.cs` | record struct; internal firstNeedsUpdate/lastNeedsUpdate fields not ported | +| `LostStreamData` (struct) | store.go:190-193 | PORTED | `Storage/StreamState.cs` `LostStreamData` class | Msgs and Bytes fields | +| `SnapshotResult` (struct) | store.go:196-200 | PARTIAL | `Snapshots/StreamSnapshotService.cs` `SnapshotRestoreResult` | Different shape; no Reader/errCh; restore-oriented | +| Stream state encoding constants | store.go:202-211 | PARTIAL | `Storage/ConsumerStateCodec.cs` | Magic/version constants exist for consumer state; stream state encoding constants not in dedicated file | +| `DeleteBlock` (interface) | store.go:218-221 | MISSING | — | Not ported | +| `DeleteBlocks` (slice type) | store.go:223 | MISSING | — | Not ported | +| `StreamReplicatedState` (struct) | store.go:227-234 | MISSING | — | Not ported as standalone type | +| `IsEncodedStreamState()` | store.go:237-239 | MISSING | — | Not ported | +| `DecodeStreamState()` | store.go:243-305 | MISSING | — | Not ported | +| `DeleteRange` (struct + methods) | store.go:308-328 | MISSING | — | Not ported | +| `DeleteSlice` (type + methods) | store.go:331-347 | MISSING | — | Not ported | +| `DeleteBlocks.NumDeleted()` | store.go:349-355 | MISSING | — | Not ported | +| `ConsumerStore` (interface) | store.go:358-374 | PORTED | `Storage/IConsumerStore.cs` | All 13 methods ported | +| `ConsumerStore.SetStarting` | store.go:359 | PORTED | `IConsumerStore.SetStarting` | Void (Go returns error) | +| `ConsumerStore.UpdateStarting` | store.go:360 | PORTED | `IConsumerStore.UpdateStarting` | Matches | +| `ConsumerStore.Reset` | store.go:361 | PORTED | `IConsumerStore.Reset` | Void (Go returns error) | +| `ConsumerStore.HasState` | store.go:362 | PORTED | `IConsumerStore.HasState` | Matches | +| `ConsumerStore.UpdateDelivered` | store.go:363 | PORTED | `IConsumerStore.UpdateDelivered` | Void (Go returns error) | +| `ConsumerStore.UpdateAcks` | store.go:364 | PORTED | `IConsumerStore.UpdateAcks` | Void (Go returns error) | +| `ConsumerStore.UpdateConfig` | store.go:365 | MISSING | — | Not on IConsumerStore | +| `ConsumerStore.Update` | store.go:366 | PORTED | `IConsumerStore.Update` | Matches | +| `ConsumerStore.State` | store.go:367 | PORTED | `IConsumerStore.State` | Returns `ConsumerState` (Go returns `(*ConsumerState, error)`) | +| `ConsumerStore.BorrowState` | store.go:368 | PORTED | `IConsumerStore.BorrowState` | Matches | +| `ConsumerStore.EncodedState` | store.go:369 | PORTED | `IConsumerStore.EncodedState` | Returns byte[] | +| `ConsumerStore.Type` | store.go:370 | PORTED | `IConsumerStore.Type` | Matches | +| `ConsumerStore.Stop` | store.go:371 | PORTED | `IConsumerStore.Stop` | Matches | +| `ConsumerStore.Delete` | store.go:372 | PORTED | `IConsumerStore.Delete` | Matches | +| `ConsumerStore.StreamDelete` | store.go:373 | PORTED | `IConsumerStore.StreamDelete` | Matches | +| `SequencePair` (struct) | store.go:377-380 | PORTED | `Storage/ConsumerState.cs` `SequencePair` | record struct | +| `ConsumerState` (struct) | store.go:383-394 | PORTED | `Storage/ConsumerState.cs` | Class with Delivered, AckFloor, Pending, Redelivered | +| `Pending` (struct) | store.go:461-464 | PORTED | `Storage/ConsumerState.cs` `Pending` | record struct | +| `encodeConsumerState()` | store.go:397-457 | PORTED | `Storage/ConsumerStateCodec.Encode()` | Wire-compatible binary encoding | +| `isOutOfSpaceErr()` | store.go:728-730 | MISSING | — | No dedicated helper | +| `errFirstSequenceMismatch` | store.go:733 | MISSING | — | Not ported | +| `isClusterResetErr()` | store.go:735-737 | MISSING | — | Not ported | +| `bytesToString()` | store.go:763-769 | NOT_APPLICABLE | — | .NET strings are native; no unsafe cast needed | +| `stringToBytes()` | store.go:772-778 | NOT_APPLICABLE | — | Same | +| `copyString()` | store.go:783-787 | NOT_APPLICABLE | — | .NET strings are immutable; always safe | +| `isPermissionError()` | store.go:789-791 | MISSING | — | No dedicated helper | + +| `StreamConfigRequest` | stream.go:41-46 | MISSING | — | No `Pedantic` field in .NET; validation is in `JetStreamConfigValidator` | +| `StreamConfig` (struct, ~80 fields) | stream.go:50-125 | PARTIAL | `Models/StreamConfig.cs` | Core fields ported; missing: `NoAck`, `Placement`, `Compression`, `MirrorDirect`, `DiscardNewPer`, `AllowRollup`, `ConsumerLimits`, `Mirror` as `StreamSource` (simplified to string), `Sources` as `[]*StreamSource` (simplified) | +| `StreamConfig.clone()` | stream.go:129-161 | PARTIAL | `StreamManager.NormalizeConfig()` | Deep copy via manual property assignment; similar intent | +| `StreamConsumerLimits` | stream.go:163-166 | MISSING | — | Not ported | +| `SubjectTransformConfig` | stream.go:169-172 | PARTIAL | `StreamConfig.SubjectTransformSource/Dest` | Flattened into StreamConfig instead of nested object | +| `RePublish` | stream.go:175-179 | PARTIAL | `StreamConfig.RePublishSource/Dest/HeadersOnly` | Flattened into StreamConfig instead of nested object | +| `PersistModeType` (enum) | stream.go:182-202 | PORTED | `Models/StreamConfig.cs` `PersistMode` | Sync=0, Async=1 | +| `PersistModeType.MarshalJSON/UnmarshalJSON` | stream.go:215-236 | NOT_APPLICABLE | — | System.Text.Json handles it | +| `JSPubAckResponse` | stream.go:239-242 | PARTIAL | `Publish/PubAck.cs` | Different structure; no wrapping ApiError | +| `PubAck` | stream.go:255-263 | PARTIAL | `Publish/PubAck.cs` | Has Stream/Seq; missing Domain, Duplicate, Value, BatchId, BatchSize | +| `CounterValue` | stream.go:267-269 | MISSING | — | Not ported | +| `CounterSources` | stream.go:273 | MISSING | — | Not ported | +| `StreamInfo` | stream.go:276-287 | PARTIAL | `Api/JetStreamApiResponse.cs` `JetStreamStreamInfo` | Has Config+State; missing Created, Domain, Cluster, Mirror, Sources, Alternates, TimeStamp | +| `ClusterInfo` | stream.go:304-312 | MISSING | — | Not ported as standalone API type | +| `PeerInfo` | stream.go:316-325 | MISSING | — | Not ported as standalone API type | +| `StreamSourceInfo` | stream.go:328-336 | PARTIAL | `MirrorSource/MirrorCoordinator.cs` `MirrorInfoResponse` | Simplified; missing External, SubjectTransforms | +| `StreamSource` (struct) | stream.go:339-349 | PARTIAL | `Models/StreamConfig.cs` `StreamSourceConfig` | Has Name, FilterSubject, SourceAccount; missing OptStartSeq, OptStartTime, SubjectTransforms, External, iname | +| `ExternalStream` | stream.go:352-355 | MISSING | — | Not ported | +| `ExternalStream.Domain()` | stream.go:358-363 | MISSING | — | Not ported | +| stream ingest constants | stream.go:366-384 | MISSING | — | streamDefaultMaxQueueMsgs/Bytes, batch inflight constants not ported | +| `stream` (struct, ~110 fields) | stream.go:388-497 | PARTIAL | `StreamManager` + `StreamHandle` | StreamHandle is simplified record; stream struct's ~110 fields reduced to Config+Store; no mu/client/sysc/msgs/gets/ackq/lseq/ddmap/mirror/sources/node/etc | +| `inflightSubjectRunningTotal` | stream.go:500-503 | MISSING | — | Not ported | +| `msgCounterRunningTotal` | stream.go:508-511 | MISSING | — | Not ported | +| `sourceInfo` (struct) | stream.go:514-533 | PARTIAL | `MirrorSource/SourceCoordinator.cs` | Simplified source tracking | +| JS header constants (JSMsgId, JSExpectedStream, etc.) | stream.go:543-598 | PARTIAL | `Publish/PublishOptions.cs` | Some headers used in publish path; no centralized header constant file | +| KV operation headers | stream.go:571-573 | MISSING | — | Not ported | +| Scheduler headers | stream.go:577-580 | MISSING | — | Not ported | +| Republish response headers | stream.go:584-591 | MISSING | — | Not ported | +| Rollup constants | stream.go:595-597 | MISSING | — | Not ported | +| Delete marker reason constants | stream.go:601-604 | MISSING | — | Not ported | +| `ddentry` (struct) | stream.go:612-616 | PARTIAL | `Publish/PublishPreconditions.cs` `DedupeEntry` | Has Sequence+Timestamp; id handled as dictionary key | +| `StreamMaxReplicas` | stream.go:619 | MISSING | — | Not ported as constant | +| `addStream()` / `addStreamWithAssignment()` | stream.go:621-980 | PARTIAL | `StreamManager.CreateOrUpdate()` | High-level create/update logic present; missing: account limits checking, inflight WaitGroup, file store config auto-tuning, cluster mode handling, internal client setup, pubAck template, dedupe rebuild, advisory sending | +| `StreamSource.composeIName()` | stream.go:986-1022 | MISSING | — | Source indexing not ported | +| `StreamSource.setIndexName()` | stream.go:1026-1027 | MISSING | — | Same | +| `stream.streamAssignment()` | stream.go:1030-1033 | MISSING | — | Not ported (cluster-specific) | +| `stream.setStreamAssignment()` | stream.go:1036-1078 | MISSING | — | Not ported (cluster-specific) | +| `stream.monitorQuitC()` | stream.go:1081-1091 | MISSING | — | Not ported | +| `stream.signalMonitorQuit()` | stream.go:1095-1102 | MISSING | — | Not ported | +| `stream.updateC()` | stream.go:1105-1111 | MISSING | — | Not ported | +| `stream.IsLeader()` / `isLeader()` | stream.go:1115-1126 | PARTIAL | `JetStreamMetaGroup` has leader concept | Not on stream level | +| `stream.setLeader()` | stream.go:1142-1185 | MISSING | — | No stream-level leader management | +| `stream.startClusterSubs()` / `stopClusterSubs()` | stream.go:1189-1200 | MISSING | — | Not ported | +| `stream.account()` | stream.go:1203-1219 | MISSING | — | No per-stream account binding | +| `stream.maxMsgSize()` | stream.go:1223-1253 | PARTIAL | `StreamApiHandlers` has MaxMsgSize validation | Not a stream method | +| `stream.autoTuneFileStorageBlockSize()` | stream.go:1259-1288 | MISSING | — | No block size auto-tuning | +| `stream.rebuildDedupe()` | stream.go:1296-1326 | PARTIAL | `PublishPreconditions` | Basic dedupe exists; no rebuild-from-store on recovery | +| `stream.lastSeqAndCLFS()` | stream.go:1330-1331 | MISSING | — | Cluster failure sequence tracking not ported | +| `stream.getCLFS()` / `setCLFS()` | stream.go:1334-1346 | MISSING | — | Same | +| `stream.lastSeq()` / `setLastSeq()` | stream.go:1349-1358 | MISSING | — | StreamHandle doesn't track lseq | +| `stream.sendCreateAdvisory()` | stream.go:1361-1390 | MISSING | — | Advisory publishing not ported at stream level | +| `stream.sendDeleteAdvisoryLocked()` | stream.go:1393-1413 | MISSING | — | Same | +| `stream.sendUpdateAdvisoryLocked()` | stream.go:1416-1436 | MISSING | — | Same | +| `stream.sendStreamBatchAbandonedAdvisory()` | stream.go:1439-1464 | MISSING | — | Same | +| `stream.createdTime()` / `setCreatedTime()` | stream.go:1468-1479 | MISSING | — | Not tracked on StreamHandle | +| `jsa.subjectsOverlap()` | stream.go:1485-1498 | PORTED | `StreamManager.ValidateConfigUpdate()` | Subject overlap check exists in ValidateConfigUpdate | +| `checkStreamCfg()` | stream.go:1500-2084 | PARTIAL | `JetStreamConfigValidator.Validate()` + `StreamManager.CreateOrUpdate()` | Basic name/subject/retention validation; many checks missing: MaxBytes limits, per-account limits, subject transform validation, republish cycle detection done separately, pedantic mode, placement validation | +| `jsa.configUpdateCheck()` | stream.go:2104-2236 | PARTIAL | `StreamManager.ValidateConfigUpdate()` | Immutable fields checked: storage, retention, mirror, sources, maxConsumers, replicas; missing: sealed/denyDelete/denyPurge unset checks, TTL status, counter setting, schedules, persist mode, limit adjustment, compression | +| `stream.update()` / `updateWithAdvisory()` | stream.go:2240-2554 | PARTIAL | `StreamManager.CreateOrUpdate()` | Config update path exists but much simplified; missing: subscription management, dedupe timer reset, source consumer management, direct/mirror-direct subscription toggling, republish transform update, subject transform update, tier usage tracking, advisory sending | +| `stream.purge()` / `purgeLocked()` | stream.go:2567-2642 | PARTIAL | `StreamManager.Purge()` + `PurgeEx()` | Basic purge and extended purge present; missing: sealed check, consumer purge propagation, preAck clearing | +| `stream.deleteMsg()` / `removeMsg()` | stream.go:2645-2663 | PARTIAL | `StreamManager.DeleteMessage()` | Basic removal present; missing: preAck clearing | +| `stream.eraseMsg()` | stream.go:2666-2678 | PARTIAL | `IStreamStore.EraseMsg` on interface | Interface defined but no erase implementation that overwrites with random data | +| `stream.isMirror()` | stream.go:2681-2685 | PORTED | `StreamManager.GetMirrorInfo()` checks mirror | Inline check | +| `stream.sourcesInfo()` | stream.go:2688-2695 | PARTIAL | `StreamManager.GetSourceInfos()` | Simplified | +| `stream.sourceInfo()` | stream.go:2699-2735 | PARTIAL | `SourceCoordinator.GetSourceInfo()` | Simplified | +| `stream.mirrorInfo()` | stream.go:2739-2743 | PARTIAL | `StreamManager.GetMirrorInfo()` | Simplified | +| `stream.retryDisconnectedSyncConsumers()` | stream.go:2747-2778 | MISSING | — | Not ported | +| source health constants | stream.go:2782-2785 | MISSING | — | Not ported | +| `stream.processMirrorMsgs()` | stream.go:2788-2854 | PARTIAL | `MirrorCoordinator` | Simplified mirror message processing; no health check, stall detection, flow control | +| `sourceInfo.isCurrentSub()` | stream.go:2859-2860 | MISSING | — | Not ported | +| `stream.processInboundMirrorMsg()` | stream.go:2863-3014 | PARTIAL | `MirrorCoordinator.OnOriginAppendAsync()` | Greatly simplified; no sequence tracking, flow control, heartbeat handling, skip logic | +| `stream.setMirrorErr()` | stream.go:3017-3022 | MISSING | — | Not ported | +| `stream.cancelMirrorConsumer()` | stream.go:3027-3032 | MISSING | — | Not ported | +| `stream.retryMirrorConsumer()` | stream.go:3038-3044 | MISSING | — | Not ported | +| `stream.skipMsgs()` | stream.go:3048-3073 | MISSING | — | No skip with NRG proposal | +| `calculateRetryBackoff()` | stream.go:3084-3089 | MISSING | — | Not ported | +| `stream.scheduleSetupMirrorConsumerRetry()` | stream.go:3098-3118 | MISSING | — | Not ported | +| `stream.setupMirrorConsumer()` | stream.go:3125-3416 | MISSING | — | Full mirror consumer setup with API-based consumer creation, subject transforms, delivery tracking not ported | +| `stream.streamSource()` | stream.go:3419-3424 | MISSING | — | Not ported | +| `stream.retrySourceConsumerAtSeq()` | stream.go:3429-3435 | MISSING | — | Not ported | +| `stream.cancelSourceConsumer()` | stream.go:3439-3443 | MISSING | — | Not ported | +| `stream.cancelSourceInfo()` | stream.go:3451-3470 | MISSING | — | Not ported | +| `stream.setupSourceConsumer()` | stream.go:3478-3500+ | MISSING | — | Full source consumer setup not ported (continues past line 4000) | + +| Status | Count | +| PORTED | 55 | +| PARTIAL | 30 | +| MISSING | 75 | +| NOT_APPLICABLE | 12 | +| DEFERRED | 0 | +| **Total** | **172** | + + + +| `(si *sourceInfo) genSourceHeader` | stream.go:4010 | MISSING | — | Generates 2.10-style source header (stream name, seq, filter, transform). No .NET equivalent; SourceCoordinator tracks seq differently. | +| `streamAndSeqFromAckReply` | stream.go:4041 | MISSING | — | Parses stream/seq from `$JS.ACK` reply tokens. No wire-level reply parsing in .NET. | +| `streamAndSeq` | stream.go:4058 | MISSING | — | Extracts stream name, index name, sequence from source header string. No .NET equivalent. | +| `(mset *stream) setStartingSequenceForSources` | stream.go:4079 | PARTIAL | `SourceCoordinator.LastOriginSequence` | Go scans stored messages backwards to find starting seq per source using gsl.SimpleSublist. .NET SourceCoordinator has simpler resume-from-last logic. | +| `(mset *stream) resetSourceInfo` | stream.go:4172 | PARTIAL | `StreamManager.RebuildReplicationCoordinators` | Go resets all sourceInfo structs with transforms. .NET rebuilds coordinators but lacks per-source subject transforms array. | +| `(mset *stream) startingSequenceForSources` | stream.go:4207 | PARTIAL | `SourceCoordinator.LastOriginSequence` | Go does reverse scan with SimpleSublist filtering. .NET relies on simpler incremental tracking. | + +| `(mset *stream) setupSourceConsumers` | stream.go:4318 | PARTIAL | `SourceCoordinator.StartSyncLoop / StartPullSyncLoop` | Go creates internal consumers per source with proper starting sequences. .NET has background sync loops but no internal consumer creation. | +| `(mset *stream) subscribeToStream` | stream.go:4348 | MISSING | — | Creates internal subscriptions for all stream subjects + mirror/source setup. .NET uses StreamManager.Capture instead of subscription-based ingest. | +| `(mset *stream) subscribeToDirect` | stream.go:4406 | PARTIAL | `DirectApiHandlers.HandleGet` | Go creates queue subscriptions for `$JS.API.DIRECT.GET.{stream}`. .NET has API handler but no queue-subscription wiring. | +| `(mset *stream) unsubscribeToDirect` | stream.go:4431 | MISSING | — | Unsubscribes direct get subscriptions. No .NET equivalent. | +| `(mset *stream) subscribeToMirrorDirect` | stream.go:4443 | MISSING | — | Sets up direct-get queue subs for mirror stream name. Not implemented in .NET. | +| `(mset *stream) unsubscribeToMirrorDirect` | stream.go:4474 | MISSING | — | Clears mirror direct-get subscriptions. Not implemented. | +| `(mset *stream) stopSourceConsumers` | stream.go:4487 | PORTED | `SourceCoordinator.StopAsync` | .NET coordinator has async stop with cancellation. | +| `(mset *stream) removeInternalConsumer` | stream.go:4494 | MISSING | — | Clears consumer name on sourceInfo. .NET doesn't track internal consumer names. | +| `(mset *stream) unsubscribeToStream` | stream.go:4503 | MISSING | — | Unsubscribes from all stream subjects, mirrors, sources. .NET uses StreamManager.Delete instead. | +| `(mset *stream) deleteInflightBatches` | stream.go:4535 | PARTIAL | `JetStreamPublisher.ClearBatches` | Go cleans up batch state with lock ordering. .NET has simplified ClearBatches on AtomicBatchPublishEngine. | +| `(mset *stream) deleteBatchApplyState` | stream.go:4552 | MISSING | — | Cleans up raft batch-apply entries with pool return. No .NET equivalent (no RAFT batch apply). | + +| `(mset *stream) subscribeInternal` | stream.go:4563 | MISSING | — | Creates internal subscription with auto-incremented SID. .NET server doesn't use internal subscriptions for JetStream. | +| `(mset *stream) queueSubscribeInternal` | stream.go:4577 | MISSING | — | Creates internal queue subscription. Not implemented in .NET. | +| `(mset *stream) unsubscribeInternal` | stream.go:4594 | MISSING | — | Removes internal subscription by subject lookup. Not implemented. | +| `(mset *stream) unsubscribe` | stream.go:4616 | MISSING | — | Unsubscribes by subscription pointer. Not implemented. | + +| `(mset *stream) setupStore` | stream.go:4623 | PARTIAL | `StreamManager.CreateStore` | Go wires up MemStore/FileStore with encryption, callbacks for storage updates, remove msg, and process inbound. .NET creates store but lacks callback wiring. | +| `(mset *stream) storeUpdates` | stream.go:4682 | MISSING | — | Callback for store changes: updates consumer pending counts, JetStream account usage. No .NET equivalent. | + +| `(mset *stream) numMsgIds` | stream.go:4714 | MISSING | — | Returns count of tracked dedup entries. Not exposed in .NET. | +| `(mset *stream) checkMsgId` | stream.go:4721 | PORTED | `PublishPreconditions.IsDuplicate` | .NET uses ConcurrentDictionary-based dedup check. | +| `(mset *stream) purgeMsgIds` | stream.go:4731 | PORTED | `PublishPreconditions.TrimOlderThan` | Go uses timer-driven purge with GC compaction. .NET does cutoff-based trim. Lacks timer scheduling. | +| `(mset *stream) storeMsgId` | stream.go:4778 | PORTED | `PublishPreconditions.Record` | .NET records msgId with timestamp. | +| `(mset *stream) storeMsgIdLocked` | stream.go:4786 | PORTED | `PublishPreconditions.Record` | Internal locked variant; .NET uses ConcurrentDictionary. | + +| `getMsgId` | stream.go:4803 | MISSING | — | Fast lookup of `Nats-Msg-Id` header from raw bytes. .NET uses PublishOptions.MsgId instead. | +| `getExpectedLastMsgId` | stream.go:4808 | MISSING | — | Fast lookup of `Nats-Expected-Last-Msg-Id`. .NET checks via PublishOptions.ExpectedLastMsgId. | +| `getExpectedStream` | stream.go:4813 | MISSING | — | Fast lookup of `Nats-Expected-Stream`. Not implemented as wire parser. | +| `getExpectedLastSeq` | stream.go:4818 | MISSING | — | Fast lookup of `Nats-Expected-Last-Sequence`. .NET uses PublishOptions.ExpectedLastSeq. | +| `getRollup` | stream.go:4827 | MISSING | — | Fast lookup of `Nats-Rollup` header. No rollup support in .NET. | +| `getExpectedLastSeqPerSubject` | stream.go:4836 | MISSING | — | Fast lookup of `Nats-Expected-Last-Subject-Sequence`. .NET uses PublishOptions but no wire parser. | +| `getExpectedLastSeqPerSubjectForSubject` | stream.go:4845 | MISSING | — | Subject override for per-subject seq check. Not in .NET. | +| `getMessageTTL` | stream.go:4852 | MISSING | — | Parses `Nats-Msg-TTL` header from raw bytes. Not implemented in .NET. | +| `parseMessageTTL` | stream.go:4863 | MISSING | — | Parses TTL string ("never", duration, seconds). Not implemented. | +| `getMessageIncr` | stream.go:4888 | MISSING | — | Parses `Nats-Msg-Incr` header for counter streams. Not implemented. | +| `getMessageSchedule` | stream.go:4898 | MISSING | — | Parses `Nats-Msg-Schedule` header. Not implemented. | +| `nextMessageSchedule` | stream.go:4907 | MISSING | — | Calculates next schedule from header + timestamp. Not implemented. | +| `getMessageScheduleTTL` | stream.go:4917 | MISSING | — | Parses schedule TTL header. Not implemented. | +| `getMessageScheduleTarget` | stream.go:4930 | MISSING | — | Parses schedule target header. Not implemented. | +| `getMessageScheduleSource` | stream.go:4938 | MISSING | — | Parses schedule source header. Not implemented. | +| `getMessageScheduler` | stream.go:4946 | MISSING | — | Parses scheduler header. Not implemented. | +| `getBatchId` | stream.go:4954 | MISSING | — | Parses `Nats-Batch-Id` header from raw bytes. .NET uses PublishOptions.BatchId instead. | +| `getBatchSequence` | stream.go:4962 | MISSING | — | Parses `Nats-Batch-Seq` header. .NET uses PublishOptions.BatchSeq. | + +| `(mset *stream) IsClustered` | stream.go:4971 | NOT_APPLICABLE | — | Checks if RAFT node is present. .NET uses JetStreamMetaGroup for cluster simulation but not per-stream RAFT. | +| `(mset *stream) isClustered` | stream.go:4978 | NOT_APPLICABLE | — | Lock-held variant. | + +| `inMsg` struct | stream.go:4983 | MISSING | — | Internal message queue struct (subj, reply, hdr, msg, sourceInfo, msgTrace). .NET doesn't have internal msg queue. | +| `inMsgPool` | stream.go:4994 | MISSING | — | sync.Pool for inMsg objects. Not needed without internal queue. | +| `(im *inMsg) returnToPool` | stream.go:4999 | MISSING | — | Returns inMsg to pool. | +| `(mset *stream) queueInbound` | stream.go:5004 | MISSING | — | Enqueues inbound message with rate limit error on overflow. .NET uses direct method calls instead. | + +| `dgPool` / `directGetReq` | stream.go:5019 | MISSING | — | Pool and struct for deferred direct get requests. | +| `(mset *stream) processDirectGetRequest` | stream.go:5033 | PARTIAL | `DirectApiHandlers.HandleGet` | Go handles full JSApiMsgGetRequest with validation. .NET only supports seq-based lookup. Missing: LastFor, NextFor, MultiLastFor, StartTime, Batch, MaxBytes. | +| `(mset *stream) processDirectGetLastBySubjectRequest` | stream.go:5087 | MISSING | — | Direct get by subject appended to request subject. Not implemented in .NET. | +| `(mset *stream) getDirectMulti` | stream.go:5158 | MISSING | — | Multi-subject direct get with batch response + EOB. Not implemented. | +| `(mset *stream) getDirectRequest` | stream.go:5270 | PARTIAL | `DirectApiHandlers.HandleGet` | Go handles seq, NextFor, LastFor, batch, MaxBytes, NoHeaders. .NET only handles seq lookup. | +| Direct get header constants (`dg`, `dgb`, `eob`, `eobm`) | stream.go:5150 | MISSING | — | Wire format header templates for direct get responses. | + +| `(mset *stream) processInboundJetStreamMsg` | stream.go:5409 | MISSING | — | Entry point: copies msg, adds trace, queues inbound. .NET uses StreamManager.Capture. | +| Error sentinel variables | stream.go:5428 | PARTIAL | — | `errLastSeqMismatch`, `errMsgIdDuplicate`, etc. .NET uses error codes in PubAck instead. | +| `(mset *stream) processJetStreamMsg` | stream.go:5437 | PARTIAL | `JetStreamPublisher.TryCaptureWithOptions + StreamManager.Capture` | Core message processing. Go: full consistency checks, subject transform, sealed/sealed check, header extraction, dedup, TTL, counter increment, schedule validation, rollup, interest retention skip, store. .NET: basic dedup + expected-last-seq + capture. Missing: rollup, TTL, counters, schedules, per-subject seq, interest retention skip, sealed check at msg level, clustered RAFT propose. | + +| `jsPubMsg` struct | stream.go:6770 | MISSING | — | Internal pub message with dest subject, reply, store msg, consumer ref. | +| `newJSPubMsg` | stream.go:6786 | MISSING | — | Constructor with pool allocation. | +| `jsPubMsgPool` | stream.go:6784 | MISSING | — | sync.Pool for jsPubMsg. | +| `(pm *jsPubMsg) returnToPool` | stream.go:6815 | MISSING | — | Pool return. | +| `(pm *jsPubMsg) size` | stream.go:6826 | MISSING | — | Size calculation for flow control. | +| `jsOutQ` struct | stream.go:6834 | MISSING | — | Typed output queue wrapper around ipQueue. | +| `(q *jsOutQ) sendMsg` | stream.go:6838 | MISSING | — | Sends message on output queue. | +| `(q *jsOutQ) send` | stream.go:6844 | MISSING | — | Pushes jsPubMsg to queue. | +| `StoredMsg` struct | stream.go:6858 | PORTED | `StoredMessage` | .NET StoredMessage record has Subject, Sequence, Payload, TimestampUtc. | +| `(mset *stream) setupSendCapabilities` | stream.go:6869 | MISSING | — | Creates output queue and launches internalLoop goroutine. | +| `(mset *stream) accName` | stream.go:6881 | NOT_APPLICABLE | — | Returns account name. .NET uses Account directly. | +| `(mset *stream) name / nameLocked` | stream.go:6892 | NOT_APPLICABLE | — | Returns stream name. .NET uses StreamHandle.Config.Name. | +| `(mset *stream) internalLoop` | stream.go:6907 | MISSING | — | Main event loop: outq dispatch, inbound msg processing, direct gets, ack queue for interest retention. Complex select{} loop. Not implemented in .NET. | + +| `(mset *stream) resetAndWaitOnConsumers` | stream.go:7037 | MISSING | — | Steps down and waits for consumer monitor goroutines. | +| `(mset *stream) delete` | stream.go:7059 | PARTIAL | `StreamManager.Delete` | Go calls stop(true, true) with advisory. .NET removes from ConcurrentDictionary. | +| `(mset *stream) stop` | stream.go:7067 | PARTIAL | `StreamManager.Delete` | Go: full shutdown sequence (mark closed, signal monitor, unsubscribe, cleanup consumers, advisories, RAFT node stop/delete, dedup timer cleanup, queue unregister, store delete/stop). .NET: simple dictionary removal. | + +| `(mset *stream) getMsg` | stream.go:7249 | PORTED | `StreamManager.GetMessage` | .NET loads from store by sequence. | +| `(mset *stream) getConsumers` | stream.go:7266 | PARTIAL | `ConsumerManager` | Go returns copy of consumer list. .NET has ConsumerManager but not per-stream consumer list. | +| `(mset *stream) numPublicConsumers` | stream.go:7273 | MISSING | — | Count of non-DIRECT consumers. Not in .NET. | +| `(mset *stream) getPublicConsumers` | stream.go:7278 | MISSING | — | Filters out DIRECT consumers. Not in .NET. | +| `(mset *stream) getDirectConsumers` | stream.go:7292 | MISSING | — | Returns only DIRECT consumers. Not in .NET. | +| `(mset *stream) numDirectConsumers` | stream.go:7471 | MISSING | — | Counts DIRECT consumers. Not in .NET. | +| `(mset *stream) state` | stream.go:7487 | PORTED | `StreamManager.GetStateAsync` | .NET delegates to store.GetStateAsync. | +| `(mset *stream) stateWithDetail` | stream.go:7490 | PARTIAL | `StreamManager.GetStateAsync` | Go has fast path (FastState) vs detailed (State). .NET always returns full state. | +| `(mset *stream) Store` | stream.go:7509 | PORTED | `StreamHandle.Store` | Direct property access in .NET record. | + +| `(mset *stream) setConsumer` | stream.go:7367 | MISSING | — | Registers consumer on stream with filter tracking and consumer sublist (gsl). .NET uses separate ConsumerManager. | +| `(mset *stream) removeConsumer` | stream.go:7388 | MISSING | — | Removes consumer from stream's maps and sublists. .NET uses ConsumerManager. | +| `(mset *stream) swapSigSubs` | stream.go:7416 | MISSING | — | Updates signal subscriptions when consumer filter changes. Complex lock ordering. Not in .NET. | +| `(mset *stream) lookupConsumer` | stream.go:7464 | PARTIAL | `ConsumerManager` | Go looks up by name on stream. .NET uses separate manager. | +| `(mset *stream) partitionUnique` | stream.go:7516 | MISSING | — | Checks consumer filter partitions don't overlap. Not implemented. | +| `(mset *stream) potentialFilteredConsumers` | stream.go:7541 | MISSING | — | Checks if filtered consumer matching is needed. Not in .NET. | + +| `checkInterestStateT/J` constants | stream.go:7307 | MISSING | — | Timer interval/jitter for interest state checks. | +| `(mset *stream) checkInterestState` | stream.go:7320 | PARTIAL | `InterestRetentionPolicy.ShouldRetain` | Go checks ack floors across all consumers and compacts. .NET has simpler per-seq interest check. | +| `(mset *stream) isInterestRetention` | stream.go:7353 | PORTED | Config check | .NET checks `stream.Config.Retention == RetentionPolicy.Interest`. | +| `(mset *stream) numConsumers` | stream.go:7360 | PARTIAL | `ConsumerManager` | Go returns from stream's consumer map. .NET has ConsumerManager. | +| `(mset *stream) noInterest` | stream.go:7556 | PARTIAL | `InterestRetentionPolicy.ShouldRetain` | .NET has simplified interest check without pre-ack support. | +| `(mset *stream) noInterestWithSubject` | stream.go:7562 | PARTIAL | `InterestRetentionPolicy.ShouldRetain` | .NET uses SubjectMatch filtering. | +| `(mset *stream) checkForInterest` | stream.go:7567 | PARTIAL | `InterestRetentionPolicy.ShouldRetain` | Go loads message for subject, checks filtered consumers. .NET simpler. | +| `(mset *stream) checkForInterestWithSubject` | stream.go:7588 | PARTIAL | `InterestRetentionPolicy.ShouldRetain` | Go iterates consumers with needAck check. .NET iterates registered interests. | +| `(mset *stream) hasPreAck` | stream.go:7605 | MISSING | — | Pre-ack tracking for messages acked before stored. Not in .NET. | +| `(mset *stream) hasAllPreAcks` | stream.go:7619 | MISSING | — | Checks if all consumers pre-acked a sequence. Not in .NET. | +| `(mset *stream) clearAllPreAcks` | stream.go:7631 | MISSING | — | Clears pre-ack map for a sequence. Not in .NET. | +| `(mset *stream) clearAllPreAcksBelowFloor` | stream.go:7636 | MISSING | — | Clears pre-acks below a floor sequence. Not in .NET. | +| `(mset *stream) registerPreAckLock` | stream.go:7645 | MISSING | — | Lock-acquiring pre-ack registration. Not in .NET. | +| `(mset *stream) registerPreAck` | stream.go:7654 | MISSING | — | Registers pre-ack for consumer/sequence. Not in .NET. | +| `(mset *stream) clearPreAck` | stream.go:7669 | MISSING | — | Clears single consumer pre-ack. Not in .NET. | +| `(mset *stream) ackMsg` | stream.go:7685 | PARTIAL | `InterestRetentionPolicy + AckProcessor` | Go: full ack processing with pre-ack, clustered proposal, interest-raise-first. .NET has basic interest check + removal. | + +| `(mset *stream) snapshot` | stream.go:7764 | PORTED | `StreamSnapshotService.CreateTarSnapshotAsync` | .NET uses TAR+S2 snapshot service. | +| `(a *Account) RestoreStream` | stream.go:7776 | PARTIAL | `StreamSnapshotService.RestoreTarSnapshotAsync` | Go: full restore with config validation, limit checks, directory management, consumer restore. .NET: simplified restore (purge + replay). Missing: config validation, consumer restore, limit checks. | + +| `(mset *stream) checkForOrphanMsgs` | stream.go:7979 | MISSING | — | Checks for dangling messages on interest retention streams at startup. Not implemented. | + +| `(mset *stream) checkConsumerReplication` | stream.go:8007 | MISSING | — | Verifies consumer replication matches stream for interest retention. Not in .NET. | +| `(mset *stream) checkInMonitor` | stream.go:8029 | NOT_APPLICABLE | — | Atomic flag for monitor goroutine. .NET has no stream monitor loop. | +| `(mset *stream) clearMonitorRunning` | stream.go:8041 | NOT_APPLICABLE | — | Clears monitor flag + cleanup. | +| `(mset *stream) isMonitorRunning` | stream.go:8051 | NOT_APPLICABLE | — | Checks monitor flag. | +| `(mset *stream) trackReplicationTraffic` | stream.go:8058 | NOT_APPLICABLE | — | Adjusts account stats for replication. .NET has no account-level replication stats. | + +| Status | Count | +| PORTED | 11 | +| PARTIAL | 24 | +| MISSING | 73 | +| NOT_APPLICABLE | 8 | +| DEFERRED | 0 | +| **Total** | **116** | + + + + +| `ConsumerInfo` | consumer.go:55 | PARTIAL | `JetStreamConsumerInfo` (Api/JetStreamApiResponse.cs) | .NET has a simplified version inside API response; missing most fields (Delivered, AckFloor, NumAckPending, NumRedelivered, NumWaiting, NumPending, Cluster, PushBound, Paused, PauseRemaining, TimeStamp, PriorityGroups) | +| `consumerInfoClusterResponse` | consumer.go:77 | MISSING | — | Cluster-internal response type for consumer info propagation | +| `PriorityGroupState` | consumer.go:82 | MISSING | — | Reported in ConsumerInfo; no equivalent struct | +| `ConsumerConfig` | consumer.go:88 | PORTED | `ConsumerConfig` (Models/ConsumerConfig.cs) | All major fields present. Minor: missing `Description`, `SampleFrequency`, `Replicas`, `MemoryStorage`, `Direct`, `InactiveThreshold`, `HeadersOnly`, `Name` (vs Durable), `DeliverGroup` | +| `SequenceInfo` | consumer.go:143 | MISSING | — | Used in ConsumerInfo; no standalone equivalent | +| `CreateConsumerRequest` | consumer.go:149 | PARTIAL | Parsed inline in `ConsumerApiHandlers.ParseConfig` | Missing `Action` and `Pedantic` fields | +| `ConsumerAction` enum | consumer.go:156 | MISSING | — | ActionCreateOrUpdate/ActionUpdate/ActionCreate not modeled | +| `ConsumerAction.MarshalJSON/UnmarshalJSON` | consumer.go:188-213 | MISSING | — | JSON serialization for consumer actions | +| `ConsumerNakOptions` | consumer.go:216 | PARTIAL | Parsed inline in `AckProcessor.ProcessAck` | Delay extraction exists but not as a typed struct | +| `PriorityPolicy` enum | consumer.go:221 | PORTED | `PriorityPolicy` (Models/ConsumerConfig.cs:80) | Has None, Overflow, PinnedClient. Missing `Prioritized` variant | +| `PriorityPolicy.MarshalJSON/UnmarshalJSON` | consumer.go:261-290 | NOT_APPLICABLE | — | .NET uses enum serialization | +| `DeliverPolicy` enum | consumer.go:293 | PORTED | `DeliverPolicy` (Models/JetStreamPolicies.cs:16) | All 6 variants present | +| `AckPolicy` enum | consumer.go:330 | PORTED | `AckPolicy` (Models/ConsumerConfig.cs:69) | All 3 variants present | +| `ReplayPolicy` enum | consumer.go:354 | PORTED | `ReplayPolicy` (Models/JetStreamPolicies.cs:27) | Both variants present | + +| `AckAck/AckOK/AckNak/AckProgress/AckNext/AckTerm` | consumer.go:376-388 | PORTED | `AckType` enum + `AckProcessor.ParseAckType` (Consumers/AckProcessor.cs:127) | All ack types recognized; AckNext (+NXT) handled implicitly | +| `ackTermLimitsReason/ackTermUnackedLimitsReason` | consumer.go:391-394 | MISSING | — | Reason strings for limit-based terminations | +| `JSPullRequestPendingMsgs/Bytes/NatsPinId` | consumer.go:43-46 | PARTIAL | Used in heartbeat headers in PushConsumerEngine | PinId header not used | +| `JsPullRequestRemainingBytesT` | consumer.go:53 | MISSING | — | Template for 409 Batch Completed response | +| `validGroupName` regex | consumer.go:49 | MISSING | — | Group name validation regex `^[a-zA-Z0-9/_=-]{1,16}$` | + +| `consumer` struct | consumer.go:409 | PARTIAL | `ConsumerHandle` (ConsumerManager.cs:331) | ConsumerHandle is a simplified record with NextSequence, Paused, PauseUntilUtc, Pending, PushFrames, AckProcessor. Missing: most of the ~80 fields (leader, client, sysc, sid, sseq, dseq, adflr, asflr, subjf, filters, dsubj, qgroup, lss, rlimit, ackSub, reqSub, etc.) | +| `subjectFilter` struct | consumer.go:529 | PORTED | `CompiledFilter` (Consumers/PullConsumerEngine.cs:15) | Different approach but equivalent functionality | +| `subjectFilters` type | consumer.go:535 | PORTED | `CompiledFilter.FromConfig` | Encapsulated in CompiledFilter | +| `proposal` struct | consumer.go:547 | MISSING | — | RAFT proposal linked list for clustered consumers | + +| `JsAckWaitDefault` (30s) | consumer.go:554 | PORTED | `ConsumerConfig.AckWaitMs = 30_000` | Default set in ConsumerConfig | +| `JsDeleteWaitTimeDefault` (5s) | consumer.go:557 | PARTIAL | `DeliveryInterestTracker(TimeSpan.FromSeconds(30))` | Different default (30s vs 5s) | +| `JsFlowControlMaxPending` (32MB) | consumer.go:559 | PARTIAL | `MaxFlowControlPending = 2` (frames, not bytes) | Different unit/approach | +| `JsDefaultMaxAckPending` (1000) | consumer.go:561 | MISSING | — | Not applied as default in ConsumerConfig | +| `JsDefaultPinnedTTL` (2min) | consumer.go:564 | MISSING | — | Not applied as default | + +| `setConsumerConfigDefaults` | consumer.go:568 | MISSING | — | 110-line function setting MaxDeliver=-1, MaxWaiting defaults, AckWait defaults, BackOff override, MaxAckPending defaults, MaxRequestBatch limits, PinnedTTL default. No .NET equivalent | +| `checkConsumerCfg` | consumer.go:680 | MISSING | — | 270-line comprehensive validation function. No .NET equivalent (JetStreamConfigValidator only validates streams) | +| `ConsumerConfig.replicas` | consumer.go:397 | MISSING | — | Replicas calculation against parent stream | +| `deliveryFormsCycle` | referenced at consumer.go:743 | MISSING | — | Cycle detection for push delivery subjects | + +| `stream.addConsumer` | consumer.go:956 | PARTIAL | `ConsumerManager.CreateOrUpdate` | Simplified: no WorkQueue checks, no replica validation, no RAFT, no event subjects | +| `stream.addConsumerWithAction` | consumer.go:952 | PARTIAL | `ConsumerManager.CreateOrUpdate` | Action parameter not used | +| `stream.addConsumerWithAssignment` | consumer.go:960 | PARTIAL | `ConsumerManager.CreateOrUpdate` | 370-line function. .NET has ~30 lines. Missing: durable update detection, WorkQueue partition uniqueness, consumer limit checks, store creation, ack subscription setup, subject filter compilation, stored state restoration, cluster assignment, rate limit setup, advisory sending | +| `consumer.updateInactiveThreshold` | consumer.go:1335 | MISSING | — | Jitter calculation for ephemeral delete timers | +| `consumer.updatePauseState` | consumer.go:1358 | PARTIAL | `ConsumerManager.Pause(stream, durable, DateTime)` | Timer-based resume exists but simpler implementation | + +| `consumer.consumerAssignment` | consumer.go:1383 | MISSING | — | No RAFT consumer assignment tracking | +| `consumer.setConsumerAssignment` | consumer.go:1389 | MISSING | — | | +| `consumer.monitorQuitC` | consumer.go:1407 | MISSING | — | Monitor goroutine quit channel | +| `consumer.signalMonitorQuit` | consumer.go:1422 | MISSING | — | | +| `consumer.updateC` | consumer.go:1431 | MISSING | — | | +| `consumer.checkQueueInterest` | consumer.go:1439 | MISSING | — | Queue group interest checking | +| `consumer.clearNode` | consumer.go:1459 | MISSING | — | RAFT node cleanup | +| `consumer.IsLeader/isLeader` | consumer.go:1469 | MISSING | — | No leader election concept | +| `consumer.setLeader` | consumer.go:1478 | MISSING | — | 240-line leader transition handling (subscribe internal ack/req/reset subs, flow control, push mode interest, delete timer, pause state, replay mode, start goroutines). Entirely missing | + +| `consumer.subscribeInternal` | consumer.go:1723 | MISSING | — | Internal subscription for ack/request subjects | +| `consumer.unsubscribe` | consumer.go:1743 | MISSING | — | | +| `consumer.handleClusterConsumerInfoRequest` | consumer.go:1718 | MISSING | — | Cluster info request handler | + +| `consumer.sendAdvisory` | consumer.go:1752 | MISSING | — | Generic advisory sender with interest check | +| `consumer.sendDeleteAdvisoryLocked` | consumer.go:1771 | MISSING | — | Delete advisory | +| `consumer.sendPinnedAdvisoryLocked` | consumer.go:1788 | MISSING | — | Pinned consumer advisory | +| `consumer.sendUnpinnedAdvisoryLocked` | consumer.go:1807 | MISSING | — | Unpinned consumer advisory | +| `consumer.sendCreateAdvisory` | consumer.go:1827 | MISSING | — | Create advisory | +| `consumer.sendPauseAdvisoryLocked` | consumer.go:1847 | MISSING | — | Pause/unpause advisory | + +| `consumer.hasDeliveryInterest` | consumer.go:1886 | PARTIAL | `DeliveryInterestTracker` (Consumers/DeliveryInterestTracker.cs) | .NET tracks subscriber count; Go checks sublist + gateway interest | +| `Server.hasGatewayInterest` | consumer.go:1908 | MISSING | — | Gateway interest checking | +| `consumer.updateDeliveryInterest` | consumer.go:1925 | PARTIAL | `DeliveryInterestTracker.OnSubscribe/OnUnsubscribe` | Simplified; no delete timer or queue group handling | +| `consumer.deleteNotActive` | consumer.go:1977 | PARTIAL | `DeliveryInterestTracker.ShouldDelete` | Go has 160-line function with pull-mode elapsed checking, pending ack consideration, clustered delete proposal with retry. .NET is a simple boolean property | +| `consumer.watchGWinterest` | consumer.go:2141 | MISSING | — | Gateway interest polling timer | + +| `Account.checkNewConsumerConfig` | consumer.go:2274 | MISSING | — | 55-line validation of which config fields can be updated | +| `consumer.updateConfig` | consumer.go:2333 | MISSING | — | 145-line config update handler (DeliverSubject, MaxAckPending, AckWait, RateLimit, SampleFrequency, MaxDeliver, InactiveThreshold, PauseUntil, FilterSubjects). Not ported | +| `consumer.updateDeliverSubject` | consumer.go:2480 | MISSING | — | Push consumer delivery subject change | +| `consumer.updateDeliverSubjectLocked` | consumer.go:2489 | MISSING | — | | +| `configsEqualSansDelivery` | consumer.go:2506 | MISSING | — | Config comparison ignoring delivery subject | + +| `jsAckMsg` struct | consumer.go:2519 | MISSING | — | Ack message struct with pooling | +| `jsAckMsgPool` | consumer.go:2526 | MISSING | — | sync.Pool for ack messages | +| `consumer.pushAck` | consumer.go:2552 | MISSING | — | Wire handler that pushes to ackMsgs queue | +| `consumer.processAck` | consumer.go:2558 | PORTED | `AckProcessor.ProcessAck(ulong, ReadOnlySpan)` | All ack types dispatched (+ACK, -NAK, +TERM, +WPI, +NXT). Missing: ackReplyInfo parsing from subject, ack reply handling | +| `consumer.progressUpdate` | consumer.go:2604 | PORTED | `AckProcessor.ProcessProgress` | Resets deadline | +| `consumer.updateSkipped` | consumer.go:2616 | MISSING | — | RAFT proposal for skip | +| `consumer.resetStartingSeq` | consumer.go:2628 | PARTIAL | `ConsumerManager.ResetToSequence` | Simplified; missing deliver policy validation, RAFT proposal, interest stream cleanup | +| `consumer.resetLocalStartingSeq` | consumer.go:2691 | PARTIAL | `ConsumerManager.ResetToSequence` | Clears pending and resets sequence | + +| `consumer.loopAndForwardProposals` | consumer.go:2700 | MISSING | — | RAFT proposal forwarding goroutine | +| `consumer.propose` | consumer.go:2761 | MISSING | — | Add to proposal linked list | +| `consumer.updateDelivered` | consumer.go:2778 | MISSING | — | RAFT-aware delivery tracking (proposes or stores locally) | +| `consumer.addAckReply` | consumer.go:2799 | MISSING | — | Pending ack reply tracking for replicated consumers | +| `consumer.addReplicatedQueuedMsg` | consumer.go:2808 | MISSING | — | Pending delivery tracking for replicated consumers | +| `consumer.updateAcks` | consumer.go:2829 | MISSING | — | RAFT-aware ack update (proposes or stores locally) | +| `consumer.addClusterPendingRequest` | consumer.go:2854 | PARTIAL | `PullConsumerEngine.ProposeWaitingRequest` | Simplified cluster pending tracking | +| `consumer.removeClusterPendingRequest` | consumer.go:2866 | PARTIAL | `PullConsumerEngine.RemoveClusterPending` | | +| `consumer.setPendingRequestsOk` | consumer.go:2877 | MISSING | — | Version-gated pending request support | +| `consumer.checkAndSetPendingRequestsOk` | consumer.go:2890 | MISSING | — | Peer version checking | +| `consumer.checkPendingRequests` | consumer.go:2915 | MISSING | — | Leadership change pending request notification | +| `consumer.releaseAnyPendingRequests` | consumer.go:2931 | MISSING | — | Release waiting requests on delete/stop | + +| `consumer.processNak` | consumer.go:2950 | PORTED | `AckProcessor.ProcessNak` | Delay extraction, backoff, redelivery queue. Missing: NAK advisory sending, JSON delay parsing (`ConsumerNakOptions`), duration string parsing | +| `consumer.processTerm` | consumer.go:3030 | PORTED | `AckProcessor.ProcessTerm` | Removes from pending. Missing: TERM advisory, reason extraction | + +| `ackWaitDelay` constant | consumer.go:3060 | MISSING | — | 1ms delay added to ack wait timer | +| `consumer.ackWait` | consumer.go:3063 | MISSING | — | AckWait + delay calculation | +| `consumer.checkRedelivered` | consumer.go:3072 | MISSING | — | Sanity check for rdc below ack floor | +| `consumer.readStoredState` | consumer.go:3091 | MISSING | — | Restore state from store | +| `consumer.applyState` | consumer.go:3107 | MISSING | — | Apply ConsumerState to consumer fields | +| `consumer.setStoreState` | consumer.go:3135 | MISSING | — | Set state from snapshot restore | +| `consumer.writeStoreState/Unlocked` | consumer.go:3147-3172 | MISSING | — | Persist state to store | + +| `consumer.initialInfo` | consumer.go:3176 | MISSING | — | One-shot initial info | +| `consumer.clearInitialInfo` | consumer.go:3189 | MISSING | — | | +| `consumer.info` | consumer.go:3196 | PARTIAL | `ConsumerManager.GetInfo` | .NET returns only config; Go returns full ConsumerInfo with delivery/ack state, pending counts, waiting count, cluster info, priority groups | +| `consumer.infoWithSnap` | consumer.go:3200 | MISSING | — | | +| `consumer.infoWithSnapAndReply` | consumer.go:3204 | MISSING | — | 120-line info builder with store state, cluster info | + +| `consumer.signalNewMessages` | consumer.go:3330 | PORTED | `PushConsumerEngine.Signal(ConsumerSignal.NewMessage)` | Channel-based signaling | +| `consumer.shouldSample` | consumer.go:3339 | PORTED | `SampleTracker.ShouldSample` (Consumers/SampleTracker.cs) | Stochastic sampling | +| `consumer.sampleAck` | consumer.go:3352 | PARTIAL | `SampleTracker.RecordLatency` | Records sample but does not send advisory | +| `consumer.processAckMsg` | consumer.go:3382 | PARTIAL | `AckProcessor.AckSequence` / `AckProcessor.AckAll` | Core ack processing for Explicit/All/None policies. Missing: sample sending, cluster replication, retention policy notification, floor advancement algorithm differs | + +| `consumer.hasMaxDeliveries` | consumer.go:2172 | PORTED | `AckProcessor.ScheduleRedelivery` (max deliver check) | Checks and handles exceeded sequences | +| `consumer.forceExpirePending` | consumer.go:2203 | MISSING | — | Force all pending to redeliver queue | +| `consumer.deliveryCount` | referenced | MISSING | — | Per-sequence delivery count lookup | +| `consumer.notifyDeliveryExceeded` | referenced at 2180 | MISSING | — | Max delivery exceeded advisory | + +| `consumer.setRateLimit` | consumer.go:2246 | PORTED | `TokenBucketRateLimiter` (Consumers/TokenBucketRateLimiter.cs) | Token bucket rate limiter. Go uses `golang.org/x/time/rate`; .NET has custom impl | +| `consumer.setRateLimitNeedsLocks` | consumer.go:2228 | MISSING | — | Lock-safe rate limit update | + +| `ConsumerState` struct (store.go) | referenced | PORTED | `ConsumerState` (Storage/ConsumerState.cs) | All fields: Delivered, AckFloor, Pending, Redelivered | +| `SequencePair` struct (store.go) | referenced | PORTED | `SequencePair` (Storage/ConsumerState.cs:8) | | +| `Pending` struct (store.go) | referenced | PORTED | `Pending` (Storage/ConsumerState.cs:17) | | +| `ConsumerStore` interface | referenced | PORTED | `IConsumerStore` (Storage/IConsumerStore.cs) | All methods: SetStarting, UpdateStarting, Reset, HasState, UpdateDelivered, UpdateAcks, Update, State, BorrowState, EncodedState, Type, Stop, Delete, StreamDelete | +| `consumerFileStore` | referenced | PORTED | `ConsumerFileStore` (Storage/ConsumerFileStore.cs) | File-backed store with background flusher | +| `encodeConsumerState/decodeConsumerState` | referenced | PORTED | `ConsumerStateCodec` (Storage/ConsumerStateCodec.cs) | Go-compatible binary codec with varint encoding | + +| Status | Count | +| PORTED | 23 | +| PARTIAL | 21 | +| MISSING | 70 | +| NOT_APPLICABLE | 1 | +| **Total** | **115** | + + +| `consumer.isFiltered()` | consumer.go:3524 | PARTIAL | `CompiledFilter` in PullConsumerEngine.cs | .NET has CompiledFilter but lacks stream-subject comparison logic (checks subjf vs mset.cfg.Subjects) | +| `consumer.needAck()` | consumer.go:3571 | PARTIAL | `InterestRetentionPolicy.ShouldRetain()` | .NET covers interest retention case but missing full ack-policy switch (AckNone/AckAll/AckExplicit), filtered match, and leader/follower state borrow | +| `consumer.isFilteredMatch()` | consumer.go:4490 | PORTED | `FilterSkipTracker.ShouldDeliver()`, `CompiledFilter.Matches()` | Both use SubjectMatch for token-based matching; Go has tokenized optimization | +| `consumer.isEqualOrSubsetMatch()` | consumer.go:4515 | MISSING | — | Checks if filter subject is equal to or subset of consumer's filter subjects; used in config validation | + +| `PriorityGroup` (struct) | consumer.go:3635 | PORTED | `PriorityGroupManager` + `PullWaitingRequest.Priority` | Go struct fields (Group, MinPending, MinAckPending, Id, Priority) mapped across .NET types | +| `consumer.setPinnedTimer()` | consumer.go:4000 | PARTIAL | `PriorityGroupManager.AssignPinId()` | .NET assigns pin ID but lacks TTL timer + unpinned advisory on timeout | +| `consumer.assignNewPinId()` | consumer.go:4020 | PARTIAL | `PriorityGroupManager.AssignPinId()` | .NET assigns but missing pinnedTS tracking and pinned advisory send | +| `consumer.unassignPinId()` | consumer.go:4032 | PORTED | `PriorityGroupManager.UnassignPinId()` | Timer stop + pin clear | + +| `waitingRequest` (struct) | consumer.go:3690 | PORTED | `PullWaitingRequest` + `PullRequest` | Represented as records; Go has linked list pointers, .NET uses separate queue | +| `waitingDelivery` (struct) | consumer.go:3731 | MISSING | — | Tracks pending replicated deliveries (seq, pending msgs/bytes); needed for clustered pull consumers | +| `waitQueue` (struct) | consumer.go:3753 | PORTED | `PullRequestWaitQueue` + `WaitingRequestQueue` | Two .NET implementations: priority-based and FIFO | +| `newWaitQueue()` | consumer.go:3761 | PORTED | `PullRequestWaitQueue` constructor | | +| `waitQueue.insertSorted()` | consumer.go:3771 | PORTED | `PullRequestWaitQueue.Enqueue()` | Stable priority insertion sort | +| `waitQueue.addPrioritized()` | consumer.go:3783 | PORTED | `PullRequestWaitQueue.Enqueue()` | | +| `waitQueue.add()` | consumer.go:3798 | PORTED | `WaitingRequestQueue.Enqueue()` | FIFO append | +| `waitQueue.isFull()` | consumer.go:3821 | PORTED | `PullRequestWaitQueue` max size check | | +| `waitQueue.isEmpty()` | consumer.go:3828 | PORTED | `WaitingRequestQueue.IsEmpty` | | +| `waitQueue.len()` | consumer.go:3835 | PORTED | `WaitingRequestQueue.Count` | | +| `waitQueue.peek()` | consumer.go:3843 | PORTED | `PullRequestWaitQueue.Peek()` | | +| `waitQueue.cycle()` | consumer.go:3850 | MISSING | — | Removes head and re-adds to tail; used for round-robin cycling | +| `waitQueue.popOrPopAndRequeue()` | consumer.go:3860 | PORTED | `PullRequestWaitQueue.PopAndRequeue()` | Dispatches based on priority policy | +| `waitQueue.pop()` | consumer.go:3873 | PORTED | `PullRequestWaitQueue.Dequeue()` | | +| `waitQueue.popAndRequeue()` | consumer.go:3892 | PORTED | `PullRequestWaitQueue.PopAndRequeue()` | | +| `insertAtPosition()` | consumer.go:3925 | PORTED | (inline in PullRequestWaitQueue.Enqueue) | Priority insertion logic | +| `waitQueue.removeCurrent()` | consumer.go:3959 | PORTED | (implicit in Dequeue) | | +| `waitQueue.remove()` | consumer.go:3964 | PARTIAL | — | Go supports arbitrary removal from linked list; .NET queue doesn't have arbitrary mid-list removal | +| `consumer.pendingRequests()` | consumer.go:3988 | PARTIAL | `PullConsumerEngine.GetClusterPendingRequests()` | Returns pending as reply-keyed map vs collection | +| `consumer.nextWaiting()` | consumer.go:4044 | PARTIAL | `PullRequestWaitQueue.PopAndRequeue()` | .NET has basic pop; Go has full priority pin-check logic, max-bytes rejection, expiry, interest checking | + +| `nextReqFromMsg()` | consumer.go:3652 | PARTIAL | `PullFetchRequest` parsing in ConsumerApiHandlers | .NET parses batch from JSON; Go parses batch, maxBytes, noWait, expires, heartbeat, priorityGroup | +| `nextMsgReq` (struct) | consumer.go:4186 | NOT_APPLICABLE | — | Pool-based message wrapper; .NET uses different request pipeline | +| `consumer.processNextMsgReq()` | consumer.go:4219 | PARTIAL | `ConsumerApiHandlers.HandleNext()` | .NET has basic batch fetch; Go validates push vs pull, API level, and queues to ipQueue | +| `consumer.processNextMsgRequest()` | consumer.go:4276 | PARTIAL | `PullConsumerEngine.FetchAsync()` | .NET handles basic fetch; Go handles maxRequestBatch/Expires/MaxBytes limits, priority group validation, noWait, interest tracking, queue add | +| `consumer.processResetReq()` | consumer.go:4241 | PORTED | `ConsumerManager.ResetToSequence()` + `ConsumerApiHandlers.HandleReset()` | API handler + manager reset | +| `trackDownAccountAndInterest()` | consumer.go:4419 | NOT_APPLICABLE | — | Account export response tracking for cross-account pull requests; not yet needed in .NET | + +| `consumer.deliveryCount()` | consumer.go:4439 | PORTED | `RedeliveryTracker._deliveryCounts` access | Via dictionary lookup | +| `consumer.incDeliveryCount()` | consumer.go:4452 | PORTED | `RedeliveryTracker.IncrementDeliveryCount()` | | +| `consumer.decDeliveryCount()` | consumer.go:4463 | MISSING | — | Adjusts delivery count down on failed delivery; not present in .NET tracker | +| `consumer.notifyDeliveryExceeded()` | consumer.go:4471 | PARTIAL | `AckProcessor.ExceededSequences` + `DeliveryExceededPolicy` | .NET tracks exceeded sequences; Go sends advisory event via sendAdvisory | + +| `consumer.getNextMsg()` | consumer.go:4540 | PARTIAL | `PullConsumerEngine.FetchAsync()` | .NET handles basic store load + filter match; Go has full redelivery queue processing, max pending check, skip list, multi-filter LoadNextMsg/LoadNextMsgMulti | +| `consumer.processWaiting()` | consumer.go:4645 | PARTIAL | `WaitingRequestQueue.RemoveExpired()` | .NET only removes expired; Go also checks interest, sends heartbeats, handles noWait EOS, replicated deliveries | +| `consumer.checkWaitingForInterest()` | consumer.go:4745 | MISSING | — | Checks if any waiting requests still have interest | +| `consumer.hbTimer()` | consumer.go:4751 | PORTED | `PushConsumerEngine.StartIdleHeartbeatTimer()` | | +| `consumer.checkAckFloor()` | consumer.go:4762 | MISSING | — | Corrects ack floor drift when stream FirstSeq advances past consumer asflr; processes stale pending entries | +| `consumer.processInboundAcks()` | consumer.go:4854 | PARTIAL | `AckProcessor.ProcessAck()` is synchronous | Go runs as dedicated goroutine with ack floor drift ticker; .NET processes inline | +| `consumer.processInboundNextMsgReqs()` | consumer.go:4902 | PARTIAL | `ConsumerApiHandlers.HandleNext()` | Go runs as dedicated goroutine draining ipQueue; .NET handles synchronously per API call | +| `consumer.suppressDeletion()` | consumer.go:4926 | MISSING | — | Resets inactivity timer on ack activity to prevent premature ephemeral deletion | +| `consumer.loopAndGatherMsgs()` | consumer.go:4948 | PARTIAL | `PushConsumerEngine.LoopAndGatherMsgsAsync()` | .NET has basic gather loop; Go has full pause check, push active check, pull wait check, replay delay, rate limit, flow control, heartbeat timer | + +| `consumer.sendIdleHeartbeat()` | consumer.go:5222 | PORTED | `PushConsumerEngine.SendIdleHeartbeatCallback()` | Includes stall header and pending counts | +| `consumer.ackReply()` | consumer.go:5234 | MISSING | — | Formats `$JS.ACK.{stream}.{consumer}.{dc}.{sseq}.{dseq}.{ts}.{pending}` reply subject | +| `consumer.setMaxPendingBytes()` | consumer.go:5239 | MISSING | — | Sets flow control byte limits; used in testing | +| `consumer.checkNumPending()` | consumer.go:5252 | MISSING | — | Sanity-checks cached num pending against store state | +| `consumer.numPending()` | consumer.go:5271 | MISSING | — | Returns cached pending count (npc clamped to 0) | +| `consumer.checkNumPendingOnEOF()` | consumer.go:5281 | MISSING | — | Resets npc/npf when consumer has caught up to stream end | +| `consumer.streamNumPendingLocked()` | consumer.go:5294 | MISSING | — | Acquires lock and delegates to streamNumPending | +| `consumer.streamNumPending()` | consumer.go:5303 | MISSING | — | Forces recalculation of num pending from store | +| `consumer.calculateNumPending()` | consumer.go:5319 | MISSING | — | Calculates num pending using NumPending/NumPendingMulti with filter awareness | +| `convertToHeadersOnly()` | consumer.go:5336 | MISSING | — | Strips msg payload and replaces with Nats-Msg-Size header for HeadersOnly consumers | +| `consumer.deliverMsg()` | consumer.go:5364 | PARTIAL | `PushConsumerEngine.Enqueue()` + `RunDeliveryLoopAsync()` | .NET enqueues frames and sends via loop; Go handles dseq tracking, pending tracking, flow control, replicated delivery, rate limiting, interest ack for AckNone | +| `consumer.replicateDeliveries()` | consumer.go:5428 | NOT_APPLICABLE | — | Cluster-specific: checks if deliveries must be replicated before sending | +| `consumer.needFlowControl()` | consumer.go:5432 | PARTIAL | `PushConsumerEngine.IsFlowControlStalled` | .NET tracks pending FC count; Go tracks pbytes vs maxpb threshold | +| `consumer.processFlowControl()` | consumer.go:5448 | PARTIAL | `PushConsumerEngine.AcknowledgeFlowControl()` | .NET decrements count; Go also doubles maxpb (slow start ramp), updates pbytes accounting | +| `consumer.fcReply()` | consumer.go:5476 | MISSING | — | Generates unique flow control reply subject: `_FCR_.{stream}.{consumer}.{rand4}` | +| `consumer.sendFlowControl()` | consumer.go:5495 | PARTIAL | FC frame in `PushConsumerEngine.RunDeliveryLoopAsync()` | .NET sends FC frame; Go also tracks fcsz/fcid state | + +| `consumer.trackPending()` | consumer.go:5507 | PARTIAL | `AckProcessor.Register()` | .NET registers with ackWaitMs; Go also handles backoff array indexing and ptmr reset | +| `consumer.creditWaitingRequest()` | consumer.go:5541 | MISSING | — | Credits back a failed delivery in the wait queue (n++, d--) | +| `consumer.didNotDeliver()` | consumer.go:5554 | MISSING | — | Handles failed delivery: decrements delivery count, checks push/pull mode, queues redelivery, checks delivery interest | +| `consumer.addToRedeliverQueue()` | consumer.go:5595 | PORTED | `RedeliveryTracker.Schedule()` | .NET uses PriorityQueue; Go uses rdq slice + rdqi set | +| `consumer.hasRedeliveries()` | consumer.go:5603 | PORTED | `RedeliveryTracker.GetDue()` returns non-empty | | +| `consumer.getNextToRedeliver()` | consumer.go:5607 | PORTED | `RedeliveryTracker.GetDue()` enumeration | | +| `consumer.onRedeliverQueue()` | consumer.go:5625 | PORTED | `RedeliveryTracker.IsTracking()` (via _deliveryCounts) | | +| `consumer.removeFromRedeliverQueue()` | consumer.go:5631 | PORTED | `RedeliveryTracker.Acknowledge()` | Removes from entries and delivery counts | +| `consumer.checkPending()` | consumer.go:5651 | PARTIAL | `AckProcessor.TryGetExpired()` | .NET checks one expired at a time; Go scans all pending with backoff, checks awl interlock, removes stale below fseq/asflr, sorts expired, adjusts timestamps | + +| `consumer.seqFromReply()` | consumer.go:5770 | MISSING | — | Extracts delivery sequence from ack reply subject | +| `consumer.streamSeqFromReply()` | consumer.go:5776 | MISSING | — | Extracts stream sequence from ack reply subject | +| `parseAckReplyNum()` | consumer.go:5782 | MISSING | — | Fast ASCII digit parser for ack reply tokens | +| `replyInfo()` | consumer.go:5798 | MISSING | — | Parses all 5 fields from `$JS.ACK.{stream}.{consumer}.{dc}.{sseq}.{dseq}.{ts}.{pending}` | +| `ackReplyInfo()` | consumer.go:5821 | MISSING | — | Parses sseq, dseq, dc from ack reply subject | + +| `lastSeqSkipList` (struct) | consumer.go:5849 | MISSING | — | Holds skip list for DeliverLastPerSubject startup | +| `consumer.hasSkipListPending()` | consumer.go:5856 | MISSING | — | Checks if skip list has pending entries | +| `consumer.selectStartingSeqNo()` | consumer.go:5861 | PARTIAL | `PullConsumerEngine.ResolveInitialSequenceAsync()` | .NET handles DeliverAll/Last/New/ByStartSeq/ByStartTime/LastPerSubject; Go also handles filtered subjects for DeliverLast, MaxMsgsPer optimization for LastPerSubject, MultiLastSeqs, time-based filtered skip-ahead, empty/future/past clamping | +| `consumer.nextSeq()` | consumer.go:5841 | PORTED | `ConsumerHandle.NextSequence` property | | + +| `isDurableConsumer()` | consumer.go:5969 | PORTED | `ConsumerConfig.Ephemeral` flag (inverse) | | +| `consumer.isDurable()` | consumer.go:5973 | PORTED | `!config.Ephemeral` | | +| `consumer.isPushMode()` | consumer.go:5978 | PORTED | `ConsumerConfig.Push` property | | +| `consumer.isPullMode()` | consumer.go:5982 | PORTED | `!config.Push` | | +| `consumer.String()` | consumer.go:5987 | NOT_APPLICABLE | — | Trivial name getter | +| `createConsumerName()` | consumer.go:5994 | PORTED | `Guid.NewGuid()` in ConsumerManager | Different generation approach but same purpose | +| `stream.deleteConsumer()` | consumer.go:5999 | PORTED | `ConsumerManager.Delete()` | | +| `consumer.getStream()` | consumer.go:6003 | PORTED | `ConsumerHandle.Stream` property | | +| `consumer.streamName()` | consumer.go:6010 | PORTED | `ConsumerHandle.Stream` property | | +| `consumer.isActive()` | consumer.go:6021 | PARTIAL | `ConsumerHandle.Paused` (inverse) | .NET uses Paused; Go checks active flag + mset != nil | +| `consumer.hasNoLocalInterest()` | consumer.go:6029 | PARTIAL | `DeliveryInterestTracker.HasInterest` (inverse) | .NET tracks subscriber count; Go checks sublist for delivery subject | + +| `consumer.purge()` | consumer.go:6040 | MISSING | — | Called when parent stream is purged; adjusts sseq/asflr, clears stale pending, filters wider purge, updates store state | +| `stopAndClearTimer()` | consumer.go:6124 | NOT_APPLICABLE | — | Go timer helper; .NET uses Timer.Dispose() | +| `consumer.stop()` | consumer.go:6135 | PARTIAL | `PushConsumerEngine.StopDeliveryLoop()` + `StopGatherLoop()` | .NET stops loops; Go has full stopWithFlags with advisory, cleanup, subscription unsubscribe | +| `consumer.deleteWithoutAdvisory()` | consumer.go:6139 | MISSING | — | Deletes without sending advisory | +| `consumer.delete()` | consumer.go:6144 | PORTED | `ConsumerManager.Delete()` | | +| `consumer.isClosed()` | consumer.go:6149 | MISSING | — | Checks closed flag under lock | +| `consumer.stopWithFlags()` | consumer.go:6155 | MISSING | — | Full lifecycle shutdown: cluster assignment check, advisory, subscription cleanup, client close, interest clear, store delete/stop, RAFT node stop, directory cleanup | +| `consumer.cleanupNoInterestMessages()` | consumer.go:6334 | MISSING | — | Removes messages with no remaining consumer interest; used on interest-policy stream consumer deletion | +| `deliveryFormsCycle()` | consumer.go:6400 | MISSING | — | Validates that delivery subject doesn't create a cycle with stream subjects | +| `consumer.switchToEphemeral()` | consumer.go:6410 | MISSING | — | Converts durable to ephemeral on startup recovery; clears Durable, updates inactive threshold | +| `consumer.requestNextMsgSubject()` | consumer.go:6430 | MISSING | — | Returns the subject for next-message pull requests | + +| `consumer.decStreamPending()` | consumer.go:6434 | MISSING | — | Decrements cached num pending on message deletion; processes pending term if message was pending | +| `consumer.account()` | consumer.go:6459 | NOT_APPLICABLE | — | Account getter | +| `consumer.signalSubs()` | consumer.go:6468 | MISSING | — | Creates sublist filter subjects for stream signal registration | +| `consumer.processStreamSignal()` | consumer.go:6494 | PARTIAL | `PushConsumerEngine.Signal(ConsumerSignal.NewMessage)` | .NET signals via channel; Go checks leader, updates npc, checks push active / pull waiting | +| `subjectSliceEqual()` | consumer.go:6517 | NOT_APPLICABLE | — | Utility set equality; trivial | +| `gatherSubjectFilters()` | consumer.go:6536 | NOT_APPLICABLE | — | Utility to collect filter strings; trivial | + +| `consumer.shouldStartMonitor()` | consumer.go:6546 | MISSING | — | Atomically checks/sets monitor running flag with WaitGroup | +| `consumer.clearMonitorRunning()` | consumer.go:6560 | MISSING | — | Clears monitor running flag and decrements WaitGroup | +| `consumer.isMonitorRunning()` | consumer.go:6571 | MISSING | — | Checks monitor running flag | +| `consumer.checkStateForInterestStream()` | consumer.go:6582 | MISSING | — | For interest/WQ streams: walks sequences from FirstSeq to ack floor, acks already-consumed messages; handles pending above floor; adjusts chkflr | + +| `consumer.resetPtmr()` | consumer.go:6692 | PARTIAL | Timer in `AckProcessor.TryGetExpired()` loop | Go uses AfterFunc timer; .NET checks expiry on poll | +| `consumer.stopAndClearPtmr()` | consumer.go:6701 | PARTIAL | — | Go stops timer and clears; .NET has no dedicated pending timer | +| `consumer.resetPendingDeliveries()` | consumer.go:6706 | MISSING | — | Recycles pending and waiting delivery pool entries; clustered delivery cleanup | + +| Status | Count | +| PORTED | 31 | +| PARTIAL | 28 | +| MISSING | 37 | +| NOT_APPLICABLE | 9 | +| DEFERRED | 0 | +| **Total** | **105** | + + + + + + + + +| `memStore` struct | memstore.go:33 | PARTIAL | `MemStore` class in MemStore.cs:16 | .NET uses `Dictionary` instead of `stree.SubjectTree[SimpleState]`; no `scb`/`rmcb`/`pmsgcb` callbacks; no `ageChk` timer; no `scheduling`; no `sdm` | +| `newMemStore` | memstore.go:54 | PORTED | `MemStore(StreamConfig)` constructor, MemStore.cs:111 | TTL init present; FirstSeq handled; no `ats.Register()` | +| `UpdateConfig` | memstore.go:86 | PARTIAL | `IStreamStore.UpdateConfig`, MemStore.cs:799 | TTL create/destroy ported; per-subject limit enforcement ported; missing: age timer start/stop, `enforceMsgLimit`/`enforceBytesLimit` on config change, scheduling support | +| `recoverTTLState` | memstore.go:147 | MISSING | -- | TTL state recovery from existing messages not implemented | +| `recoverMsgSchedulingState` | memstore.go:171 | MISSING | -- | Message scheduling not implemented | + +| `storeRawMsg` (internal) | memstore.go:195 | PARTIAL | `StoreInternal`, MemStore.cs:872 | Core store logic ported (seq check, per-subject tracking, msg/bytes limits); missing: `DiscardNew` policy checks, `DiscardNewPer` check, TTL tracking in THW, age timer management, scheduling support | +| `StoreRawMsg` | memstore.go:329 | PARTIAL | `IStreamStore.StoreRawMsg`, MemStore.cs:299 | Delegates to StoreInternal; missing: `scb` callback, `receivedAny` first-message age check | +| `StoreMsg` | memstore.go:350 | PORTED | `IStreamStore.StoreMsg`, MemStore.cs:287 | Core logic matches Go | +| `SkipMsg` | memstore.go:368 | PORTED | `IStreamStore.SkipMsg`, MemStore.cs:308 | Sequence check and dmap handling present | +| `SkipMsgs` | memstore.go:395 | PORTED | `IStreamStore.SkipMsgs`, MemStore.cs:332 | Multi-skip ported | +| `FlushAllPending` | memstore.go:424 | PORTED | `IStreamStore.FlushAllPending`, MemStore.cs:358 | No-op in both | + +| `RegisterStorageUpdates` | memstore.go:431 | MISSING | -- | Storage update callback not implemented | +| `RegisterStorageRemoveMsg` | memstore.go:439 | MISSING | -- | Removal callback not implemented | +| `RegisterProcessJetStreamMsg` | memstore.go:446 | MISSING | -- | JetStream msg processing callback not implemented | + +| `GetSeqFromTime` | memstore.go:454 | PORTED | `IStreamStore.GetSeqFromTime`, MemStore.cs:571 | Binary search with gap awareness ported | +| `FilteredState` | memstore.go:531 | PORTED | `IStreamStore.FilteredState`, MemStore.cs:639 | Delegates to internal method | +| `filteredStateLocked` | memstore.go:540 | PARTIAL | `FilteredStateLocked`, MemStore.cs (internal) | Core matching ported; lacks `lastPerSubject` partial-scan optimization, `firstNeedsUpdate`/`lastNeedsUpdate` lazy recalculation | +| `SubjectsState` | memstore.go:748 | PORTED | `IStreamStore.SubjectsState`, MemStore.cs:643 | Implemented with Dictionary instead of SubjectTree | +| `AllLastSeqs` | memstore.go:780 | PORTED | `IStreamStore.AllLastSeqs`, MemStore.cs:679 | Sorted last sequences | +| `filterIsAll` | memstore.go:811 | MISSING | -- | Helper to check if filters == stream subjects | +| `MultiLastSeqs` | memstore.go:828 | PORTED | `IStreamStore.MultiLastSeqs`, MemStore.cs:691 | Matching logic ported | +| `SubjectsTotals` | memstore.go:881 | PORTED | `IStreamStore.SubjectsTotals`, MemStore.cs:664 | Per-subject counts ported | +| `NumPending` | memstore.go:913 | PORTED | `IStreamStore.NumPending`, MemStore.cs:747 | Delegates to filteredStateLocked | +| `NumPendingMulti` | memstore.go:923 | MISSING | -- | Multi-subject sublist-based pending count not implemented | + +| `enforcePerSubjectLimit` | memstore.go:1072 | PORTED | `EnforcePerSubjectLimit`, MemStore.cs (internal) | Removes oldest per-subject messages | +| `enforceMsgLimit` | memstore.go:1088 | PARTIAL | Inline in `StoreInternal` | Logic present but not separated into a named method; `DiscardOld` policy not checked | +| `enforceBytesLimit` | memstore.go:1102 | PARTIAL | Inline in `StoreInternal` | Similar to enforceMsgLimit | + +| `startAgeChk` | memstore.go:1116 | MISSING | -- | Age check timer not implemented | +| `resetAgeChk` | memstore.go:1126 | MISSING | -- | Age check timer reset not implemented | +| `cancelAgeChk` | memstore.go:1194 | MISSING | -- | Age check cancellation not implemented | +| `expireMsgs` | memstore.go:1203 | MISSING | -- | Full MaxAge + TTL expiration loop not implemented | +| `shouldProcessSdm` | memstore.go:1322 | MISSING | -- | Subject delete marker tracking not implemented | +| `shouldProcessSdmLocked` | memstore.go:1329 | MISSING | -- | SDM tracking internals | +| `handleRemovalOrSdm` | memstore.go:1362 | MISSING | -- | SDM/removal handler | +| `runMsgScheduling` | memstore.go:1383 | MISSING | -- | Message scheduling not implemented | + +| `PurgeEx` | memstore.go:1422 | PORTED | `IStreamStore.PurgeEx`, MemStore.cs:481 | Subject-specific + keep + seq purge ported | +| `Purge` | memstore.go:1471 | PORTED | `IStreamStore.Purge`, MemStore.cs:472 | Full purge ported | +| `purge` (internal) | memstore.go:1475 | PORTED | `PurgeInternal`, MemStore.cs | Resets msgs/fss/dmap | +| `Compact` | memstore.go:1509 | PORTED | `IStreamStore.Compact`, MemStore.cs:533 | Compaction ported | +| `compact` (internal) | memstore.go:1513 | PORTED | `CompactInternal`, MemStore.cs | Removes messages before seq | +| `reset` | memstore.go:1583 | PORTED | `IStreamStore.Truncate(0)`, MemStore.cs:540 | Full reset handled via Truncate(0) | +| `Truncate` | memstore.go:1618 | PORTED | `IStreamStore.Truncate`, MemStore.cs:536 | Removes messages after seq | + +| `SubjectForSeq` | memstore.go:1678 | PORTED | `IStreamStore.SubjectForSeq`, MemStore.cs:736 | | +| `LoadMsg` | memstore.go:1692 | PORTED | `IStreamStore.LoadMsg`, MemStore.cs:361 | | +| `loadMsgLocked` | memstore.go:1697 | PORTED | Inline in LoadMsg | Lock parameter not needed (.NET uses `lock(_gate)`) | +| `LoadLastMsg` | memstore.go:1724 | PORTED | `IStreamStore.LoadLastMsg`, MemStore.cs:393 | Wildcard + literal + fwcs paths | +| `loadLastLocked` | memstore.go:1733 | PORTED | Inline in LoadLastMsg | | +| `LoadNextMsgMulti` | memstore.go:1763 | MISSING | -- | Multi-subject sublist-based next message not implemented | +| `LoadNextMsg` | memstore.go:1798 | PARTIAL | `IStreamStore.LoadNextMsg`, MemStore.cs:372 | Linear scan only; missing `shouldLinearScan` optimization, `nextWildcardMatchLocked`, `nextLiteralMatchLocked` | +| `nextWildcardMatchLocked` | memstore.go:1810 | MISSING | -- | FSS-based wildcard bounds optimization | +| `nextLiteralMatchLocked` | memstore.go:1852 | MISSING | -- | FSS-based literal bounds optimization | +| `shouldLinearScan` | memstore.go:1868 | MISSING | -- | Linear scan decision helper | +| `loadNextMsgLocked` | memstore.go:1877 | PARTIAL | Inline in LoadNextMsg | Core linear scan ported; FSS-based optimizations missing | +| `LoadPrevMsg` | memstore.go:1925 | PORTED | `IStreamStore.LoadPrevMsg`, MemStore.cs:439 | Backward walk ported | +| `LoadPrevMsgMulti` | memstore.go:1952 | MISSING | -- | Multi-subject sublist-based prev message not implemented | + +| `RemoveMsg` | memstore.go:1987 | PORTED | `IStreamStore.RemoveMsg`, MemStore.cs:454 | | +| `EraseMsg` | memstore.go:1995 | PARTIAL | `IStreamStore.EraseMsg`, MemStore.cs:463 | Does not overwrite with random bytes (secure erase) | +| `updateFirstSeq` | memstore.go:2004 | PORTED | Inline in `RemoveInternal` | Updates first seq on removal | +| `removeSeqPerSubject` | memstore.go:2037 | PORTED | `RemoveSeqPerSubject`, MemStore.cs | Per-subject tracking update on remove | +| `recalculateForSubj` | memstore.go:2070 | MISSING | -- | Lazy first/last recalculation for per-subject state (uses `firstNeedsUpdate`/`lastNeedsUpdate` flags) | +| `removeMsg` (internal) | memstore.go:2110 | PARTIAL | `RemoveInternal`, MemStore.cs | Core removal ported; missing: TTL removal from THW, secure erase with random bytes, `scb` callback | +| `deleteFirstMsgOrPanic` | memstore.go:1667 | PORTED | Inline in limit enforcement | | +| `deleteFirstMsg` | memstore.go:1673 | PORTED | Inline in limit enforcement | | + +| `Type` | memstore.go:2167 | PORTED | `IStreamStore.Type`, MemStore.cs:797 | Returns `StorageType.Memory` | +| `FastState` | memstore.go:2173 | PORTED | `IStreamStore.FastState`, MemStore.cs:781 | Populates ref struct | +| `State` | memstore.go:2192 | PORTED | `IStreamStore.State`, MemStore.cs:757 | Full state with deleted list | +| `Utilization` | memstore.go:2221 | MISSING | -- | Returns (total, reported) byte counts | +| `memStoreMsgSize` | memstore.go:2231 | PORTED | `MsgSize`, MemStore.cs (internal) | Size calculation for accounting | +| `memStoreMsgSizeRaw` | memstore.go:2227 | PORTED | Inline in MsgSize | | +| `ResetState` | memstore.go:2236 | PORTED | `IStreamStore.ResetState`, MemStore.cs:846 | No-op (scheduling not implemented) | +| `Delete` | memstore.go:2245 | PORTED | `IStreamStore.Delete`, MemStore.cs:831 | | +| `Stop` | memstore.go:2249 | PORTED | `IStreamStore.Stop`, MemStore.cs:828 | | +| `isClosed` | memstore.go:2272 | MISSING | -- | Closed check (msgs == nil) | +| `EncodedStreamState` | memstore.go:2322 | PARTIAL | `IStreamStore.EncodedStreamState`, MemStore.cs:849 | Returns empty array stub — binary encoding not implemented | +| `SyncDeleted` | memstore.go:2357 | MISSING | -- | Delete block synchronization for NRG not implemented | + +| `consumerMemStore` struct | memstore.go:2278 | MISSING | -- | In-memory consumer state store not implemented as a distinct type | +| `ConsumerStore` (factory) | memstore.go:2286 | MISSING | -- | Consumer store creation stub; returns `NotSupportedException` | +| `AddConsumer` | memstore.go:2301 | MISSING | -- | Consumer count tracking | +| `RemoveConsumer` | memstore.go:2308 | MISSING | -- | Consumer count tracking | +| `Snapshot` | memstore.go:2317 | NOT_APPLICABLE | -- | Returns "no impl" in Go too | +| `consumerMemStore.Update` | memstore.go:2382 | MISSING | -- | Consumer state update | +| `consumerMemStore.SetStarting` | memstore.go:2428 | MISSING | -- | Set starting sequence | +| `consumerMemStore.UpdateStarting` | memstore.go:2437 | MISSING | -- | Update starting sequence | +| `consumerMemStore.Reset` | memstore.go:2451 | MISSING | -- | Reset consumer state | +| `consumerMemStore.HasState` | memstore.go:2459 | MISSING | -- | Check if state exists | +| `consumerMemStore.UpdateDelivered` | memstore.go:2466 | MISSING | -- | Update delivery tracking | +| `consumerMemStore.UpdateAcks` | memstore.go:2532 | MISSING | -- | Ack processing (AckAll / AckExplicit) | +| `consumerMemStore.UpdateConfig` | memstore.go:2606 | MISSING | -- | Update consumer config | +| `consumerMemStore.Stop` | memstore.go:2615 | MISSING | -- | Stop consumer store | +| `consumerMemStore.Delete` | memstore.go:2624 | MISSING | -- | Delete consumer store | +| `consumerMemStore.StreamDelete` | memstore.go:2628 | MISSING | -- | Stream-level delete | +| `consumerMemStore.State` | memstore.go:2632 | MISSING | -- | Get consumer state | +| `consumerMemStore.BorrowState` | memstore.go:2638 | MISSING | -- | Borrow state (no copy) | +| `consumerMemStore.EncodedState` | memstore.go:2672 | MISSING | -- | Binary-encode consumer state | +| `consumerMemStore.Type` | memstore.go:2700 | MISSING | -- | Returns MemoryStorage | + + +| `FileStoreConfig` struct | filestore.go:55 | PORTED | `FileStoreConfig` class, FileStoreConfig.cs:12 | All fields ported (StoreDir, BlockSize, CacheExpire, SubjectStateExpire, SyncInterval, SyncAlways, AsyncFlush, Cipher, Compression); missing: `srv` internal reference | +| `FileStreamInfo` | filestore.go:80 | MISSING | -- | Created + StreamConfig wrapper not defined | +| `StoreCipher` enum | filestore.go:85 | PORTED | `StoreCipher` enum, AeadEncryptor.cs:22 | ChaCha, AES, NoCipher | +| `StoreCompression` enum | filestore.go:106 | PORTED | `StoreCompression` enum, AeadEncryptor.cs:39 | NoCompression, S2Compression | +| `StoreCompression.String/MarshalJSON/UnmarshalJSON` | filestore.go:113-151 | NOT_APPLICABLE | -- | .NET enums have built-in string conversion; JSON handled by System.Text.Json | +| `FileConsumerInfo` | filestore.go:154 | MISSING | -- | Consumer info with Created + Name + Config | +| `psi` struct | filestore.go:166 | MISSING | -- | Per-subject index (total, fblk, lblk) for PSIM tree | +| `fileStore` struct | filestore.go:172 | PARTIAL | `FileStore` class, FileStore.cs:23 | .NET uses in-memory Dictionary instead of block-based PSIM; missing: `psim` SubjectTree, `bim` block index map, `tombs`, `ld`, callbacks (`scb`/`rmcb`/`pmsgcb`), age timer, sync timer, `prf`/`aek` encryption keys, `hh` highway hash, `qch`/`fsld` channels, async flush state | +| `msgBlock` struct | filestore.go:217 | PARTIAL | `MsgBlock` class, MsgBlock.cs:24 | Basic block file + index + delete tracking ported; missing: encryption (`aek`/`bek`/`seed`/`nonce`), compression (`cmp`), FSS subject tree, cache expiry timer (`ctmr`), write-error tracking (`werr`), `fch`/`qch` channels, loading/flusher flags, compact/sync flags | +| `cache` struct | filestore.go:270 | PARTIAL | `_cache` Dictionary in MsgBlock.cs:49 | .NET uses Dictionary; Go uses byte-buffer + index array | +| `msgId` struct | filestore.go:278 | MISSING | -- | Sequence+timestamp pair | +| Constants (magic, version, dirs, sizes) | filestore.go:283-377 | PARTIAL | -- | Some constants like block sizes defined differently in `FileStoreOptions`; most Go constants (magic bytes, dir names, scan patterns, thresholds) not ported | + +| `newFileStore` | filestore.go:379 | PARTIAL | `FileStore` constructor, FileStore.cs | .NET creates directory + first block; missing: `dynBlkSize`, encryption key setup, highway hash init, full recovery pipeline | +| `newFileStoreWithCreated` | filestore.go:383 | PARTIAL | (merged into constructor) | Core directory setup present; missing: `prf`/`oldprf` key gen, AEK recovery, full state recovery, TTL/scheduling recovery, tombstone processing, meta file write, sync timer, flush state loop | +| `lockAllMsgBlocks`/`unlockAllMsgBlocks` | filestore.go:641-653 | NOT_APPLICABLE | -- | .NET uses different concurrency model | +| `UpdateConfig` | filestore.go:655 | PARTIAL | `IStreamStore.UpdateConfig` in FileStore.cs | Basic config update present; missing: meta file write, TTL/scheduling, limits enforcement, async flush toggle | +| `dynBlkSize` | filestore.go:763 | MISSING | -- | Dynamic block size calculation based on retention/maxBytes/encryption | + +| `genEncryptionKey` | filestore.go:800 | PORTED | `AeadEncryptor.Encrypt/Decrypt`, AeadEncryptor.cs:73 | ChaCha20-Poly1305 and AES-GCM both supported | +| `genEncryptionKeys` | filestore.go:816 | MISSING | -- | Key encryption key generation with PRF not ported | +| `genBlockEncryptionKey` | filestore.go:864 | MISSING | -- | Block-level stream cipher (ChaCha20/AES-CTR) not ported | +| `recoverAEK` | filestore.go:878 | MISSING | -- | AEK recovery from key file | +| `setupAEK` | filestore.go:907 | MISSING | -- | AEK initial setup | +| `writeStreamMeta` | filestore.go:929 | MISSING | -- | Stream metadata + checksum write | +| `loadEncryptionForMsgBlock` | filestore.go:1078 | MISSING | -- | Per-block encryption key loading | +| `convertCipher` | filestore.go:1318 | MISSING | -- | Cipher conversion between ChaCha/AES | +| `convertToEncrypted` | filestore.go:1407 | MISSING | -- | Plaintext to encrypted block conversion | + +| `getMsgBlockBuf`/`recycleMsgBlockBuf` | filestore.go:996-1032 | MISSING | -- | Pooled block buffers (Go sync.Pool); .NET uses GC | +| `msgHdrSize`/`checksumSize`/`emptyRecordLen` | filestore.go:1034-1038 | PARTIAL | `MessageRecord.Encode/Decode` in MessageRecord.cs | .NET uses its own binary format (not identical to Go's 22-byte header) | +| `noTrackSubjects` | filestore.go:1041 | MISSING | -- | Helper to check if subject tracking is needed | +| `initMsgBlock` | filestore.go:1046 | PARTIAL | `MsgBlock.Create`, MsgBlock.cs:174 | Basic block init; missing highway hash, cache/sync config | + +| `checkAndLoadEncryption` | filestore.go:1068 | MISSING | -- | Encryption check on block load | +| `ensureLastChecksumLoaded` | filestore.go:1139 | MISSING | -- | Lazy last checksum load | +| `recoverMsgBlock` | filestore.go:1148 | PARTIAL | `MsgBlock.Recover`, MsgBlock.cs:188 | Basic file scan + index rebuild present; missing: encryption handling, index file check, FSS population, lost data tracking | +| `lostData`/`addLostData`/`removeFromLostData` | filestore.go:1224-1275 | MISSING | -- | Lost data tracking not implemented | +| `rebuildState`/`rebuildStateLocked` | filestore.go:1277-1315 | PARTIAL | -- | FileStore does a simplified rebuild; Go's full block-by-block state aggregation not ported | +| `rebuildStateLocked` (msgBlock) | filestore.go:1454 | MISSING | -- | Per-block state rebuild from raw buffer | +| `rebuildStateFromBufLocked` | filestore.go:1493 | MISSING | -- | Detailed record-by-record scan with tombstone/ebit/gap detection | + +| `warn`/`debug` (logging) | filestore.go:1708-1724 | NOT_APPLICABLE | -- | .NET uses ILogger | +| `updateTrackingState` | filestore.go:1727 | MISSING | -- | Helper to aggregate per-block state | +| `trackingStatesEqual` | filestore.go:1743 | MISSING | -- | State consistency check | +| `recoverFullState` | filestore.go:1754 | MISSING | -- | Full state recovery from index.db | +| `recoverTTLState` | filestore.go:2042 | MISSING | -- | TTL hash wheel recovery from thw.db | +| `recoverMsgSchedulingState` | filestore.go:2123 | MISSING | -- | Message scheduling recovery from sched.db | +| `lastChecksum` (msgBlock) | filestore.go:2204 | PARTIAL | `MsgBlock.LastChecksum`, MsgBlock.cs:155 | Checksum tracked on write; missing: load from encrypted block | +| `cleanupOldMeta` | filestore.go:2236 | MISSING | -- | Remove legacy .idx/.fss files | +| `recoverMsgs` | filestore.go:2263 | PARTIAL | Recovery in FileStore constructor | Basic block scan + re-open present; missing: per-block first/last propagation, tombstone handling, orphan key cleanup | +| `expireMsgsOnRecover` | filestore.go:2401 | MISSING | -- | Startup age expiration | +| `copyMsgBlocks` | filestore.go:2589 | NOT_APPLICABLE | -- | Go slice copy helper | + +| `GetSeqFromTime` (fs) | filestore.go:2600 | PARTIAL | In FileStore.cs | Basic implementation present; missing: block selection by timestamp, binary search across blocks | +| `firstMatchingMulti` (mb) | filestore.go:2666 | MISSING | -- | Block-level multi-subject first match | +| `firstMatching` (mb) | filestore.go:2806 | MISSING | -- | Block-level first matching with FSS optimization | +| `prevMatchingMulti` (mb) | filestore.go:2948 | MISSING | -- | Block-level reverse multi-subject match | +| `filteredPending` / `filteredPendingLocked` (mb) | filestore.go:3062-3188 | MISSING | -- | Per-block filtered pending count | +| `FilteredState` (fs) | filestore.go:3191 | PARTIAL | In FileStore.cs | Simplified version; missing: per-block aggregation with FSS | +| `checkSkipFirstBlock` | filestore.go:3241 | MISSING | -- | Block skip optimization using PSIM | +| `checkSkipFirstBlockMulti` | filestore.go:3269 | MISSING | -- | Multi-subject block skip | +| `selectSkipFirstBlock` | filestore.go:3287 | MISSING | -- | Block index selection helper | +| `numFilteredPending` / `numFilteredPendingNoLast` / `numFilteredPendingWithLast` | filestore.go:3308-3410 | MISSING | -- | Optimized filtered pending using PSIM | +| `SubjectsState` (fs) | filestore.go:3413 | PARTIAL | In FileStore.cs | In-memory implementation; missing: per-block FSS aggregation | +| `AllLastSeqs` / `allLastSeqsLocked` (fs) | filestore.go:3495-3553 | PARTIAL | In FileStore.cs | In-memory implementation; missing: reverse block walk with FSS | +| `filterIsAll` (fs) | filestore.go:3558 | MISSING | -- | Filter optimization helper | +| `MultiLastSeqs` (fs) | filestore.go:3575 | PARTIAL | In FileStore.cs | In-memory implementation; missing: PSIM-based optimization, per-block walk | +| `NumPending` (fs) | filestore.go:3710 | PARTIAL | In FileStore.cs | Simplified version; missing: PSIM optimization, block-level aggregation | + +| Status | MemStore | FileStore (1-4000) | Total | +| PORTED | 33 | 4 | 37 | +| PARTIAL | 11 | 16 | 27 | +| MISSING | 36 | 35 | 71 | +| NOT_APPLICABLE | 1 | 4 | 5 | +| DEFERRED | 0 | 0 | 0 | +| **Total** | **81** | **59** | **140** | + + +| `fs.NumPendingMulti` | filestore.go:4051 | MISSING | -- | Multi-filter NumPending using SimpleSublist; consumer optimization path | +| `fs.SubjectsTotals` | filestore.go:4387 | PORTED | `FileStore.SubjectsTotals` | .NET iterates _messages dict; Go uses psim trie Match | +| `fs.subjectsTotalsLocked` | filestore.go:4394 | PORTED | (inlined in SubjectsTotals) | Internal locked helper | +| `fs.RegisterStorageUpdates` | filestore.go:4412 | MISSING | -- | Callback registration for storage change notifications | +| `fs.RegisterStorageRemoveMsg` | filestore.go:4424 | MISSING | -- | Callback registration for message removals (used by replicated streams) | +| `fs.RegisterProcessJetStreamMsg` | filestore.go:4431 | MISSING | -- | Callback registration for new JetStream message processing | +| `fs.hashKeyForBlock` | filestore.go:4439 | MISSING | -- | HighwayHash key derivation per block index | +| `mb.setupWriteCache` | filestore.go:4443 | PARTIAL | `MsgBlock._cache` / `WriteCacheManager` | .NET has a cache dict + WriteCacheManager; Go has elastic pointer, cache expiry timer, and loadMsgs fallback | +| `mb.finishedWithCache` | filestore.go:4477 | PARTIAL | `WriteCacheManager.EvictBlock` | .NET evicts on block rotation; Go weakens elastic reference when no pending writes | +| `fs.newMsgBlockForWrite` | filestore.go:4485 | PARTIAL | `FileStore.RotateBlock` | .NET creates new MsgBlock; Go also handles flush of pending, cache recycling, HighwayHash init, encryption key gen, flush loop spin-up | +| `fs.genEncryptionKeysForBlock` | filestore.go:4575 | MISSING | -- | Per-block AEAD key generation and keyfile writing; .NET uses per-payload AEAD instead | +| `fs.storeRawMsg` (internal) | filestore.go:4599 | PARTIAL | `FileStore.StoreRawMsg` | .NET stores but lacks: per-subject max enforcement (MaxMsgsPer), DiscardNew checks, psim updates, enforceMsgLimit/enforceBytesLimit calls, per-message TTL wheel, message scheduling | +| `fs.StoreRawMsg` (exported) | filestore.go:4759 | PORTED | `FileStore.StoreRawMsg` | Public wrapper with callback | +| `fs.StoreMsg` | filestore.go:4779 | PORTED | `FileStore.StoreMsg` | Returns (seq, ts); .NET simplified | +| `mb.skipMsg` | filestore.go:4801 | PARTIAL | `MsgBlock.WriteSkip` | .NET writes skip record; Go also handles empty-block meta update, dmap insert, ebit flag | +| `fs.SkipMsg` | filestore.go:4829 | PORTED | `FileStore.SkipMsg` | Sequence mismatch check, first/last update | +| `fs.SkipMsgs` | filestore.go:4868 | PORTED | `FileStore.SkipMsgs` | .NET iterates SkipMsg; Go batch-inserts into dmap with single write placeholder | +| `fs.FlushAllPending` | filestore.go:4933 | PORTED | `FileStore.FlushAllPending` | .NET flushes active block + writes stream.state | +| `fs.rebuildFirst` | filestore.go:4940 | MISSING | -- | Rebuilds first-block state after removal errors | +| `fs.firstSeqForSubj` | filestore.go:4966 | MISSING | -- | Optimized first-seq lookup per subject using psim fblk/lblk | +| `fs.enforceMsgLimit` | filestore.go:5033 | MISSING | -- | Drop oldest messages when MaxMsgs exceeded; bulk purge of full blocks | +| `fs.enforceBytesLimit` | filestore.go:5061 | MISSING | -- | Drop oldest messages when MaxBytes exceeded; bulk purge of full blocks | +| `fs.enforceMsgPerSubjectLimit` | filestore.go:5091 | MISSING | -- | Complex per-subject enforcement with psim skew detection and rebuild | +| `fs.deleteFirstMsg` | filestore.go:5222 | MISSING | -- | Trivial wrapper: removeMsgViaLimits(firstSeq) | +| `fs.removeMsgViaLimits` | filestore.go:5229 | MISSING | -- | Remove via limits (no forced index update) | +| `fs.RemoveMsg` | filestore.go:5235 | PORTED | `FileStore.RemoveMsg` | .NET simplified: removes from dict + soft-deletes in block | +| `fs.EraseMsg` | filestore.go:5239 | PORTED | `FileStore.EraseMsg` | .NET does secure erase via MsgBlock.Delete(secureErase: true) | +| `fs.removePerSubject` | filestore.go:5245 | MISSING | -- | Decrements psim per-subject counter, removes from trie at zero | +| `fs.removeMsg` (internal) | filestore.go:5267 | PARTIAL | `FileStore.RemoveMsg` / `FileStore.EraseMsg` | .NET simplified; Go handles: tombstone writing, cache loading, per-subject tracking, FIFO optimization, out-of-order dmap insert, inline compaction, secure erase, callback | +| `fs.removeMsgFromBlock` | filestore.go:5307 | PARTIAL | `FileStore.DeleteInBlock` | .NET delegates to MsgBlock.Delete; Go handles full per-block accounting, tombstone write, secure erase, FIFO selectNextFirst, dmap, inline compact, empty-block removal | +| `fs.removeMsgsInRange` | filestore.go:5509 | MISSING | -- | Bulk removal in [first,last] with block-level purge optimization | +| `mb.shouldCompactInline` | filestore.go:5553 | MISSING | -- | Heuristic: compact when rbytes > minimum and 2x savings possible | +| `mb.shouldCompactSync` | filestore.go:5561 | MISSING | -- | Sync-time compaction heuristic | +| `mb.compact` | filestore.go:5567 | MISSING | -- | Full block rewrite compaction | +| `mb.compactWithFloor` | filestore.go:5576 | MISSING | -- | Compaction with tombstone cleanup; handles compression, encryption, dmap pruning | +| `mb.slotInfo` | filestore.go:5735 | MISSING | -- | Cache index slot lookup for record offset/length | +| `fs.isClosed` | filestore.go:5778 | PORTED | `FileStore._stopped` | Atomic bool in Go; simple bool in .NET | +| `mb.spinUpFlushLoop` | filestore.go:5783 | MISSING | -- | Goroutine-based flush loop per block | +| `mb.spinUpFlushLoopLocked` | filestore.go:5791 | MISSING | -- | Locked version of flush loop startup | +| `kickFlusher` | filestore.go:5805 | MISSING | -- | Non-blocking channel send to wake flush goroutine | +| `mb.kickFlusher` | filestore.go:5815 | MISSING | -- | Per-block flusher kick | +| `mb.setInFlusher` | filestore.go:5821 | MISSING | -- | Flusher state tracking | +| `mb.clearInFlusher` | filestore.go:5827 | MISSING | -- | Flusher cleanup, close channels | +| `mb.flushLoop` | filestore.go:5842 | MISSING | -- | Background goroutine: coalesce pending writes, flush, close FDs when no longer last block | +| `mb.eraseMsg` | filestore.go:5890 | PARTIAL | `MsgBlock.Delete(secureErase)` | .NET overwrites payload with random bytes; Go overwrites full record including header, recalculates hash, updates both cache and disk | +| `mb.truncate` | filestore.go:5940 | MISSING | -- | Truncate block to target sequence; handles decompression/re-encryption, eof calc, per-subject reset | +| `mb.isEmpty` | filestore.go:6046 | PARTIAL | `MsgBlock.MessageCount == 0` | Go uses atomic first/last comparison | +| `mb.selectNextFirst` | filestore.go:6051 | MISSING | -- | Walk dmap to find next non-deleted first sequence | +| `fs.selectNextFirst` | filestore.go:6091 | MISSING | -- | Clean up empty leading blocks, advance first | +| `mb.resetCacheExpireTimer` | filestore.go:6123 | MISSING | -- | Per-block timer reset for cache expiry | +| `mb.startCacheExpireTimer` | filestore.go:6135 | MISSING | -- | Start per-block cache expiry timer | +| `mb.clearCacheAndOffset` | filestore.go:6141 | PARTIAL | `MsgBlock.ClearCache` | .NET clears cache dict; Go also resets linear scan tracker | +| `mb.clearCache` | filestore.go:6148 | PARTIAL | `MsgBlock.ClearCache` | .NET nulls cache dict; Go handles fss expiry, elastic pointer cleanup, buffer recycling | +| `mb.expireCache` | filestore.go:6176 | PARTIAL | `WriteCacheManager` background tick | .NET uses 500ms periodic timer; Go uses per-block AfterFunc timer | +| `mb.tryForceExpireCache` | filestore.go:6182 | MISSING | -- | Force expire by temporarily clearing timestamps | +| `mb.tryForceExpireCacheLocked` | filestore.go:6189 | MISSING | -- | Locked force-expire | +| `mb.tryExpireWriteCache` | filestore.go:6199 | MISSING | -- | Expire write cache on block rotation, return recyclable buffer | +| `mb.expireCacheLocked` | filestore.go:6220 | PARTIAL | `WriteCacheManager` eviction logic | .NET time-based eviction; Go handles elastic pointer strengthening, pending write detection, fss separate expiry | +| `fs.startAgeChk` | filestore.go:6277 | PARTIAL | `FileStore.RegisterTtl` + `HashWheel` | .NET uses HashWheel; Go uses AfterFunc timer | +| `fs.resetAgeChk` | filestore.go:6287 | PARTIAL | (implicit in ExpireFromWheel) | Go has sophisticated next-expire calculation with minimum 250ms floor | +| `fs.cancelAgeChk` | filestore.go:6355 | PARTIAL | (implicit in Dispose) | .NET disposes wheel; Go stops and nils timer | +| `fs.expireMsgs` | filestore.go:6364 | PARTIAL | `FileStore.ExpireFromWheel` | .NET does basic seq removal; Go handles: negative TTL (never-expire), SDM processing, THW-based TTL expiry, sorted removal for SDM, age delta recalculation | +| `fs.shouldProcessSdm` | filestore.go:6482 | MISSING | -- | Subject Delete Marker pending tracking | +| `fs.shouldProcessSdmLocked` | filestore.go:6489 | MISSING | -- | Locked SDM state check with rate limiting | +| `fs.handleRemovalOrSdm` | filestore.go:6522 | MISSING | -- | Either removes via callback or emits SDM header message | +| `fs.runMsgScheduling` | filestore.go:6543 | MISSING | -- | Background message scheduling with repeating schedule support | +| `fs.checkAndFlushLastBlock` | filestore.go:6581 | PARTIAL | `FileStore.FlushAllPending` | .NET flushes active block; Go also handles rebuild state on lost data | +| `fs.checkMsgs` | filestore.go:6598 | MISSING | -- | Full checksum verification scan of all blocks | +| `mb.enableForWriting` | filestore.go:6622 | NOT_APPLICABLE | -- | Go-specific: open file descriptor, spin up flusher; .NET MsgBlock always has FileStream open | +| `mb.writeTombstone` | filestore.go:6646 | PARTIAL | `MsgBlock.WriteSkip` | .NET writes skip/tombstone via WriteSkip; Go uses tbit flag on sequence | +| `mb.writeTombstoneNoFlush` | filestore.go:6652 | MISSING | -- | Tombstone write without flush (used in bulk operations) | +| `mb.writeMsgRecord` | filestore.go:6660 | PORTED | `MsgBlock.Write` / `MsgBlock.WriteAt` | .NET has both auto-seq and explicit-seq variants | +| `mb.writeMsgRecordLocked` | filestore.go:6669 | PARTIAL | `MsgBlock.WriteAt` (under lock) | .NET writes record + updates index/cache; Go handles: per-subject fss tracking, elastic pointer strengthening, buffer pool management, HighwayHash checksum, tombstone accounting, flush-in-place, flusher kick | +| `mb.pendingWriteSize` | filestore.go:6821 | MISSING | -- | Go tracks pending = cache.buf - cache.wp; .NET writes synchronously | +| `mb.pendingWriteSizeLocked` | filestore.go:6831 | MISSING | -- | Locked version | +| `mb.closeFDs` | filestore.go:6843 | PARTIAL | `MsgBlock.Dispose` | .NET disposes FileStream; Go checks pending data first | +| `mb.closeFDsLocked` | filestore.go:6849 | PARTIAL | `MsgBlock.Dispose` | .NET unconditional; Go returns error if pending data | +| `mb.closeFDsLockedNoCheck` | filestore.go:6857 | PARTIAL | `MsgBlock.Dispose` | .NET disposes; Go just closes mfd | +| `mb.bytesPending` | filestore.go:6867 | NOT_APPLICABLE | -- | Go write-cache pending slice; .NET writes synchronously (no pending buffer) | +| `mb.blkSize` | filestore.go:6885 | PORTED | `MsgBlock.BytesUsed` | Returns total bytes including deleted | +| `mb.updateAccounting` | filestore.go:6897 | PARTIAL | (inline in MsgBlock.Write/WriteAt) | .NET updates first/last/totalWritten; Go handles ebit, atomic first/last, rbytes/bytes split | +| `fs.checkLastBlock` | filestore.go:6920 | PARTIAL | `FileStore.EnsureActiveBlock` | .NET rotates on IsSealed; Go checks rbytes + rl > BlockSize | +| `fs.writeMsgRecord` | filestore.go:6933 | PARTIAL | `FileStore.StoreMsg` / `StoreRawMsg` | .NET writes via MsgBlock; Go checks msg too large, increments dirty counter | +| `fs.writeTombstone` | filestore.go:6956 | MISSING | -- | fs-level tombstone with block size enforcement | +| `fs.writeTombstoneNoFlush` | filestore.go:6968 | MISSING | -- | No-flush variant | +| `mb.recompressOnDiskIfNeeded` | filestore.go:6978 | MISSING | -- | On-disk block recompression (S2) after block seal | + +| Status | Count | +| PORTED | 14 | +| PARTIAL | 27 | +| MISSING | 37 | +| NOT_APPLICABLE | 2 | +| **Total** | **80** | + + + + + +| `mb.recompressOnDiskIfNeeded` (tail) | filestore.go:7000-7027 | PARTIAL | `FileStore.TransformForPersist` | .NET applies compression at write time (S2Codec); no post-hoc re-compression of existing blocks | +| `mb.atomicOverwriteFile` | filestore.go:7030-7111 | MISSING | — | Go writes block to temp file then renames; .NET uses direct `RandomAccess.Write` with no atomic overwrite | +| `mb.decompressIfNeeded` | filestore.go:7114-7132 | PORTED | `FileStore.RestorePayload` / `S2Codec.Decompress` | .NET decompresses via S2Codec in RestorePayload; metadata parsing differs | +| `mb.encryptOrDecryptIfNeeded` | filestore.go:7135-7145 | PARTIAL | `AeadEncryptor.Encrypt/Decrypt` | .NET uses AEAD (ChaCha20/AES-GCM) not XOR stream cipher; no block-level BEK regeneration | +| `mb.ensureRawBytesLoaded` | filestore.go:7148-7163 | MISSING | — | .NET MsgBlock tracks `_writeOffset` directly; no lazy raw-bytes stat from disk | +| `fs.syncBlocks` | filestore.go:7166-7291 | MISSING | — | No periodic block sync timer; .NET uses `FileStream.Flush(flushToDisk: true)` on demand | +| `fs.selectMsgBlock` | filestore.go:7296-7299 | PORTED | `FileStore.FindBlockForSequence` | Binary search over `_blocks` by sequence range | +| `fs.selectMsgBlockWithIndex` | filestore.go:7302-7343 | PORTED | `FileStore.FindBlockForSequence` / `FindFirstBlockAtOrAfter` | Combined linear/binary search; .NET uses pure binary search | +| `fs.selectMsgBlockForStart` (by time) | filestore.go:7348-7372 | MISSING | — | No time-based block selection; `GetSeqFromTime` does linear scan over `_messages` | +| `mb.indexCacheBuf` | filestore.go:7376-7557 | MISSING | — | Go builds index+FSS from raw block buffer; .NET uses `Dictionary` index from `RebuildIndex` (much simpler) | +| `mb.flushPendingMsgs` | filestore.go:7560-7572 | PARTIAL | `MsgBlock.Flush` | .NET calls `_file.Flush(flushToDisk: true)`; Go has pending write buffer with encryption and rebuild-on-error | +| `mb.writeAt` | filestore.go:7577-7588 | PORTED | `RandomAccess.Write` in `MsgBlock.Write/WriteAt` | .NET uses `RandomAccess.Write` for positional I/O; Go uses `dios` semaphore for I/O gating | +| `mb.flushPendingMsgsLocked` | filestore.go:7592-7694 | PARTIAL | `MsgBlock.Flush` | Go has complex pending-write logic with encryption, rebuild-on-error, sync-always mode; .NET is simpler flush | +| `mb.clearLoading` | filestore.go:7697-7699 | NOT_APPLICABLE | — | Go loading-guard flag; .NET has no equivalent concurrent loading concern | +| `mb.loadMsgs` / `mb.loadMsgsWithLock` | filestore.go:7703-7885 | PARTIAL | `MsgBlock.RebuildIndex` / `MsgBlock.Read` | Go loads full block into cache from disk with decrypt/decompress; .NET reads on-demand per-record | +| `mb.cacheAlreadyLoaded` / `mb.cacheNotLoaded` | filestore.go:7711-7724 | NOT_APPLICABLE | — | Go weak-reference cache management; .NET uses simple `Dictionary?` cache | +| `mb.fssNotLoaded` | filestore.go:7728-7730 | NOT_APPLICABLE | — | Go per-subject state tree tracking; .NET has no block-level FSS | +| `mb.openBlock` | filestore.go:7734-7740 | NOT_APPLICABLE | — | Go opens block file with dios semaphore; .NET holds `FileStream` open for block lifetime | +| `mb.loadBlock` | filestore.go:7745-7791 | PARTIAL | `MsgBlock.RebuildIndex` (disk read) | Go loads entire block file into pooled buffer; .NET reads record-by-record during recovery | +| `mb.fetchMsg` / `mb.fetchMsgNoCopy` / `mb.fetchMsgEx` | filestore.go:7890-7940 | PORTED | `MsgBlock.Read` | .NET does cache-then-disk lookup; Go has copy/no-copy variants and expiry tracking | +| Error sentinels (`errNoCache`, `errDeletedMsg`, etc.) | filestore.go:7943-7959 | PARTIAL | Exceptions in `MsgBlock.Read` / `FileStore.LoadMsg` | .NET uses `null` returns and `KeyNotFoundException` instead of sentinel errors | +| Bit constants (`cbit`, `dbit`, `hbit`, `ebit`, `tbit`) | filestore.go:7973-7983 | PARTIAL | `MessageRecord.Deleted` flag | Go uses bit-packed flags in seq/rl fields; .NET uses explicit `Deleted` bool in `MessageRecord` | +| `mb.cacheLookup` / `mb.cacheLookupNoCopy` / `mb.cacheLookupEx` | filestore.go:7988-8096 | PARTIAL | `MsgBlock.Read` (cache path) | .NET checks `_cache` dictionary then falls back to disk; Go has full slot-based index lookup with hash verification | +| `fs.sizeForSeq` | filestore.go:8101-8112 | MISSING | — | No per-sequence size calculation helper | +| `fs.msgForSeq` / `fs.msgForSeqLocked` | filestore.go:8117-8160 | PORTED | `FileStore.LoadMsg` | .NET does hash-map lookup then binary-search block fallback | +| `mb.msgFromBuf` / `mb.msgFromBufNoCopy` / `mb.msgFromBufEx` | filestore.go:8165-8300 | PARTIAL | `MessageRecord.Decode` | Go parses wire-format from raw buffer with header support; .NET uses `MessageRecord.Decode` with simpler format | +| `fs.LoadMsg` | filestore.go:8308-8350 | PORTED | `FileStore.LoadMsg` | Both implement sequence-based message loading with block selection | +| `fs.LoadLastMsg` | filestore.go:8352-8400 | PORTED | `FileStore.LoadLastMsg` | Both support subject-filtered last-message lookup | +| `mb.firstMatching` | filestore.go:8402-8470 | MISSING | — | Go's per-block first-matching scan with FSS optimization; .NET filters `_messages` dictionary | +| `mb.prevMatchingMulti` | filestore.go:~8470-8500 | MISSING | — | Go's per-block reverse multi-filter scan; .NET has no equivalent | +| `fs.LoadNextMsg` | filestore.go:8508-8577 | PORTED | `FileStore.LoadNextMsg` | Both support filtered forward scan; Go uses psim skip-ahead, .NET uses LINQ | +| `fs.LoadPrevMsg` | filestore.go:8579-8631 | PORTED | `FileStore.LoadPrevMsg` | Both walk backward from start; .NET iterates `_messages` dictionary | +| `fs.LoadPrevMsgMulti` | filestore.go:8633-8670 | MISSING | — | No multi-filter reverse load in .NET | +| `fs.Type` | filestore.go:8673-8675 | PORTED | `FileStore.Type()` | Returns `StorageType.File` | +| `fs.numSubjects` | filestore.go:8679-8681 | PARTIAL | `FileStore.State()` counts subjects | .NET counts in `State()` method; no dedicated `numSubjects` helper | +| `fs.numConsumers` | filestore.go:8684-8688 | MISSING | — | No consumer count tracking in FileStore; Go uses `cmu`+`cfs` | +| `fs.FastState` | filestore.go:8692-8711 | PORTED | `FileStore.FastState` | Both populate minimal state struct without allocating deleted arrays | +| `fs.State` | filestore.go:8714-8756 | PORTED | `FileStore.State` | Both return full state with deleted sequences and subject counts | +| `fs.Utilization` | filestore.go:8758-8768 | MISSING | — | No total/reported bytes utilization tracking across blocks | +| `fileStoreMsgSizeRaw` / `fileStoreMsgSize` / `fileStoreMsgSizeEstimate` | filestore.go:8770-8785 | MISSING | — | Go wire-format size calculation; .NET uses `MessageRecord.Encode` length | +| `fs.ResetState` | filestore.go:8788-8794 | PORTED | `FileStore.ResetState` | .NET is no-op (re-derives from blocks on construction); Go clears scheduling inflight | +| `mb.sinceLastActivity` | filestore.go:8797-8812 | MISSING | — | Go tracks lwts/lrts/llts/lsts for cache expiry timing; .NET uses `WriteCacheManager` TTL | +| `mb.sinceLastWriteActivity` | filestore.go:8816-8825 | MISSING | — | No separate write-activity tracking | +| `checkNewHeader` | filestore.go:8827-8833 | MISSING | — | Go index file magic/version check; .NET uses JSON for stream state | +| `mb.readIndexInfo` | filestore.go:8836-8935 | MISSING | — | Go reads binary index (.idx) files with varint encoding; .NET rebuilds from block scan | +| `fs.cacheLoads` | filestore.go:8938-8948 | MISSING | — | Cache load counter; no equivalent metric in .NET | +| `fs.cacheSize` | filestore.go:8951-8971 | MISSING | — | Cache size tracking; .NET `WriteCacheManager.TotalCachedBytes` is approximate | +| `fs.dmapEntries` | filestore.go:8974-8982 | MISSING | — | Total dmap entries across blocks; .NET `SequenceSet` per block but no aggregate | +| `subjectsEqual` / `subjectsAll` / `compareFn` | filestore.go:8986-9001 | PARTIAL | `FileStore.SubjectMatchesFilter` | .NET has unified `SubjectMatchesFilter`; Go has separate comparator functions | +| `fs.PurgeEx` | filestore.go:9005-9194 | PORTED | `FileStore.PurgeEx` | Both support subject-filtered purge with keep/seq limits; Go has tombstone writing, .NET is simpler | +| `fs.Purge` / `fs.purge` | filestore.go:9198-9313 | PORTED | `FileStore.Purge` | Both clear all messages; Go does atomic directory rename with tombstone, .NET does `_messages.Clear()` + block dispose | +| `fs.recoverPartialPurge` | filestore.go:9316-9337 | MISSING | — | Crash recovery for partial purge (ndir/pdir cleanup); .NET has no partial-purge recovery | +| `fs.Compact` / `fs.compact` | filestore.go:9342-9591 | PORTED | `FileStore.Compact` | Both remove messages below seq; Go has block-level compaction with encryption/compression, .NET is in-memory | +| `fs.reset` | filestore.go:9594-9641 | PARTIAL | `FileStore.Truncate(0)` | .NET `Truncate(0)` clears everything; Go `reset()` resets to zero state with psim/sdm cleanup | +| `mb.tombs` / `mb.tombsLocked` | filestore.go:9644-9682 | MISSING | — | Tombstone record scanning from raw block buffer; .NET has no tombstone concept | +| `mb.numPriorTombs` / `mb.numPriorTombsLocked` | filestore.go:9685-9738 | MISSING | — | Count tombstones for prior blocks; .NET has no tombstone tracking | +| `fs.Truncate` | filestore.go:9741-9925 | PORTED | `FileStore.Truncate` | Both remove messages above seq; Go has block-level truncation with tombstone writes, .NET is in-memory | +| `fs.lastSeq` | filestore.go:9927-9932 | PORTED | `FileStore._last` field | Direct field access vs Go lock-guarded helper | +| `fs.numMsgBlocks` | filestore.go:9935-9939 | PORTED | `FileStore.BlockCount` | Property returns `_blocks.Count` | +| `fs.addMsgBlock` | filestore.go:9943-9947 | PORTED | `FileStore.RotateBlock` (inline) | .NET adds to `_blocks` list and sets `_activeBlock` | +| `fs.removeMsgBlockFromList` | filestore.go:9951-9964 | PARTIAL | `FileStore.DisposeAllBlocks` | .NET only bulk-removes blocks; no individual block removal from list | +| `fs.removeMsgBlock` | filestore.go:9968-9988 | MISSING | — | Individual block removal with tombstone writes for last-seq preservation | +| `fs.forceRemoveMsgBlock` | filestore.go:9992-9995 | MISSING | — | Force removal (dirty close + remove from list) | +| `fs.purgeMsgBlock` | filestore.go:9999-10054 | MISSING | — | Per-block purge with per-subject tracking cleanup and scheduling metadata | +| `mb.dirtyClose` | filestore.go:10058-10062 | PARTIAL | `MsgBlock.Dispose` | .NET disposes file handle; Go clears cache, stops timers, closes channels | +| `mb.dirtyCloseWithRemove` | filestore.go:10065-10103 | PARTIAL | `MsgBlock.Dispose` + `FileStore.CleanBlockFiles` | Go closes and optionally removes files; .NET separates disposal from file cleanup | +| `mb.removeSeqPerSubject` | filestore.go:10107-10145 | MISSING | — | Per-block per-subject state tracking with lazy first/last recalculation | +| `mb.recalculateForSubj` | filestore.go:10149-10259 | MISSING | — | Scan cache buffer to recalculate first/last seq for a subject within a block | +| `fs.resetGlobalPerSubjectInfo` | filestore.go:10262-10271 | MISSING | — | Rebuild psim (global per-subject index map) from all blocks | +| `mb.resetPerSubjectInfo` / `mb.generatePerSubjectInfo` | filestore.go:10274-10339 | MISSING | — | Per-block subject tree (FSS) generation from block scan | +| `mb.ensurePerSubjectInfoLoaded` | filestore.go:10343-10356 | MISSING | — | Lazy FSS loading guard; .NET has no block-level subject tracking | +| `fs.populateGlobalPerSubjectInfo` | filestore.go:10360-10383 | MISSING | — | Populate psim from block FSS during recovery | +| `mb.close` | filestore.go:10386-10424 | PORTED | `MsgBlock.Dispose` | Both close file handles; Go also flushes pending, clears FSS, stops timers | +| `fs.closeAllMsgBlocks` | filestore.go:10426-10430 | PORTED | `FileStore.DisposeAllBlocks` | Both iterate and close/dispose all blocks | +| `fs.Delete` | filestore.go:10432-10511 | PORTED | `FileStore.Delete` | Both stop and remove all data; Go has async directory removal with `dios` gating | +| `fs.setSyncTimer` / `fs.cancelSyncTimer` | filestore.go:10514-10532 | MISSING | — | Periodic sync timer with jitter; .NET has no equivalent timer | +| Full state magic/version constants | filestore.go:10537-10541 | MISSING | — | Binary full-state format versioning; .NET uses JSON | +| `fs.flushStreamStateLoop` | filestore.go:10544-10571 | MISSING | — | Background goroutine for periodic state flush; .NET writes on `FlushAllPending` call | +| `timestampNormalized` | filestore.go:10574-10579 | PARTIAL | Inline in `FileStore` methods | .NET normalizes via `DateTimeOffset.ToUnixTimeMilliseconds` | +| `fs.writeFullState` / `fs._writeFullState` | filestore.go:10583-10831 | PARTIAL | `FileStore.WriteStreamStateAsync` | Go writes binary-encoded full state with psim, block metadata, checksums; .NET writes JSON snapshot | +| `fs.writeTTLState` | filestore.go:10833-10845 | MISSING | — | TTL state persistence; .NET uses in-memory `HashWheel` only | +| `fs.writeMsgSchedulingState` | filestore.go:10847-10859 | MISSING | — | Message scheduling state persistence | +| `fs.Stop` / `fs.stop` | filestore.go:10862-10940 | PORTED | `FileStore.Stop` | Both flush and close; Go also stops consumer stores, cancels timers, writes full state | +| `fs.streamSnapshot` | filestore.go:10945-10997+ | PARTIAL | `FileStore.CreateSnapshotAsync` | .NET creates JSON snapshot; Go creates S2-compressed tar archive with block files | + +| Status | Count | +| PORTED | 26 | +| PARTIAL | 19 | +| MISSING | 32 | +| NOT_APPLICABLE | 5 | +| DEFERRED | 0 | +| **Total** | **82** | + + +| `mb.dirtyClose()` | filestore.go:10058 | PARTIAL | `MsgBlock.Dispose()` | .NET disposes file handle; Go also stops cache timer, closes fds without flush, optionally removes files | +| `mb.dirtyCloseWithRemove(remove)` | filestore.go:10065 | PARTIAL | `MsgBlock.Dispose()` | .NET has no remove-on-close path; Go removes .blk and key files when `remove=true` | +| `mb.removeSeqPerSubject(subj, seq)` | filestore.go:10107 | MISSING | — | Per-subject index tracking within a block; .NET MsgBlock has no per-subject FSS tree | +| `mb.recalculateForSubj(subj, ss)` | filestore.go:10149 | MISSING | — | Scans cache to recalculate first/last seq for a subject within a block | +| `mb.resetPerSubjectInfo()` | filestore.go:10274 | MISSING | — | Clears and regenerates per-subject info for a block | +| `mb.generatePerSubjectInfo()` | filestore.go:10281 | MISSING | — | Builds per-subject SimpleState tree (fss) from block cache | +| `mb.ensurePerSubjectInfoLoaded()` | filestore.go:10343 | MISSING | — | Lazy-loads per-subject info when first needed | +| `mb.close(sync)` | filestore.go:10386 | PARTIAL | `MsgBlock.Dispose()` | .NET disposes only; Go flushes pending, clears cache, syncs fd, marks closed | + +| `fs.resetGlobalPerSubjectInfo()` | filestore.go:10262 | MISSING | — | Clears and rebuilds global PSIM (per-subject index map) from all blocks | +| `fs.populateGlobalPerSubjectInfo(mb)` | filestore.go:10360 | MISSING | — | Populates global PSIM from a single block's fss tree | + +| `fs.closeAllMsgBlocks(sync)` | filestore.go:10426 | PORTED | `FileStore.DisposeAllBlocks()` | .NET iterates blocks and disposes; Go supports optional sync | +| `fs.Delete(inline)` | filestore.go:10432 | PORTED | `FileStore.Delete(bool)` | .NET calls Stop + Directory.Delete; Go has more elaborate rename-then-remove, storage callback, purge directory handling | +| `fs.setSyncTimer()` | filestore.go:10514 | MISSING | — | Periodic sync timer for dirty blocks; .NET has no equivalent background sync timer | +| `fs.cancelSyncTimer()` | filestore.go:10527 | MISSING | — | Cancels the periodic sync timer | + +| `fullStateMagic` / `fullStateVersion` | filestore.go:10537 | MISSING | — | Binary state file versioning constants | +| `fs.flushStreamStateLoop(qch, done)` | filestore.go:10544 | PARTIAL | `FileStore.WriteStreamStateAsync()` | .NET has on-demand JSON state write; Go has a goroutine loop that periodically writes binary index.db | +| `timestampNormalized(t)` | filestore.go:10574 | MISSING | — | Utility: returns UnixNano or 0 for zero time | +| `fs.writeFullState()` | filestore.go:10583 | PARTIAL | `FileStore.WriteStreamStateAsync()` | .NET writes JSON snapshot; Go writes binary format with PSIM, per-block dmaps, block checksums, encryption, highway hash verification | +| `fs.forceWriteFullState()` | filestore.go:10588 | PARTIAL | `FileStore.WriteStreamStateAsync()` | .NET has no force vs non-force distinction; Go skips if not dirty (non-forced) or too complex | +| `fs._writeFullState(force)` | filestore.go:10599 | PARTIAL | `FileStore.WriteStreamStateAsync()` | .NET is a simplified JSON version; Go has full binary encoding with PSIM, block iteration, encryption, consistency checking | +| `fs.writeTTLState()` | filestore.go:10833 | MISSING | — | Persists TTL tracking state to ttl.state file | +| `fs.writeMsgSchedulingState()` | filestore.go:10847 | MISSING | — | Persists message scheduling state to disk | + +| `fs.Stop()` | filestore.go:10862 | PORTED | `FileStore.Stop()` | .NET flushes active block and disposes; Go also writes full state, cancels timers, stops consumer stores, unregisters ATS | +| `fs.stop(delete, writeState)` | filestore.go:10867 | PARTIAL | `FileStore.Stop()` / `FileStore.Delete()` | .NET has separate Stop/Delete; Go combines into one method with flags for delete and writeState | + +| `fs.streamSnapshot(w, includeConsumers, errCh)` | filestore.go:10945 | MISSING | — | Streams tar+S2 snapshot through a pipe; .NET has `StreamSnapshotService` at a higher level but no block-level streaming snapshot | +| `fs.Snapshot(deadline, checkMsgs, includeConsumers)` | filestore.go:11129 | MISSING | — | Creates `SnapshotResult` with pipe reader; .NET `CreateSnapshotAsync` in IStreamStore returns byte[] | + +| `fs.fileStoreConfig()` | filestore.go:11171 | NOT_APPLICABLE | — | Simple getter under lock; .NET options are immutable | +| `fs.readLockAllMsgBlocks()` | filestore.go:11179 | NOT_APPLICABLE | — | Go RWMutex pattern; .NET uses per-block RWLockSlim | +| `fs.readUnlockAllMsgBlocks()` | filestore.go:11187 | NOT_APPLICABLE | — | Go RWMutex pattern; .NET uses per-block RWLockSlim | + +| `fs.EncodedStreamState(failed)` | filestore.go:11194 | PARTIAL | `FileStore.EncodedStreamState(ulong)` | .NET returns empty array (stub); Go returns full binary encoding with deleted blocks | +| `fs.deleteBlocks()` | filestore.go:11253 | MISSING | — | Returns DeleteBlocks (ranges + avl seqsets) representing interior deletes between and within blocks | +| `fs.deleteMap()` | filestore.go:11293 | MISSING | — | Unions all block dmaps into a single SequenceSet | +| `fs.SyncDeleted(dbs)` | filestore.go:11313 | MISSING | — | Synchronises deleted state from a peer's DeleteBlocks — used for NRG cluster replication | + +| `fs.ConsumerStore(name, created, cfg)` | filestore.go:11379 | PARTIAL | `FileStore.ConsumerStore(name, created, cfg)` | .NET creates ConsumerFileStore with state file; Go also handles encryption key setup, meta file writing, cipher conversion, memory-storage override | + +| `o.convertCipher()` | filestore.go:11511 | MISSING | — | Converts consumer state between ChaCha and AES ciphers | +| `o.kickFlusher()` | filestore.go:11570 | PORTED | (implicit via `_dirty` flag + background flusher) | .NET uses periodic polling; Go uses channel-based kick | +| `o.setInFlusher()` | filestore.go:11581 | PORTED | `ConsumerFileStore.InFlusher` property | | +| `o.clearInFlusher()` | filestore.go:11588 | PORTED | `ConsumerFileStore.RunFlusherAsync()` finally block | | +| `o.inFlusher()` | filestore.go:11595 | PORTED | `ConsumerFileStore.InFlusher` | | +| `o.flushLoop(fch, qch)` | filestore.go:11602 | PORTED | `ConsumerFileStore.RunFlusherAsync()` | .NET uses Task.Delay polling; Go uses channel-based select with minTime throttle | +| `o.SetStarting(sseq)` | filestore.go:11658 | PARTIAL | `ConsumerFileStore.SetStarting(sseq)` | .NET sets AckFloor only; Go also sets Delivered.Stream, AckFloor.Stream, encodes + writes to disk immediately | +| `o.UpdateStarting(sseq)` | filestore.go:11671 | PARTIAL | `ConsumerFileStore.UpdateStarting(sseq)` | .NET sets AckFloor only; Go checks sseq > Delivered.Stream, handles AckNone policy, kicks flusher | +| `o.Reset(sseq)` | filestore.go:11687 | PORTED | `ConsumerFileStore.Reset(sseq)` | | +| `o.HasState()` | filestore.go:11695 | PARTIAL | `ConsumerFileStore.HasState()` | .NET checks in-memory only; Go also checks disk file existence | +| `o.UpdateDelivered(dseq, sseq, dc, ts)` | filestore.go:11708 | PARTIAL | `ConsumerFileStore.UpdateDelivered(...)` | .NET has basic pending/redelivery tracking; Go has more nuanced handling: skips if dseq <= AckFloor, updates existing pending entries, respects MaxDeliver, stores dc-1 in redelivered | +| `o.UpdateAcks(dseq, sseq)` | filestore.go:11777 | PARTIAL | `ConsumerFileStore.UpdateAcks(dseq, sseq)` | .NET has simplified ack floor advancement; Go handles AckAll policy separately, uses original dseq from pending, walks forward for floor advancement | +| `o.EncodedState()` | filestore.go:11857 | PORTED | `ConsumerFileStore.EncodedState()` | | +| `o.encodeState()` | filestore.go:11863 | PORTED | `ConsumerStateCodec.Encode()` | | +| `o.UpdateConfig(cfg)` | filestore.go:11872 | MISSING | — | Updates consumer config and rewrites meta file | +| `o.Update(state)` | filestore.go:11883 | PARTIAL | `ConsumerFileStore.Update(state)` | .NET does direct replacement; Go validates ranges, deep-copies pending/redelivered, checks for outdated updates | +| `o.encryptState(buf)` | filestore.go:11932 | MISSING | — | AEAD encryption of consumer state | +| `dios` semaphore + `init()` | filestore.go:11948 | NOT_APPLICABLE | — | Go disk I/O semaphore to limit OS thread blocking; .NET uses async I/O natively | +| `o.writeState(buf)` | filestore.go:11969 | PARTIAL | `ConsumerFileStore.FlushState()` | .NET writes synchronously with File.WriteAllBytes; Go has encryption, writing flag, dios semaphore, atomic write | +| `o.updateConfig(cfg)` | filestore.go:12005 | MISSING | — | Internal config update for ephemeral recovery | +| `o.writeConsumerMeta()` | filestore.go:12014 | MISSING | — | Writes meta.json + meta.sum with optional encryption and key file | +| `checkConsumerHeader(hdr)` | filestore.go:12070 | PORTED | `ConsumerStateCodec.Decode()` (inline check) | Magic byte and version check done inline in .NET decode | +| `o.copyPending()` | filestore.go:12082 | PORTED | `ConsumerFileStore.State()` (inline copy) | Done inline in State() method | +| `o.copyRedelivered()` | filestore.go:12090 | PORTED | `ConsumerFileStore.State()` (inline copy) | Done inline in State() method | +| `o.Type()` | filestore.go:12099 | PORTED | `ConsumerFileStore.Type()` | | +| `o.State()` | filestore.go:12103 | PORTED | `ConsumerFileStore.State()` | | +| `o.BorrowState()` | filestore.go:12109 | PORTED | `ConsumerFileStore.BorrowState()` | | +| `o.stateWithCopy(doCopy)` | filestore.go:12113 | PARTIAL | `ConsumerFileStore.State()` / `BorrowState()` | .NET always returns from memory; Go falls back to disk read with decryption | +| `o.stateWithCopyLocked(doCopy)` | filestore.go:12120 | PARTIAL | `ConsumerFileStore.State()` / `BorrowState()` | .NET has no encrypted disk fallback | +| `o.loadState()` | filestore.go:12203 | PORTED | `ConsumerFileStore` constructor | .NET loads state in constructor from disk | +| `decodeConsumerState(buf)` | filestore.go:12216 | PORTED | `ConsumerStateCodec.Decode()` | Full Go wire format compatibility including v1 and v2 | +| `o.Stop()` | filestore.go:12328 | PORTED | `ConsumerFileStore.Stop()` | | +| `o.waitOnFlusher()` | filestore.go:12368 | PARTIAL | `ConsumerFileStore.Stop()` (Task.Wait) | .NET uses Task.Wait with timeout; Go polls with sleep | +| `o.Delete()` | filestore.go:12383 | PORTED | `ConsumerFileStore.Delete()` | | +| `o.StreamDelete()` | filestore.go:12387 | PORTED | `ConsumerFileStore.StreamDelete()` | | +| `o.delete(streamDeleted)` | filestore.go:12391 | PARTIAL | `ConsumerFileStore.Delete()` | .NET always deletes file; Go skips dir removal if stream was already deleted | + +| `fs.AddConsumer(o)` | filestore.go:12423 | MISSING | — | Registers a ConsumerStore with the fileStore's consumer list | +| `fs.RemoveConsumer(o)` | filestore.go:12430 | MISSING | — | Removes a ConsumerStore from the fileStore's consumer list | + +| `CompressionInfo.MarshalMetadata()` | filestore.go:12451 | MISSING | — | Encodes compression metadata header (algorithm + original size) | +| `CompressionInfo.UnmarshalMetadata(b)` | filestore.go:12459 | MISSING | — | Decodes compression metadata header | +| `StoreCompression.Compress(buf)` | filestore.go:12477 | PORTED | `S2Codec.CompressWithTrailingChecksum()` | .NET uses IronSnappy; Go uses S2 stream writer | +| `StoreCompression.Decompress(buf)` | filestore.go:12517 | PORTED | `S2Codec.DecompressWithTrailingChecksum()` | .NET uses IronSnappy; Go uses S2 stream reader | + +| `fs.writeFileWithOptionalSync(name, data, perm)` | filestore.go:12549 | PORTED | `AtomicFileWriter.WriteAtomicallyAsync()` | .NET uses temp-file + rename pattern; Go identical pattern + optional O_SYNC + dios semaphore | +| `writeFileWithSync(name, data, perm)` | filestore.go:12553 | PARTIAL | `AtomicFileWriter.WriteAtomicallyAsync()` | .NET always async, no O_SYNC; Go forces sync | +| `writeAtomically(name, data, perm, sync)` | filestore.go:12557 | PORTED | `AtomicFileWriter.WriteAtomicallyAsync()` | Core implementation ported; .NET lacks O_SYNC flag and directory fsync | + +| `validatePathExists(path, dir)` | dirstore.go:40 | MISSING | — | Path validation utility | +| `validateDirPath(path)` | dirstore.go:68 | MISSING | — | Directory path validation | +| `JWTChanged` callback type | dirstore.go:73 | MISSING | — | Change notification delegate | +| `DirJWTStore` struct | dirstore.go:77 | MISSING | — | JWT store with sharding, expiration, LRU | +| `newDir(dirPath, create)` | dirstore.go:89 | MISSING | — | Directory creation helper | +| `NewImmutableDirJWTStore(...)` | dirstore.go:110 | MISSING | — | Read-only JWT store factory | +| `NewDirJWTStore(...)` | dirstore.go:121 | MISSING | — | Writable JWT store factory | +| `deleteType` enum | dirstore.go:133 | MISSING | — | NoDelete / RenameDeleted / HardDelete | +| `NewExpiringDirJWTStore(...)` | dirstore.go:150 | MISSING | — | JWT store with TTL, LRU eviction, limits | +| `store.IsReadOnly()` | dirstore.go:193 | MISSING | — | | +| `store.LoadAcc(publicKey)` | dirstore.go:197 | MISSING | — | Load account JWT | +| `store.SaveAcc(publicKey, jwt)` | dirstore.go:201 | MISSING | — | Save account JWT | +| `store.LoadAct(hash)` | dirstore.go:205 | MISSING | — | Load activation JWT | +| `store.SaveAct(hash, jwt)` | dirstore.go:209 | MISSING | — | Save activation JWT | +| `store.Close()` | dirstore.go:213 | MISSING | — | Close expiration tracker | +| `store.Pack(maxJWTs)` | dirstore.go:223 | MISSING | — | Pack JWTs for replication | +| `store.PackWalk(maxJWTs, cb)` | dirstore.go:267 | MISSING | — | Streaming pack with callback | +| `store.Merge(pack)` | dirstore.go:318 | MISSING | — | Merge packed JWTs into store | +| `store.Reload()` | dirstore.go:339 | MISSING | — | Reload JWTs from disk | +| `store.pathForKey(publicKey)` | dirstore.go:377 | MISSING | — | Compute file path with optional sharding | +| `store.load(publicKey)` | dirstore.go:395 | MISSING | — | Load JWT from disk | +| `store.write(path, publicKey, jwt)` | dirstore.go:412 | MISSING | — | Write JWT with hash tracking, LRU eviction | +| `store.delete(publicKey)` | dirstore.go:449 | MISSING | — | Delete JWT (rename or hard delete) | +| `store.save(publicKey, jwt)` | dirstore.go:478 | MISSING | — | Save JWT with change callback | +| `store.saveIfNewer(publicKey, jwt)` | dirstore.go:506 | MISSING | — | Save only if JWT is newer | +| `xorAssign(lVal, rVal)` | dirstore.go:549 | MISSING | — | XOR hash helper | +| `store.Hash()` | dirstore.go:556 | MISSING | — | Return XOR hash of all indexed JWTs | +| `jwtItem` struct | dirstore.go:567 | MISSING | — | Priority queue item | +| `expirationTracker` struct | dirstore.go:575 | MISSING | — | Heap + LRU + hash tracker | +| `expirationTracker` heap methods | dirstore.go:587–619 | MISSING | — | Len, Less, Swap, Push, Pop | +| `pq.updateTrack(publicKey)` | dirstore.go:621 | MISSING | — | Update expiration + LRU position | +| `pq.unTrack(publicKey)` | dirstore.go:635 | MISSING | — | Remove from tracker | +| `pq.track(publicKey, hash, jwt)` | dirstore.go:643 | MISSING | — | Add/update tracking entry | +| `pq.close()` | dirstore.go:672 | MISSING | — | Stop expiration goroutine | +| `store.startExpiring(...)` | dirstore.go:680 | MISSING | — | Start background expiration ticker | + + +| Status | Count | +| PORTED | 22 | +| PARTIAL | 23 | +| MISSING | 24 | +| NOT_APPLICABLE | 4 | +| **Total** | **73** | + +| Status | Count | +| PORTED | 0 | +| PARTIAL | 0 | +| MISSING | 33 | +| NOT_APPLICABLE | 0 | +| **Total** | **33** | + +| Status | Count | +| PORTED | 22 | +| PARTIAL | 23 | +| MISSING | 57 | +| NOT_APPLICABLE | 4 | +| **Total** | **106** | + + + + + +| `jetStreamCluster` struct | :44-84 | PARTIAL | `JetStreamMetaGroup` in `Cluster/JetStreamMetaGroup.cs` | .NET has meta group tracking (streams map, inflight, peer management) but missing: `streamResults`/`consumerResults` subscriptions, `stepdown`/`peerRemove`/`peerStreamMove`/`peerStreamCancelMove` subscriptions, `qch`/`stopped` channels, `lastMetaSnapTime`/`lastMetaSnapDuration` monitoring fields | +| `inflightStreamInfo` struct | :87-91 | PORTED | `InflightInfo` record in `JetStreamMetaGroup.cs:1016` | Mapped to a record with `OpsCount`, `Deleted`, `Assignment` | +| `inflightConsumerInfo` struct | :94-98 | PORTED | `InflightInfo` record in `JetStreamMetaGroup.cs:1016` | Shared record type for both stream and consumer inflight | +| `peerRemoveInfo` struct | :101-106 | MISSING | — | Peer-remove reply tracking for responding after quorum | +| `Placement` struct | :109-113 | PORTED | `PlacementPolicy` in `Cluster/PlacementEngine.cs:229-243` | .NET adds `ExcludeTags` and `UniqueTag` beyond Go's Cluster+Tags | +| `entryOp` type + constants | :116-150 | PARTIAL | `MetaEntryType` enum in `JetStreamMetaGroup.cs:967-990` | .NET has 7 values (StreamCreate, StreamDelete, ConsumerCreate, ConsumerDelete, PeerAdd, PeerRemove, Unknown). Missing granular stream ops: `streamMsgOp`, `purgeStreamOp`, `deleteMsgOp`, `updateDeliveredOp`, `updateAcksOp`, `updateSkipOp`, `compressedStreamMsgOp`, `deleteRangeOp`, `batchMsgOp`, `batchCommitMsgOp`, `resetSeqOp`, `addPendingRequest`, `removePendingRequest` | +| `raftGroup` struct | :154-163 | PORTED | `RaftGroup` in `Cluster/ClusterAssignmentTypes.cs:9-105` | Full port with `Name`, `Peers`, `Storage`, `Cluster`, `Preferred`, plus .NET-specific `DesiredReplicas`, `QuorumSize`, `IsMember`, `SetPreferred`, `RemovePeer`, `AddPeer` helpers | +| `streamAssignment` struct | :166-184 | PORTED | `StreamAssignment` in `ClusterAssignmentTypes.cs:111-139` | Core fields ported. Missing: `Client`, `Restore`, `err`, `unsupported`, `resetting` fields | +| `unsupportedStreamAssignment` struct | :186-247 | MISSING | — | Entire unsupported stream assignment subsystem (info sub, handler) | +| `consumerAssignment` struct | :250-266 | PORTED | `ConsumerAssignment` in `ClusterAssignmentTypes.cs:145-164` | Core fields ported. Missing: `Client`, `State`, `err`, `unsupported` fields | +| `unsupportedConsumerAssignment` struct | :268-330 | MISSING | — | Entire unsupported consumer assignment subsystem | +| `writeableConsumerAssignment` struct | :332-340 | MISSING | — | Serialization DTO for consumer assignments in snapshots | +| `streamPurge` struct | :343-350 | MISSING | — | Replicated purge operation struct | +| `streamMsgDelete` struct | :353-360 | MISSING | — | Replicated message delete operation struct | +| Constants (`defaultStoreDirName`, etc.) | :362-367 | PARTIAL | — | No explicit constants in .NET; `defaultMetaGroupName` logic is embedded | + +| `trackedJetStreamServers()` | :370-385 | MISSING | — | Mixed-mode node counting | +| `getJetStreamCluster()` | :387-399 | MISSING | — | Server-level accessor for JS cluster state | +| `JetStreamIsClustered()` | :401-403 | MISSING | — | Atomic boolean check | +| `JetStreamIsLeader()` | :405-407 | PARTIAL | `JetStreamMetaGroup.IsLeader()` | .NET has equivalent on meta group, not on Server | +| `JetStreamIsCurrent()` | :409-428 | MISSING | — | Meta RAFT currency check | +| `JetStreamSnapshotMeta()` | :430-451 | MISSING | — | Force meta snapshot | +| `JetStreamStepdownStream()` | :453-477 | PARTIAL | `ClusterControlApiHandlers.HandleStreamLeaderStepdown()` | .NET has API handler stub but missing actual RAFT node interaction | +| `JetStreamStepdownConsumer()` | :479-508 | PARTIAL | `ClusterControlApiHandlers.HandleConsumerLeaderStepdown()` | .NET has API handler stub but missing actual RAFT node interaction | +| `JetStreamSnapshotStream()` | :510-539 | MISSING | — | Force stream-level snapshot | +| `JetStreamClusterPeers()` | :541-568 | MISSING | — | Returns online JS peer names | +| `cc.isLeader()` | :571-577 | PORTED | `JetStreamMetaGroup.IsLeader()` | Equivalent logic | + +| `cc.isStreamCurrent()` | :582-618 | MISSING | — | Checks if stream is up-to-date via RAFT node + catchup state | +| `isStreamHealthy()` | :622-679 | MISSING | — | Comprehensive stream health check (R1 check, node skew, monitor running, catching up, healthy) | +| `isConsumerHealthy()` | :683-746 | MISSING | — | Comprehensive consumer health check | +| `subjectsOverlap()` | :751-766 | MISSING | — | Cross-cluster subject overlap detection for stream assignments | + +| `getJetStreamFromAccount()` | :768-784 | MISSING | — | Account-level JS accessor | +| `JetStreamIsStreamLeader()` (Server) | :786-794 | MISSING | — | Server-level stream leader check | +| `JetStreamIsStreamLeader()` (Account) | :796-804 | MISSING | — | Account-level stream leader check | +| `JetStreamIsStreamCurrent()` | :806-814 | MISSING | — | Server-level stream currency check | +| `JetStreamIsConsumerLeader()` (Account) | :816-824 | MISSING | — | Account-level consumer leader check | +| `JetStreamIsConsumerLeader()` (Server) | :826-834 | MISSING | — | Server-level consumer leader check | +| `enableJetStreamClustering()` | :836-862 | MISSING | — | Startup: validates cluster name, routes, calls setupMetaGroup | +| `isClustered()` | :866-869 | MISSING | — | Lock-safe clustered check | +| `isClusteredNoLock()` | :875-877 | MISSING | — | Lock-free atomic clustered check | +| `setupMetaGroup()` | :879-993 | MISSING | — | Creates meta RAFT group: WAL, filestore, bootstrap, observer mode, starts monitorCluster | +| `getMetaGroup()` | :995-1002 | MISSING | — | Returns meta RaftNode | +| `server()` | :1004-1007 | NOT_APPLICABLE | — | Trivial getter | + +| `isLeaderless()` | :1010-1026 | MISSING | — | Checks if meta group has no leader beyond lost-quorum interval | +| `isGroupLeaderless()` | :1029-1071 | MISSING | — | Checks if a specific raft group is leaderless | +| `JetStreamIsStreamAssigned()` | :1073-1086 | MISSING | — | Server-level stream assignment check | +| `streamAssigned()` (jsAccount) | :1089-1101 | MISSING | — | Account-level stream assignment check | +| `cc.isStreamAssigned()` | :1104-1121 | MISSING | — | Core stream assignment membership check | + +| `cc.isStreamLeader()` | :1124-1154 | MISSING | — | Checks if this node is leader for a stream's raft group | +| `cc.isConsumerLeader()` | :1157-1188 | MISSING | — | Checks if this node is leader for a consumer's raft group | +| `trackInflightStreamProposal()` | :1193-1209 | PORTED | `JetStreamMetaGroup.TrackInflightStreamProposal()` | Full equivalent with ops counting | +| `removeInflightStreamProposal()` | :1214-1229 | PORTED | `JetStreamMetaGroup.RemoveInflightStreamProposal()` | Full equivalent | +| `trackInflightConsumerProposal()` | :1234-1255 | PORTED | `JetStreamMetaGroup.TrackInflightConsumerProposal()` | Full equivalent | +| `removeInflightConsumerProposal()` | :1260-1280 | PORTED | `JetStreamMetaGroup.RemoveInflightConsumerProposal()` | Full equivalent | + +| `clusterQuitC()` | :1283-1290 | MISSING | — | Returns cluster quit channel | +| `clusterStoppedC()` | :1293-1300 | MISSING | — | Returns cluster stopped channel | +| `setMetaRecovering()` | :1303-1310 | MISSING | — | Sets meta recovery state | +| `clearMetaRecovering()` | :1313-1317 | MISSING | — | Clears meta recovery state | +| `isMetaRecovering()` | :1320-1324 | MISSING | — | Checks meta recovery state | +| `recoveryUpdates` struct | :1327-1333 | MISSING | — | Recovery-time diff tracking for streams/consumers | +| `ru.removeStream()` | :1335-1342 | MISSING | — | Recovery diff: track stream removal | +| `ru.addStream()` | :1344-1347 | MISSING | — | Recovery diff: track stream addition | +| `ru.updateStream()` | :1349-1352 | MISSING | — | Recovery diff: track stream update | +| `ru.removeConsumer()` | :1354-1364 | MISSING | — | Recovery diff: track consumer removal | +| `ru.addOrUpdateConsumer()` | :1366-1373 | MISSING | — | Recovery diff: track consumer add/update | + +| `checkForOrphans()` | :1378-1428 | MISSING | — | Post-recovery orphan stream/consumer cleanup | +| `getOrphans()` | :1433-1453 | MISSING | — | Returns orphaned streams and consumers not in cluster assignments | + +| `monitorCluster()` | :1455-1825 | PARTIAL | `JetStreamClusterMonitor.StartAsync()` in `Cluster/JetStreamClusterMonitor.cs` | .NET has basic channel-reading entry dispatch loop. Missing: compact/snapshot timer, leader check timer, health check timer, recovery state machine, snapshot scheduling (sync/async), leader change processing, cluster size checks, cold-boot mixed-mode adjustment | + +| `checkClusterSize()` | :1828-1869 | MISSING | — | Adjusts cluster size for mixed-mode deployments | +| `writeableStreamAssignment` struct | :1872-1879 | MISSING | — | Serialization DTO for snapshot encoding | +| `clusterStreamConfig()` | :1881-1888 | MISSING | — | Returns stream config from cluster assignment | +| `metaSnapshot()` | :1890-1895 | PARTIAL | `MetaSnapshotCodec.Encode()` | .NET has encoding but not the full `js.metaSnapshot()` wrapper | +| `applyMetaSnapshot()` | :1897-2028 | PARTIAL | `JetStreamClusterMonitor.ApplyMetaSnapshot()` + `JetStreamMetaGroup.ReplaceAllAssignments()` | .NET has basic snapshot apply via `ReplaceAllAssignments()`. Missing: the full diff algorithm (saAdd/saDel/saChk), consumer delta processing, recovery-aware branching, orphan cleanup post-snapshot | +| `decodeMetaSnapshot()` | :2031-2071 | PORTED | `MetaSnapshotCodec.Decode()` in `Cluster/MetaSnapshotCodec.cs` | S2-compressed JSON decode with version header | +| `encodeMetaSnapshot()` | :2075-2143 | PORTED | `MetaSnapshotCodec.Encode()` in `Cluster/MetaSnapshotCodec.cs` | S2-compressed JSON encode with version header, metrics tracking not ported | +| `collectStreamAndConsumerChanges()` | :2146-2237 | MISSING | — | Async snapshot pipeline: replay RAFT entries into assignment state machine | + +| `setStreamAssignmentRecovering()` | :2241-2251 | MISSING | — | Marks stream assignment as recovering | +| `setConsumerAssignmentRecovering()` | :2254-2263 | MISSING | — | Marks consumer assignment as recovering | +| `sa.copyGroup()` | :2267-2272 | MISSING | — | Deep-copies stream assignment with group | +| `ca.copyGroup()` | :2276-2281 | MISSING | — | Deep-copies consumer assignment with group | +| `sa.missingPeers()` | :2284-2286 | PORTED | `RaftGroup.IsUnderReplicated` in `ClusterAssignmentTypes.cs:36` | Property-based equivalent | +| `processAddPeer()` | :2290-2339 | PORTED | `JetStreamMetaGroup.ProcessAddPeer()` in `JetStreamMetaGroup.cs:790-819` | .NET adds new peer to under-replicated streams. Missing: cluster affinity check, consumer re-assignment proposals | +| `processRemovePeer()` | :2342-2393 | PARTIAL | `JetStreamMetaGroup.ProcessRemovePeer()` in `JetStreamMetaGroup.cs:827-841` | .NET removes from known peers and calls RemovePeerFromStream. Missing: self-removal advisory, DisableJetStream, consumer ephemeral cleanup | +| `removePeerFromStream()` | :2396-2400 | PORTED | `JetStreamMetaGroup.RemovePeerFromStream()` in `JetStreamMetaGroup.cs:849-862` | Equivalent logic | +| `removePeerFromStreamLocked()` | :2403-2439 | PARTIAL | `JetStreamMetaGroup.RemovePeerFromStream()` | .NET has basic peer removal + remap. Missing: RAFT proposals for stream + consumer reassignment | + +| `hasPeerEntries()` | :2442-2449 | MISSING | — | Checks if entries contain peer add/remove | +| `sa.recoveryKey()` | :2453-2458 | MISSING | — | Recovery key generation for stream assignments | +| `ca.streamRecoveryKey()` | :2460-2465 | MISSING | — | Recovery key for consumer's parent stream | +| `ca.recoveryKey()` | :2467-2472 | MISSING | — | Recovery key generation for consumer assignments | +| `applyMetaEntries()` | :2474-2608 | PARTIAL | `JetStreamClusterMonitor.ApplyMetaEntry()` in `JetStreamClusterMonitor.cs:108-146` | .NET dispatches on JSON "Op" field. Go dispatches on binary `entryOp` byte. Missing: catchup entry handling, snapshot entry handling, peer add/remove entry handling, recovery-aware branching, peer-remove reply delivery | + +| `rg.isMember()` | :2611-2621 | PORTED | `RaftGroup.IsMember()` in `ClusterAssignmentTypes.cs:48` | Equivalent | +| `rg.setPreferred()` | :2623-2656 | PARTIAL | `RaftGroup.SetPreferred()` in `ClusterAssignmentTypes.cs:55-61` | .NET requires explicit peer ID. Go version auto-selects from online peers | +| `createRaftGroup()` | :2659-2789 | MISSING | — | Full RAFT group creation: WAL setup, filestore/memstore, peer bootstrap, campaign, HA asset limits check | +| `mset.raftGroup()` | :2792-2802 | NOT_APPLICABLE | — | Trivial accessor | +| `mset.raftNode()` | :2804-2811 | NOT_APPLICABLE | — | Trivial accessor | +| `mset.removeNode()` | :2813-2820 | MISSING | — | Deletes RAFT node from stream | +| `genPeerInfo()` | :2824-2837 | MISSING | — | Helper for peer set splitting during migration | + +| `waitOnConsumerAssignments()` | :2842-2892 | MISSING | — | Waits for consumer assignments before proceeding (interest retention) | +| `monitorStream()` | :2895-3506 | PARTIAL | `StreamReplicaGroup` in `Cluster/StreamReplicaGroup.cs` | .NET has a simplified stream replica group model with election, propose, apply, snapshot. Missing: the full monitor loop (apply queue, leader change, compact timer, migration monitoring, direct access monitoring, interest state check, restore handling, scale-down logic, catchup error handling, `resetClusteredState` integration) | + +| `isMigrating()` | :3509-3531 | MISSING | — | Detects stream migration (group peers != config replicas) | +| `resetClusteredState()` | :3534-3637 | MISSING | — | Full clustered state reset: stepdown, delete/stop node, re-create stream + consumers | +| `isControlHdr()` | :3639-3641 | NOT_APPLICABLE | — | Trivial header check | + +| `applyStreamEntries()` | :3643-4000+ | PARTIAL | `StreamReplicaGroup.ApplyCommittedEntriesAsync()` in `StreamReplicaGroup.cs:228-269` | .NET dispatches on string-based command prefixes (smsg:, centry:, +peer:, -peer:). Go dispatches on binary `entryOp`. Missing: full `batchMsgOp`/`batchCommitMsgOp` batch apply logic, `deleteMsgOp` with erase/remove, `purgeStreamOp` with request replay, `EntrySnapshot` with `StreamReplicatedState`, `EntryRemovePeer` processing, recovering-aware error handling, `resetClusteredState` on error | + +| `encodeAddStreamAssignment()` | :8703+ (referenced) | PORTED | `AssignmentCodec.EncodeStreamAssignment()` in `Cluster/AssignmentCodec.cs:52-53` | JSON serialization | +| `decodeStreamAssignment()` | :8733+ (referenced) | PORTED | `AssignmentCodec.DecodeStreamAssignment()` in `Cluster/AssignmentCodec.cs:61-74` | JSON deserialization | +| `encodeAddConsumerAssignment()` | :9175+ (referenced) | PORTED | `AssignmentCodec.EncodeConsumerAssignment()` in `Cluster/AssignmentCodec.cs:87-88` | JSON serialization | +| `decodeConsumerAssignment()` | :9195+ (referenced) | PORTED | `AssignmentCodec.DecodeConsumerAssignment()` in `Cluster/AssignmentCodec.cs:96-109` | JSON deserialization | +| `encodeAddConsumerAssignmentCompressed()` | :9226+ (referenced) | PORTED | `AssignmentCodec.CompressIfLarge()` in `Cluster/AssignmentCodec.cs:131-141` | Snappy compression with sentinel byte | +| `decodeConsumerAssignmentCompressed()` | :9238+ (referenced) | PORTED | `AssignmentCodec.DecompressIfNeeded()` in `Cluster/AssignmentCodec.cs:151-157` | Snappy decompression | + +| `selectPeerGroup()` | :7212+ (referenced) | PORTED | `PlacementEngine.SelectPeerGroup()` in `Cluster/PlacementEngine.cs:34-97` | Full pipeline: cluster affinity, tag filter, HA limit split, weighted score sort, unique-tag constraint | +| `remapStreamAssignment()` | :7077+ (referenced) | PORTED | `JetStreamMetaGroup.RemapStreamAssignment()` in `JetStreamMetaGroup.cs:872-904` | Replacement peer selection with fallback shrink | + +| `jsClusteredStreamRequest()` | :7620+ (referenced) | PORTED | `ClusteredRequestProcessor` in `Api/ClusteredRequestProcessor.cs` | Channel-per-request pattern using TaskCompletionSource; register, wait, deliver, cancel-all | + +| Status | Count | +| PORTED | 25 | +| PARTIAL | 14 | +| MISSING | 48 | +| NOT_APPLICABLE | 4 | +| DEFERRED | 0 | +| **Total** | **91** | + + + + + +| `stream.skipBatchIfRecovering` | 4034-4066 | MISSING | -- | Batch recovery skip logic for stream RAFT apply; requires real RAFT + stream integration | +| `jetStream.applyStreamMsgOp` | 4068-4242 | PARTIAL | `StreamReplicaGroup.ApplyStreamMsgOp` | .NET has simplified Store/Remove/Purge enum but lacks S2 decompression, flow-control reply, lastSeq mismatch retry, inflight/counter tracking, preAck clearing | +| `Server.replicas` | 4246-4259 | MISSING | -- | Converts RAFT node peers to external PeerInfo with lag/active time | +| `jetStream.processStreamLeaderChange` | 4262-4365 | PARTIAL | `JetStreamMetaGroup.ProcessLeaderChange` | .NET has leader change event + inflight clearing but no dedupe-ID clearing, inflight map clearing, advisory sending, stream setLeader, or API response to client | +| `stream.shouldSendLostQuorum` | 4371-4379 | MISSING | -- | Throttled quorum-loss advisory gate | +| `Server.sendStreamLostQuorumAdvisory` | 4381-4414 | MISSING | -- | Stream lost-quorum advisory publication | +| `Server.sendStreamLeaderElectAdvisory` | 4416-4444 | MISSING | -- | Stream leader-elected advisory publication | +| `jetStream.streamAssignment` (lookup) | 4448-4458 | PORTED | `JetStreamMetaGroup.GetStreamAssignment` | Direct lookup by account+stream | +| `jetStream.streamAssignmentOrInflight` | 4462-4481 | PARTIAL | `JetStreamMetaGroup.IsStreamInflight` + `GetStreamAssignment` | Separate methods exist but no single combined lookup that returns the SA from inflight | +| `jetStream.streamAssignmentsOrInflightSeq` | 4485-4507 | MISSING | -- | Iterator over all stream assignments (applied + inflight) for an account | +| `jetStream.streamAssignmentsOrInflightSeqAllAccounts` | 4511-4538 | MISSING | -- | Iterator over all stream assignments across all accounts | +| `jetStream.processStreamAssignment` | 4540-4647 | PARTIAL | `JetStreamMetaGroup.ProcessStreamAssignment` | .NET validates and adds assignment but lacks account lookup, stream creation dispatch, raft group node transfer, unsupported stream detection, syncSubject bug check | +| `jetStream.processUpdateStreamAssignment` | 4650-4763 | PARTIAL | `JetStreamMetaGroup.ProcessUpdateStreamAssignment` | .NET updates config but lacks member check, raft node transfer, stream removal on non-member, unsupported handling | +| `Server.removeStream` | 4767-4794 | MISSING | -- | Removes stream from non-member server (stepdown, node delete, monitor quit, stop) | +| `jetStream.processClusterUpdateStream` | 4798-4940 | MISSING | -- | Full cluster stream update with scaling up/down, raft group creation, monitor start/stop, advisory response | +| `jetStream.processClusterCreateStream` | 4944-5213 | MISSING | -- | Full cluster stream create with raft group, monitor goroutine, restore handling, single-replica path | +| `jetStream.processStreamRemoval` | 5216-5264 | PARTIAL | `JetStreamMetaGroup.ProcessStreamRemoval` / `JetStreamClusterMonitor.ProcessStreamRemoval` | .NET removes assignment from meta state; lacks account lookup, created-time check, file cleanup | +| `jetStream.processClusterDeleteStream` | 5266-5360 | MISSING | -- | Full stream deletion: raft node delete, monitor shutdown, file cleanup, response sending | +| `jetStream.processConsumerAssignment` | 5362-5541 | PARTIAL | `JetStreamMetaGroup.ProcessConsumerAssignment` | .NET validates and adds to meta state; lacks member check, raft group transfer, state capture, unsupported handling, non-member removal with stepdown | +| `jetStream.processConsumerRemoval` | 5543-5590 | PARTIAL | `JetStreamMetaGroup.ProcessConsumerRemoval` | .NET removes from meta state; lacks account lookup, created-time check, disk cleanup dispatch | +| `consumerAssignmentResult` (struct) | 5592-5597 | MISSING | -- | Result struct for consumer assignment error forwarding | +| `jetStream.processClusterCreateConsumer` | 5600-5854 | MISSING | -- | Full consumer create: raft group, add/update consumer, scale-down handling, monitor start, snapshot send, config update response | +| `jetStream.processClusterDeleteConsumer` | 5856-5922 | MISSING | -- | Full consumer deletion: stop consumer, node delete, file cleanup, response | +| `jetStream.consumerAssignment` (lookup) | 5926-5931 | PORTED | `JetStreamMetaGroup.GetConsumerAssignment` | Lookup by account/stream/consumer | +| `jetStream.consumerAssignmentOrInflight` | 5935-5955 | PARTIAL | `JetStreamMetaGroup.IsConsumerInflight` + `GetConsumerAssignment` | Separate methods but no combined lookup returning the CA from inflight | +| `jetStream.consumerAssignmentsOrInflightSeq` | 5959-5986 | MISSING | -- | Iterator over consumer assignments (applied + inflight) for account/stream | +| `jsAccount.consumerAssigned` | 5989-5999 | MISSING | -- | Checks if this server has a consumer assigned | +| `jetStreamCluster.isConsumerAssigned` | 6003-6025 | MISSING | -- | Checks consumer membership in cluster | +| `consumer.streamAndNode` | 6028-6035 | NOT_APPLICABLE | -- | Go-specific accessor for consumer's stream + raft node | +| `consumer.replica` | 6039-6049 | NOT_APPLICABLE | -- | Go-specific replica count getter | +| `consumer.raftGroup` | 6051-6061 | NOT_APPLICABLE | -- | Go-specific raft group accessor | +| `consumer.clearRaftNode` | 6063-6070 | NOT_APPLICABLE | -- | Go-specific raft node clear | +| `consumer.raftNode` | 6072-6079 | NOT_APPLICABLE | -- | Go-specific raft node accessor | +| `jetStream.monitorConsumer` | 6081-6349 | MISSING | -- | Consumer RAFT monitor loop: apply entries, leader change, snapshot, migration monitoring, compaction | +| `jetStream.applyConsumerEntries` | 6351-6531 | PARTIAL | `StreamReplicaGroup.ApplyConsumerEntry` | .NET has simplified Ack/Nak/Deliver/Term/Progress enum tracking but lacks full op decoding (updateDeliveredOp, updateAcksOp, updateSkipOp, resetSeqOp, addPendingRequest, removePendingRequest), snapshot application, peer removal | +| `consumer.processReplicatedAck` | 6535-6585 | MISSING | -- | Full replicated ack processing with AckAll gap handling, retention policy | +| `decodeAckUpdate` | 6590-6600 | MISSING | -- | Binary uvarint decoder for ack updates | +| `decodeDeliveredUpdate` | 6602-6620 | MISSING | -- | Binary uvarint/varint decoder for delivered updates | +| `jetStream.processConsumerLeaderChange` | 6622-6698 | MISSING | -- | Consumer leader change: advisory, setLeader, API response, pause advisory | +| `consumer.shouldSendLostQuorum` | 6701-6709 | MISSING | -- | Throttled consumer quorum-loss advisory gate | +| `Server.sendConsumerLostQuorumAdvisory` | 6711-6745 | MISSING | -- | Consumer lost-quorum advisory publication | +| `Server.sendConsumerLeaderElectAdvisory` | 6747-6777 | MISSING | -- | Consumer leader-elected advisory publication | +| `streamAssignmentResult` (struct) | 6779-6785 | MISSING | -- | Result struct for stream assignment error forwarding | +| `isInsufficientResourcesErr` | 6788-6790 | MISSING | -- | Error classification helper | +| `jetStream.processStreamAssignmentResults` | 6794-6870 | MISSING | -- | Stream assignment error result processing with retry across clusters | +| `jetStream.processConsumerAssignmentResults` | 6872-6906 | MISSING | -- | Consumer assignment error result processing | +| `streamAssignmentSubj` / `consumerAssignmentSubj` (consts) | 6908-6911 | MISSING | -- | System subject constants for assignment results | +| `jetStream.startUpdatesSub` | 6914-6937 | MISSING | -- | Subscribe to system subjects for assignment results, stepdown, peer remove, stream move | +| `jetStream.stopUpdatesSub` | 6940-6970 | MISSING | -- | Unsubscribe from system subjects | +| `Server.sendDomainLeaderElectAdvisory` | 6972-6999 | MISSING | -- | Domain leader-elected advisory publication | +| `jetStream.processLeaderChange` (meta) | 7001-7074 | PARTIAL | `JetStreamMetaGroup.ProcessLeaderChange` | .NET clears inflight and fires event; lacks server atomic update, startUpdatesSub/stopUpdatesSub, observer stepdown, streamsCheck fix | +| `jetStreamCluster.remapStreamAssignment` | 7077-7111 | PORTED | `JetStreamMetaGroup.RemapStreamAssignment` | .NET implements retain/replace/shrink logic | +| `selectPeerError` (struct + methods) | 7113-7207 | MISSING | -- | Detailed peer-selection error reporting with tag/storage/offline reasons | +| `jetStreamCluster.selectPeerGroup` | 7212-7507 | PARTIAL | `PlacementEngine.SelectPeerGroup` | .NET has tag filtering, cluster affinity, unique-tag, HA limit, weighted scoring; lacks per-peer storage availability check against MaxBytes, peerStreams/peerHA counting from live assignments, quorum check for online peers, detailed error reporting | +| `groupNameForStream` | 7509-7511 | MISSING | -- | Stream raft group name generator with hash | +| `groupNameForConsumer` | 7513-7515 | MISSING | -- | Consumer raft group name generator with hash | +| `groupName` | 7517-7520 | MISSING | -- | Group name helper: prefix-R{n}{storage}-{hash} | +| `jetStream.tieredStreamAndReservationCount` | 7524-7543 | MISSING | -- | Account-tier stream count + storage reservation tracking | +| `jetStream.createGroupForStream` | 7547-7576 | MISSING | -- | Creates raft group for new stream with multi-cluster fallback | +| `Account.selectLimits` | 7578-7596 | MISSING | -- | Selects account tier limits by replica count | +| `jetStream.jsClusteredStreamLimitsCheck` | 7599-7618 | MISSING | -- | Pre-proposal stream limits validation (max streams, account limits) | +| `Server.jsClusteredStreamRequest` | 7620-7701 | PARTIAL | `ClusteredRequestProcessor` | .NET has request-id correlation + timeout pattern; lacks full stream config validation, subject overlap check, placement + group creation, meta propose | +| `sysRequest[T]` | 7710-7755 | MISSING | -- | Generic blocking system-account request utility | +| `Server.jsClusteredStreamUpdateRequest` | 7757-8000+ | MISSING | -- | Full clustered stream update: config validation, move request handling, replica scaling, consumer remapping | +| `lostQuorumAdvInterval` (const) | 4368 | MISSING | -- | 10-second throttle interval for quorum-loss advisories | + +| Status | Count | +| PORTED | 4 | +| PARTIAL | 14 | +| MISSING | 38 | +| NOT_APPLICABLE | 5 | +| **Total** | **61** | + + + + +| `Cluster/JetStreamMetaGroup.cs` | Meta-group assignment tracking, proposals, inflight, peer mgmt | +| `Cluster/StreamReplicaGroup.cs` | Per-stream RAFT group model, propose/apply/stepdown, msg ops | +| `Cluster/PlacementEngine.cs` | Topology-aware peer selection (selectPeerGroup) | +| `Cluster/ClusterAssignmentTypes.cs` | RaftGroup, StreamAssignment, ConsumerAssignment types | +| `Cluster/AssignmentCodec.cs` | JSON encode/decode for assignments, S2 compression | +| `Cluster/MetaSnapshotCodec.cs` | Binary meta snapshot encode/decode | +| `Cluster/JetStreamClusterMonitor.cs` | Background loop consuming RAFT entries, dispatching | +| `Cluster/AssetPlacementPlanner.cs` | Simple replica count planning | +| `Api/Handlers/StreamApiHandlers.cs` | Clustered stream create/update/delete handlers | +| `Api/Handlers/ConsumerApiHandlers.cs` | Clustered consumer create/delete handlers | +| `Api/Handlers/ClusterControlApiHandlers.cs` | Meta stepdown, stream/consumer leader stepdown | +| `Api/ClusteredRequestProcessor.cs` | Pending request correlation (propose + wait) | +| `Snapshots/StreamSnapshotService.cs` | TAR-based snapshot create/restore | + +| `jsClusteredStreamUpdateRequest` (tail) | jetstream_cluster.go:7757 (8001–8138) | PARTIAL | `StreamApiHandlers.HandleClusteredUpdateAsync` | Go version handles scale up/down, move requests, peer set reassignment, consumer migration. .NET version only does simple config update via `ProcessUpdateStreamAssignment`. Missing: scale up/down logic, move request handling, consumer peer reassignment. | +| `jsClusteredStreamDeleteRequest` | jetstream_cluster.go:8140–8165 | PORTED | `StreamApiHandlers.HandleClusteredDeleteAsync` | Proposes deletion to meta group, validates existence first. | +| `jsClusteredStreamPurgeRequest` | jetstream_cluster.go:8168–8212 | PARTIAL | `StreamApiHandlers.HandlePurge` (local) | Go proposes purge through RAFT when clustered (stream group node). .NET has local purge only, no RAFT proposal path for purge. | +| `jsClusteredStreamRestoreRequest` | jetstream_cluster.go:8214–8262 | PARTIAL | `StreamApiHandlers.HandleRestore` + `StreamSnapshotService` | Go creates a new RAFT group, sets preferred leader, proposes as stream assignment with Restore state. .NET has local restore path but no RAFT-based clustered restore proposal. | +| `allPeersOffline` | jetstream_cluster.go:8265–8278 | MISSING | — | Server-level helper to check if all peers in a RAFT group are offline. No nodeToInfo equivalent in .NET. | +| `jsClusteredStreamListRequest` | jetstream_cluster.go:8282–8444 | PARTIAL | `JetStreamApiRouter` routes to `StreamApiHandlers.HandleList` | Go does scatter-gather across cluster (sends internal msgs to each stream peer, collects responses, handles timeouts, offline peers, missing names). .NET has local-only list from StreamManager. | +| `jsClusteredConsumerListRequest` | jetstream_cluster.go:8448–8591 | PARTIAL | `JetStreamApiRouter` routes to `ConsumerApiHandlers.HandleList` | Same scatter-gather pattern as stream list. .NET has local-only consumer list. | +| `encodeStreamPurge` | jetstream_cluster.go:8593–8598 | MISSING | — | Binary encode of streamPurge for RAFT proposal. | +| `decodeStreamPurge` | jetstream_cluster.go:8600–8604 | MISSING | — | Binary decode of streamPurge from RAFT log. | +| `jsClusteredConsumerDeleteRequest` | jetstream_cluster.go:8606–8643 | PORTED | `ConsumerApiHandlers.HandleClusteredDeleteAsync` | Proposes consumer deletion via meta RAFT group. | +| `encodeMsgDelete` | jetstream_cluster.go:8645–8650 | MISSING | — | Binary encode of streamMsgDelete for RAFT proposal. | +| `decodeMsgDelete` | jetstream_cluster.go:8652–8656 | MISSING | — | Binary decode of streamMsgDelete from RAFT log. | +| `jsClusteredMsgDeleteRequest` | jetstream_cluster.go:8658–8701 | MISSING | — | Clustered message delete by sequence — proposes through stream RAFT node. No .NET equivalent. | +| `encodeAddStreamAssignment` | jetstream_cluster.go:8703–8711 | PORTED | `AssignmentCodec.EncodeStreamAssignment` | JSON serialization of stream assignment. | +| `encodeUpdateStreamAssignment` | jetstream_cluster.go:8713–8721 | PARTIAL | `AssignmentCodec.EncodeStreamAssignment` | Go uses a separate opcode byte (updateStreamOp). .NET uses same encoder without op-code prefix. | +| `encodeDeleteStreamAssignment` | jetstream_cluster.go:8723–8731 | PARTIAL | `AssignmentCodec.EncodeStreamAssignment` | Go uses removeStreamOp opcode byte. .NET lacks op-code prefix differentiation. | +| `decodeStreamAssignment` | jetstream_cluster.go:8733–8742 | PORTED | `AssignmentCodec.DecodeStreamAssignment` | JSON deserialization of stream assignment. | +| `decodeStreamAssignmentConfig` | jetstream_cluster.go:8744–8764 | PARTIAL | `AssignmentCodec.DecodeStreamAssignment` | Go does DisallowUnknownFields for forward-compat (marks as unsupported). .NET does basic decode without unsupported field detection. | +| `encodeDeleteRange` | jetstream_cluster.go:8766–8771 | MISSING | — | Binary encode of DeleteRange for catchup gap markers. | +| `decodeDeleteRange` | jetstream_cluster.go:8773–8780 | MISSING | — | Binary decode of DeleteRange. | +| `createGroupForConsumer` | jetstream_cluster.go:8783–8821 | PARTIAL | `PlacementEngine.SelectPeerGroup` | Go selects peers from stream's group, checks active/offline, enforces quorum, picks storage type. .NET PlacementEngine does general peer selection but not the specific consumer-from-stream-peers logic. | +| `jsClusteredConsumerRequest` | jetstream_cluster.go:8824–9173 | PARTIAL | `ConsumerApiHandlers.HandleClusteredCreateAsync` | Go has full validation: account limits, maxConsumers check, WQ policy checks, ephemeral naming, HA asset limits, scale up/down, consumer state transfer. .NET does basic leader + stream-exists validation only. | +| `encodeAddConsumerAssignment` | jetstream_cluster.go:9175–9183 | PORTED | `AssignmentCodec.EncodeConsumerAssignment` | JSON serialization of consumer assignment. | +| `encodeDeleteConsumerAssignment` | jetstream_cluster.go:9185–9193 | PARTIAL | `AssignmentCodec.EncodeConsumerAssignment` | Go uses removeConsumerOp opcode. .NET lacks op-code differentiation. | +| `decodeConsumerAssignment` | jetstream_cluster.go:9195–9204 | PORTED | `AssignmentCodec.DecodeConsumerAssignment` | JSON deserialization of consumer assignment. | +| `decodeConsumerAssignmentConfig` | jetstream_cluster.go:9206–9224 | PARTIAL | `AssignmentCodec.DecodeConsumerAssignment` | Go does unsupported-field detection + API level check. .NET does basic decode. | +| `encodeAddConsumerAssignmentCompressed` | jetstream_cluster.go:9226–9236 | PORTED | `AssignmentCodec.CompressIfLarge` | S2/Snappy compression for large consumer assignments. | +| `decodeConsumerAssignmentCompressed` | jetstream_cluster.go:9238–9250 | PORTED | `AssignmentCodec.DecompressIfNeeded` | Snappy decompression for compressed consumer assignments. | +| `decodeStreamMsg` | jetstream_cluster.go:9254–9309 | MISSING | — | Binary wire decode of replicated stream messages (seq, ts, subject, reply, hdr, msg, flags). Critical for RAFT replication. | +| `decodeBatchMsg` | jetstream_cluster.go:9311–9332 | MISSING | — | Binary decode of batch message wrapper (batchId, batchSeq, inner op). | +| `encodeStreamMsg` | jetstream_cluster.go:9339–9341 | MISSING | — | Binary wire encode of replicated stream messages. | +| `encodeStreamMsgAllowCompress` | jetstream_cluster.go:9343–9345 | MISSING | — | Wrapper calling encode with compression allowed. | +| `encodeStreamMsgAllowCompressAndBatch` | jetstream_cluster.go:9352–9421 | MISSING | — | Full binary encoder with S2 compression (threshold 8KB), batch support, source/mirror flags. Core of clustered message replication. | +| `compressThreshold` (const) | jetstream_cluster.go:9349 | MISSING | — | 8192-byte threshold for message compression. | +| `msgFlagFromSourceOrMirror` (const) | jetstream_cluster.go:9336 | MISSING | — | Flag constant for source/mirror tracking. | +| `supportsBinarySnapshot` | jetstream_cluster.go:9424–9428 | MISSING | — | Checks if all peers support binary snapshot format. | +| `supportsBinarySnapshotLocked` | jetstream_cluster.go:9432–9450 | MISSING | — | Lock-held version checking peer nodeInfo for binary snapshot support. | +| `streamSnapshot` (struct) | jetstream_cluster.go:9454–9461 | MISSING | — | Legacy JSON snapshot type (v1 format with deleted[]uint64). | +| `stateSnapshot` | jetstream_cluster.go:9464–9468 | MISSING | — | Creates binary/legacy snapshot for RAFT. .NET has `StreamSnapshotService` but it's TAR-based, not the RAFT binary snapshot format. | +| `stateSnapshotLocked` | jetstream_cluster.go:9472–9501 | MISSING | — | Lock-held snapshot with binary vs legacy fallback and EncodedStreamState. | +| `processClusteredInboundMsg` | jetstream_cluster.go:9507–9690 | MISSING | — | Proposes inbound published message to stream RAFT group. Core of clustered publish path: leader check, sealed check, limits, dedup, lag warning, message trace. | +| `streamLagWarnThreshold` (const) | jetstream_cluster.go:9504 | MISSING | — | 10,000 threshold for lag warning. | +| `getAndDeleteMsgTrace` | jetstream_cluster.go:9692–9703 | MISSING | — | Retrieves and deletes a message trace by sequence. | +| `streamSyncRequest` (struct) | jetstream_cluster.go:9707–9713 | MISSING | — | Request structure for stream catchup (Peer, FirstSeq, LastSeq, DeleteRangesOk, MinApplied). | +| `calculateSyncRequest` | jetstream_cluster.go:9717–9728 | MISSING | — | Calculates catchup request based on local state vs snapshot. | +| `processSnapshotDeletes` | jetstream_cluster.go:9732–9756 | MISSING | — | Processes deletes from snapshot (Compact, SyncDeleted). | +| `setCatchupPeer` | jetstream_cluster.go:9758–9768 | MISSING | — | Sets catchup peer with lag tracking. | +| `updateCatchupPeer` | jetstream_cluster.go:9771–9780 | MISSING | — | Decrements catchup lag by one. | +| `decrementCatchupPeer` | jetstream_cluster.go:9782–9796 | MISSING | — | Decrements catchup lag by N. | +| `clearCatchupPeer` | jetstream_cluster.go:9798–9804 | MISSING | — | Clears a single catchup peer. | +| `clearAllCatchupPeers` | jetstream_cluster.go:9807–9811 | MISSING | — | Clears all catchup peers. | +| `lagForCatchupPeer` | jetstream_cluster.go:9813–9820 | MISSING | — | Returns lag for a specific catchup peer. | +| `hasCatchupPeers` | jetstream_cluster.go:9822–9826 | MISSING | — | Returns true if any catchup peers exist. | +| `setCatchingUp` | jetstream_cluster.go:9828–9830 | MISSING | — | Atomic bool: set catching up. | +| `clearCatchingUp` | jetstream_cluster.go:9832–9834 | MISSING | — | Atomic bool: clear catching up. | +| `isCatchingUp` | jetstream_cluster.go:9836–9838 | MISSING | — | Atomic bool: is catching up. | +| `isCurrent` | jetstream_cluster.go:9842–9847 | MISSING | — | Check if non-leader stream is current (RAFT current + not catching up). | +| `maxConcurrentSyncRequests` (const) | jetstream_cluster.go:9850 | MISSING | — | 32 max concurrent sync requests. | +| Error sentinel vars (catchup errors) | jetstream_cluster.go:9852–9860 | MISSING | — | errCatchupCorruptSnapshot, errCatchupStalled, errCatchupStreamStopped, etc. | +| `processSnapshot` | jetstream_cluster.go:9863–10000+ | MISSING | — | Full stream snapshot processing: delete sync, calculate sync request, pause RAFT apply, semaphore-limited catchup with retries (max 3). ~140 lines. | +| `processCatchupMsg` | jetstream_cluster.go:10160–10251 | MISSING | — | Processes out-of-band catchup messages: deleteRange handling, S2 decompression, preAck clearing, store writes, dedup tracking. | +| `flushAllPending` | jetstream_cluster.go:10254–10256 | MISSING | — | Flush pending writes after snapshot install. | +| `handleClusterSyncRequest` | jetstream_cluster.go:10258–10265 | MISSING | — | Handler for inbound cluster sync requests; starts runCatchup goroutine. | +| `offlineClusterInfo` | jetstream_cluster.go:10268–10280 | MISSING | — | Returns ClusterInfo for offline RAFT group (all replicas marked offline). | +| `clusterInfo` | jetstream_cluster.go:10283–10355 | MISSING | — | Returns full ClusterInfo for RAFT group: leader, replicas, lag, current status, sorted by name. Central to /jsz and stream info responses. | +| `checkClusterInfo` | jetstream_cluster.go:10357–10365 | MISSING | — | Adjusts ClusterInfo replicas for catchup lag. | +| `streamAlternates` | jetstream_cluster.go:10370–10420 | MISSING | — | Returns ranked list of stream mirrors/alternates for client connection. | +| `handleClusterStreamInfoRequest` | jetstream_cluster.go:10423–10425 | MISSING | — | Inbound handler for cluster stream info (dispatches to processClusterStreamInfoRequest). | +| `processClusterStreamInfoRequest` | jetstream_cluster.go:10427–10460 | MISSING | — | Processes cluster stream info request: gathers state, config, cluster info, sources, mirror. | +| `defaultMaxTotalCatchupOutBytes` (const) | jetstream_cluster.go:10465 | MISSING | — | 64 MB max total server-wide catchup outbound bytes. | +| `gcbTotal` | jetstream_cluster.go:10468–10472 | MISSING | — | Returns total outstanding catchup bytes. | +| `gcbBelowMax` | jetstream_cluster.go:10476–10480 | MISSING | — | Returns true if catchup bytes below max. | +| `gcbAdd` | jetstream_cluster.go:10485–10493 | MISSING | — | Adds to server-wide and local catchup byte counters. | +| `gcbSubLocked` | jetstream_cluster.go:10499–10509 | MISSING | — | Subtracts from catchup byte counters (locked). | +| `gcbSub` | jetstream_cluster.go:10512–10516 | MISSING | — | Locking wrapper for gcbSubLocked. | +| `gcbSubLast` | jetstream_cluster.go:10522–10527 | MISSING | — | Final subtract + zero out local counter on catchup exit. | +| `cbKickChan` | jetstream_cluster.go:10530–10534 | MISSING | — | Returns kick channel for catchup bandwidth management. | +| `runCatchup` | jetstream_cluster.go:10536–10846 | MISSING | — | Leader-side catchup sender: flow control, batch sending, ack tracking, delete ranges, gap markers, activity timeout, global bandwidth cap. ~310 lines. | +| `syncSubjForStream` | jetstream_cluster.go:10850–10852 | MISSING | — | Returns sync subject for stream catchup. | +| `syncReplySubject` | jetstream_cluster.go:10854–10856 | MISSING | — | Returns reply subject for sync. | +| `infoReplySubject` | jetstream_cluster.go:10858–10860 | MISSING | — | Returns reply subject for info. | +| `syncAckSubject` | jetstream_cluster.go:10862–10864 | MISSING | — | Returns ack subject for sync flow control. | +| `syncSubject` | jetstream_cluster.go:10866–10880 | MISSING | — | Generates random sync subject with base encoding. | +| `clusterStreamInfoT` (const) | jetstream_cluster.go:10883 | MISSING | — | Subject template for cluster stream info requests. | +| `clusterConsumerInfoT` (const) | jetstream_cluster.go:10884 | MISSING | — | Subject template for cluster consumer info requests. | +| `jsaUpdatesSubT` / `jsaUpdatesPubT` (consts) | jetstream_cluster.go:10885–10886 | MISSING | — | Subject templates for JSC account-related update subscriptions. | +| `jscAllSubj` (const) | jetstream_cluster.go:10848 | MISSING | — | `$JSC.>` wildcard subscription for all cluster subjects. | + +| Status | Count | +| PORTED | 8 | +| PARTIAL | 13 | +| MISSING | 56 | +| NOT_APPLICABLE | 0 | +| DEFERRED | 0 | +| **Total** | **77** | + + + + + +## Change Log + +| Date | Change | By | +|------|--------|----| +| 2026-02-25 | JS-2a: store.go full + stream.go lines 1-4000. 172 symbols. PORTED:55 PARTIAL:30 MISSING:75 NA:12 | opus | +| 2026-02-25 | JS-2b: stream.go lines 4001-end. 116 symbols. PORTED:11 PARTIAL:24 MISSING:73 NA:8 | opus | +| 2026-02-25 | JS-3a: consumer.go lines 1-3500. 115 symbols. PORTED:23 PARTIAL:21 MISSING:70 NA:1 | opus | +| 2026-02-25 | JS-3b: consumer.go lines 3501-end. 105 symbols. PORTED:31 PARTIAL:28 MISSING:37 NA:9 | opus | +| 2026-02-25 | JS-4a: memstore + filestore 1-4000. 140 symbols. PORTED:37 PARTIAL:27 MISSING:71 NA:5 | opus | +| 2026-02-25 | JS-4b: filestore 4001-7000. 80 symbols. PORTED:14 PARTIAL:27 MISSING:37 NA:2 | opus | +| 2026-02-25 | JS-4c: filestore 7001-10000. 82 symbols. PORTED:26 PARTIAL:19 MISSING:32 NA:5 | opus | +| 2026-02-25 | JS-4d: filestore 10001-end + dirstore. 106 symbols. PORTED:22 PARTIAL:23 MISSING:57 NA:4 | opus | +| 2026-02-25 | JS-5a: jetstream_cluster.go lines 1-4000. 91 symbols. PORTED:25 PARTIAL:14 MISSING:48 NA:4 | opus | +| 2026-02-25 | JS-5b: jetstream_cluster.go lines 4001-8000. 61 symbols. PORTED:4 PARTIAL:14 MISSING:38 NA:5 | opus | +| 2026-02-25 | JS-5c: jetstream_cluster.go lines 8001-end. 77 symbols. PORTED:8 PARTIAL:13 MISSING:56 | opus | +| 2026-02-25 | File created with LLM analysis instructions | auto | +| 2026-02-25 | JS-1 Core sub-pass: analyzed jetstream.go, jetstream_api.go, jetstream_events.go, jetstream_errors.go, jetstream_versioning.go, jetstream_batching.go. 150+ symbols inventoried. | opus | diff --git a/gaps/leaf-nodes.md b/gaps/leaf-nodes.md new file mode 100644 index 0000000..22590bc --- /dev/null +++ b/gaps/leaf-nodes.md @@ -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 + + + +### 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 ` 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 ` 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 | diff --git a/gaps/logging.md b/gaps/logging.md new file mode 100644 index 0000000..2cb14b7 --- /dev/null +++ b/gaps/logging.md @@ -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`) 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 + + + +### 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 _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 | +| `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 | diff --git a/gaps/misc-uncategorized.md b/gaps/misc-uncategorized.md new file mode 100644 index 0000000..1960b13 --- /dev/null +++ b/gaps/misc-uncategorized.md @@ -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 + + + +### 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 | diff --git a/gaps/monitoring.md b/gaps/monitoring.md new file mode 100644 index 0000000..d8e2f06 --- /dev/null +++ b/gaps/monitoring.md @@ -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 + + + +### 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 | diff --git a/gaps/mqtt.md b/gaps/mqtt.md new file mode 100644 index 0000000..1abf35a --- /dev/null +++ b/gaps/mqtt.md @@ -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 + + + +### 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 | diff --git a/gaps/protocol.md b/gaps/protocol.md new file mode 100644 index 0000000..d63987b --- /dev/null +++ b/gaps/protocol.md @@ -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`. +- 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 + + + +### 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:57–134 | 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` + `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:27–34 | 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 | diff --git a/gaps/raft.md b/gaps/raft.md new file mode 100644 index 0000000..6da8c1b --- /dev/null +++ b/gaps/raft.md @@ -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 + + + +### 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 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 | +| 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 | diff --git a/gaps/routes.md b/gaps/routes.md new file mode 100644 index 0000000..0fac506 --- /dev/null +++ b/gaps/routes.md @@ -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 + + + +### `golang/nats-server/server/route.go` + +| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes | +|-----------|:-------------|--------|:----------------|-------| +| `RouteType` (type alias + consts `Implicit`/`Explicit`) | route.go:36–44 | PORTED | `src/NATS.Server/Routes/RouteConnection.cs` — implicit/explicit distinction tracked in `RouteConnection` handshake and `RouteManager.ConnectToRouteWithRetryAsync` | No explicit enum; Implicit/Explicit distinction is encoded in how routes are established (solicited vs inbound) | +| `route` struct (unexported) | route.go:56–94 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:8` | Fields `remoteID`, `poolIdx`, `accName`, `noPool`, `compression`, `gossipMode` are present. Fields for `lnoc`, `lnocu`, `jetstream`, `connectURLs`, `wsConnURLs`, `gatewayURL`, `leafnodeURL`, `hash`, `idHash`, `startNewRoute`, `retry` are MISSING — not modelled in .NET | +| `routeInfo` struct (unexported) | route.go:97–101 | MISSING | — | Used internally for deferred pool-connection creation after first PONG; no .NET equivalent | +| `gossipDefault`/`gossipDisabled`/`gossipOverride` consts | route.go:104–108 | MISSING | — | Gossip mode bytes used in INFO propagation; not implemented in .NET | +| `connectInfo` struct | route.go:110–124 | 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:127–130 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:73,83` | Handshake uses a simplified `ROUTE ` format rather than `CONNECT ` / `INFO ` | +| `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:145–148 | 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:187–222` (`ReadFramesAsync`) | .NET parses basic RMSG account/subject/reply/size. Missing: queue-group routing indicators (`+`/`|`), header size field, multi-field arg parsing | +| `(c *client) processInboundRoutedMsg` | route.go:460 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:219–221` | .NET fires `RoutedMessageReceived` callback; missing: stats update, gateway reply handling (`handleGatewayReply`), account lookup and fanout via `processMsgResults` | +| `(c *client) sendRouteConnect` | route.go:503 | MISSING | — | Outbound CONNECT protocol (with cluster auth, TLS flags, etc.) not sent; .NET uses a simpler `ROUTE ` handshake | +| `computeRoutePoolIdx` | route.go:538 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:149` (`ComputeRoutePoolIdx`) | FNV-1a 32-bit hash, identical algorithm | +| `(c *client) processRouteInfo` | route.go:549 | MISSING | — | Full INFO processing (cluster name negotiation, compression negotiation, duplicate detection, account route setup) not implemented; .NET handshake is a simple ID exchange | +| `(s *Server) negotiateRouteCompression` | route.go:897 | PARTIAL | `src/NATS.Server/Routes/RouteCompressionCodec.cs:82` (`NegotiateCompression`) | .NET has the negotiation logic; but integration into handshake (INFO exchange, switching compression writer/reader mid-stream) is MISSING | +| `(s *Server) updateRemoteRoutePerms` | route.go:953 | MISSING | — | Route permission update on INFO reload not implemented | +| `(s *Server) sendAsyncInfoToClients` | route.go:1015 | 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:177–185` | .NET fires `RemoteSubscriptionReceived` with `IsRemoval=true`; missing: sub key lookup and removal from SubList, gateway/leafnode interest updates | +| `(c *client) processRemoteSub` | route.go:1489 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:167–175` | .NET fires `RemoteSubscriptionReceived`; missing: key construction with type byte prefix, account lookup/creation, permission check (`canExport`), SubList insertion, gateway/leafnode updates, queue-weight delta tracking | +| `(c *client) addRouteSubOrUnsubProtoToBuf` | route.go:1729 | PARTIAL | `src/NATS.Server/Routes/RouteConnection.cs:95–109` (`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:347–354` | .NET solicits configured routes with pool connections; missing: per-account (pinned) route solicitation, `saveRouteTLSName` | +| `(c *client) processRouteConnect` | route.go:3011 | MISSING | — | Parsing and validation of inbound CONNECT from route (cluster name check, wrong-port detection, LNOC/LNOCU flags) not implemented; .NET uses a simpler handshake | +| `(s *Server) removeAllRoutesExcept` | route.go:3085 | PORTED | `src/NATS.Server/Routes/RouteManager.cs:602` (`RemoveAllRoutesExcept`) | Equivalent behavior: remove all routes not in the keep-set | +| `(s *Server) removeRoute` | route.go:3113 | PARTIAL | `src/NATS.Server/Routes/RouteManager.cs: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 | diff --git a/gaps/stillmissing.md b/gaps/stillmissing.md new file mode 100644 index 0000000..9755f80 --- /dev/null +++ b/gaps/stillmissing.md @@ -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. diff --git a/gaps/subscriptions.md b/gaps/subscriptions.md new file mode 100644 index 0000000..2e887e2 --- /dev/null +++ b/gaps/subscriptions.md @@ -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 + + + +### 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` 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 | diff --git a/gaps/tls-security.md b/gaps/tls-security.md new file mode 100644 index 0000000..85d17cf --- /dev/null +++ b/gaps/tls-security.md @@ -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 + + + +### 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 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 | diff --git a/gaps/utilities-and-other.md b/gaps/utilities-and-other.md new file mode 100644 index 0000000..561ea1b --- /dev/null +++ b/gaps/utilities-and-other.md @@ -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` 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)` — 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.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` 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` 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` 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` reader | +| `outMsgPool` | golang/nats-server/server/sendq.go:103 | NOT_APPLICABLE | — | `sync.Pool` for `outMsg` structs; .NET uses `ArrayPool` 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` 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` or `Channel.CreateUnbounded` | +| `ipQueue[T].push` | golang/nats-server/server/ipqueue.go:113 | NOT_APPLICABLE | — | Maps to `ChannelWriter.TryWrite` | +| `ipQueue[T].pop` | golang/nats-server/server/ipqueue.go:152 | NOT_APPLICABLE | — | Maps to `ChannelReader.ReadAllAsync` | +| `ipQueue[T].popOne` | golang/nats-server/server/ipqueue.go:178 | NOT_APPLICABLE | — | Maps to `ChannelReader.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.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 | diff --git a/gaps/websocket.md b/gaps/websocket.md new file mode 100644 index 0000000..c310859 --- /dev/null +++ b/gaps/websocket.md @@ -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 + + + +### `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:885–916 | 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:43–96 | PORTED | `src/NATS.Server/WebSocket/WsConstants.cs:9–65` | 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 |