21 KiB
21 KiB
Subscriptions — Gap Analysis
This file tracks what has and hasn't been ported from Go to .NET for the Subscriptions module. See stillmissing.md for the full LOC comparison across all modules.
LLM Instructions: How to Analyze This Category
Step 1: Read the Go Reference Files
Read each Go source file listed below. For every file:
- Extract all exported types (structs, interfaces, type aliases)
- Extract all exported methods on those types (receiver functions)
- Extract all exported standalone functions
- Note key constants, enums, and protocol states
- Note important unexported helpers that implement core logic (functions >20 lines)
- Pay attention to concurrency patterns (goroutines, mutexes, channels) — these map to different .NET patterns
Step 2: Read the .NET Implementation Files
Read all .cs files in the .NET directories listed below. For each Go symbol found in Step 1:
- Search for a matching type, method, or function in .NET
- If found, compare the behavior: does it handle the same edge cases? Same error paths?
- If partially implemented, note what's missing
- If not found, note it as MISSING
Step 3: Cross-Reference Tests
Compare Go test functions against .NET test methods:
- For each Go
Test*function, check if a corresponding .NET[Fact]or[Theory]exists - Note which test scenarios are covered and which are missing
- Check the parity DB (
docs/test_parity.db) for existing mappings:sqlite3 docs/test_parity.db "SELECT go_test, dotnet_test, confidence FROM test_mappings tm JOIN go_tests gt ON tm.go_test_id=gt.rowid JOIN dotnet_tests dt ON tm.dotnet_test_id=dt.rowid WHERE gt.go_file LIKE '%PATTERN%'"
Step 4: Classify Each Item
Use these status values:
| Status | Meaning |
|---|---|
| PORTED | Equivalent exists in .NET with matching behavior |
| PARTIAL | .NET implementation exists but is incomplete (missing edge cases, error handling, or features) |
| MISSING | No .NET equivalent found — needs to be ported |
| NOT_APPLICABLE | Go-specific pattern that doesn't apply to .NET (build tags, platform-specific goroutine tricks, etc.) |
| DEFERRED | Intentionally skipped for now (document why) |
Step 5: Fill In the Gap Inventory
Add rows to the Gap Inventory table below. Group by Go source file. Include the Go file and line number so a porting LLM can jump directly to the reference implementation.
Key Porting Notes for Subscriptions
- The
Sublisttrie is performance-critical — every published message triggers aMatch()call. - Cache invalidation uses atomic generation counters (
Interlockedin .NET). subject_transform.gohandles subject mapping for imports/exports — check if the .NETImports/directory covers this.
Go Reference Files (Source)
golang/nats-server/server/sublist.go— Trie-based subject matcher with wildcard support (*single,>multi). Nodes havepsubs(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.gogolang/nats-server/server/subject_transform_test.go
.NET Implementation Files (Source)
src/NATS.Server/Subscriptions/SubjectMatch.cssrc/NATS.Server/Subscriptions/SubList.cssrc/NATS.Server/Subscriptions/SubListResult.cssrc/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<byte> vs []byte |
HasInterest() |
sublist.go:532 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:638 |
Cache-aware fast path |
NumInterest() |
sublist.go:537 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:669 |
Returns (plainCount, queueCount) tuple |
matchNoLock() |
sublist.go:543 | NOT_APPLICABLE | — | Internal; .NET lock model differs (no equivalent needed) |
match() |
sublist.go:547 | NOT_APPLICABLE | — | Internal implementation; split into Match() + private helpers in .NET |
hasInterest() |
sublist.go:624 | NOT_APPLICABLE | — | Internal; maps to HasInterestLevel() private helper |
reduceCacheCount() |
sublist.go:675 | PORTED | src/NATS.Server/Subscriptions/SubListCacheSweeper.cs:7 + SubList.cs:458 |
Background goroutine mapped to SubListCacheSweeper + SweepCache() |
isRemoteQSub() |
sublist.go:689 | NOT_APPLICABLE | — | Go has client.kind == ROUTER/LEAF; .NET uses RemoteSubscription model instead |
UpdateRemoteQSub() |
sublist.go:695 | MISSING | — | Go updates weight of remote qsub; .NET uses ApplyRemoteSub() for a different model (full add/remove) |
addNodeToResults() |
sublist.go:706 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:549 (private AddNodeToResults()) |
Remote qsub weight expansion present in Go missing in .NET |
findQSlot() |
sublist.go:745 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:562 (inline in AddNodeToResults) |
Inlined in .NET |
matchLevel() |
sublist.go:757 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:506 (private MatchLevel()) |
Core trie descent algorithm |
matchLevelForAny() |
sublist.go:785 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:754 (private HasInterestLevel()) + CountInterestLevel() |
Split into two .NET helpers |
remove() (internal) |
sublist.go:843 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:282 (private RemoveInternal()) |
Internal removal logic |
Remove() |
sublist.go:915 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:255 |
Fires InterestChanged event |
RemoveBatch() |
sublist.go:920 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:687 |
Cache disable/re-enable pattern ported |
level.pruneNode() |
sublist.go:953 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:345 (inline in RemoveInternal) |
Inlined in removal path |
node.isEmpty() |
sublist.go:968 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:981 (TrieNode.IsEmpty property) |
Property instead of method |
level.numNodes() |
sublist.go:978 | NOT_APPLICABLE | — | Used internally in visitLevel(); no .NET equivalent needed |
removeFromNode() |
sublist.go:993 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:324 (inline in RemoveInternal) |
Inlined |
Count() |
sublist.go:1023 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:41 (Count property) |
|
CacheCount() |
sublist.go:1030 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:97 (CacheCount property) |
|
SublistStats (struct) |
sublist.go:1038 | PORTED | src/NATS.Server/Subscriptions/SubListStats.cs:3 |
All public fields present; unexported fields totFanout, cacheCnt, cacheHits dropped (computed inline in Stats()) |
SublistStats.add() |
sublist.go:1052 | MISSING | — | Aggregates multiple SublistStats into one; used for cluster monitoring; no .NET equivalent |
Stats() |
sublist.go:1076 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:580 |
Full fanout stats computed |
numLevels() |
sublist.go:1120 | MISSING | — | Debug/test utility counting trie depth; not ported |
visitLevel() |
sublist.go:1126 | MISSING | — | Internal helper for numLevels(); not ported |
subjectHasWildcard() |
sublist.go:1159 | PARTIAL | src/NATS.Server/Subscriptions/SubjectMatch.cs:50 (IsLiteral() — inverse) |
.NET !IsLiteral() is equivalent but not a dedicated function |
subjectIsLiteral() |
sublist.go:1174 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:50 (IsLiteral()) |
Exact equivalent |
IsValidPublishSubject() |
sublist.go:1187 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:67 |
|
IsValidSubject() |
sublist.go:1192 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:9 |
|
isValidSubject() (internal, checkRunes) |
sublist.go:1196 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:211 (IsValidSubject(string, bool)) |
|
IsValidLiteralSubject() |
sublist.go:1236 | PARTIAL | src/NATS.Server/Subscriptions/SubjectMatch.cs:50 (IsLiteral()) |
IsLiteral() does not validate the subject first; IsValidPublishSubject() combines both |
isValidLiteralSubject() (tokens iter) |
sublist.go:1241 | NOT_APPLICABLE | — | Takes iter.Seq[string] (Go 1.23 iterator); C# uses different iteration model |
ValidateMapping() |
sublist.go:1258 | MISSING | — | Validates a mapping destination subject string including {{function()}} syntax; no public .NET equivalent |
analyzeTokens() |
sublist.go:1298 | NOT_APPLICABLE | — | Internal helper used only in SubjectsCollide(); logic inlined in .NET SubjectsCollide() |
tokensCanMatch() |
sublist.go:1314 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:199 (private TokensCanMatch()) |
|
SubjectsCollide() |
sublist.go:1326 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:159 |
|
numTokens() |
sublist.go:1374 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:118 (NumTokens()) |
|
tokenAt() |
sublist.go:1389 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:132 (TokenAt()) |
Go is 1-based index; .NET is 0-based |
tokenizeSubjectIntoSlice() |
sublist.go:1407 | NOT_APPLICABLE | — | Internal slice-reuse helper; .NET uses Tokenize() private method in SubList |
SubjectMatchesFilter() |
sublist.go:1421 | PARTIAL | src/NATS.Server/JetStream/Storage/MemStore.cs:1175 (private), FileStore.cs:773 (private) |
Duplicated as private methods in MemStore and FileStore; not a public standalone function |
subjectIsSubsetMatch() |
sublist.go:1426 | MISSING | — | No public .NET equivalent; logic exists privately in MemStore/FileStore |
isSubsetMatch() |
sublist.go:1434 | MISSING | — | Internal; no public .NET equivalent |
isSubsetMatchTokenized() |
sublist.go:1444 | MISSING | — | Internal; no public .NET equivalent |
matchLiteral() |
sublist.go:1483 | PORTED | src/NATS.Server/Subscriptions/SubjectMatch.cs:75 (MatchLiteral()) |
|
addLocalSub() |
sublist.go:1552 | NOT_APPLICABLE | — | Filters by client kind (CLIENT/SYSTEM/JETSTREAM/ACCOUNT/LEAF); no .NET equivalent needed (client kind routing done elsewhere) |
Sublist.addNodeToSubs() |
sublist.go:1562 | NOT_APPLICABLE | — | Internal helper for localSubs(); not ported |
Sublist.collectLocalSubs() |
sublist.go:1581 | NOT_APPLICABLE | — | Internal helper for localSubs(); not ported |
Sublist.localSubs() |
sublist.go:1597 | MISSING | — | Returns only local-client subscriptions (excludes routes/gateways); no .NET equivalent |
Sublist.All() |
sublist.go:1604 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:712 |
|
Sublist.addAllNodeToSubs() |
sublist.go:1610 | NOT_APPLICABLE | — | Internal helper for All(); inlined in .NET |
Sublist.collectAllSubs() |
sublist.go:1627 | NOT_APPLICABLE | — | Internal; inlined in CollectAllSubs() private method in .NET |
ReverseMatch() |
sublist.go:1649 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:727 |
|
reverseMatchLevel() |
sublist.go:1674 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:860 (private ReverseMatchLevel()) |
|
getAllNodes() |
sublist.go:1714 | PORTED | src/NATS.Server/Subscriptions/SubList.cs:909 (private CollectAllNodes()) |
golang/nats-server/server/subject_transform.go
| Go Symbol | Go File:Line | Status | .NET Equivalent | Notes |
|---|---|---|---|---|
Transform type constants (NoTransform…Random) |
subject_transform.go:43 | PARTIAL | src/NATS.Server/Subscriptions/SubjectTransform.cs:682 (private TransformType enum) |
Random (value 11 in Go) is absent from the .NET enum and switch statement; all others ported |
subjectTransform (struct) |
subject_transform.go:61 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:11 |
Internal fields mapped to _source, _dest, _sourceTokens, _destTokens, _ops |
SubjectTransformer (interface) |
subject_transform.go:73 | NOT_APPLICABLE | — | Go exports interface for polymorphism; C# uses concrete SubjectTransform class directly |
NewSubjectTransformWithStrict() |
subject_transform.go:81 | MISSING | — | Strict mode validates that all source wildcards are used in dest; no .NET equivalent |
NewSubjectTransform() |
subject_transform.go:198 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:31 (Create()) |
Non-strict creation |
NewSubjectTransformStrict() |
subject_transform.go:202 | MISSING | — | Strict version for import mappings; no .NET equivalent |
getMappingFunctionArgs() |
subject_transform.go:206 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:639 (private GetFunctionArgs()) |
|
transformIndexIntArgsHelper() |
subject_transform.go:215 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:610 (private ParseIndexIntArgs()) |
|
indexPlaceHolders() |
subject_transform.go:237 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:486 (ParseDestToken() + ParseMustacheToken()) |
Split into two methods; Random branch missing |
transformTokenize() |
subject_transform.go:378 | MISSING | — | Converts foo.*.* to foo.$1.$2; used for import subject mapping reversal; no .NET equivalent |
transformUntokenize() |
subject_transform.go:399 | MISSING | — | Inverse of above; used in reverse(); no .NET equivalent |
tokenizeSubject() |
subject_transform.go:414 | NOT_APPLICABLE | — | Internal tokenizer; .NET uses string.Split('.') or Tokenize() private method |
subjectTransform.Match() |
subject_transform.go:433 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:126 (Apply()) |
Renamed; returns null instead of error on no-match |
subjectTransform.TransformSubject() |
subject_transform.go:456 | PARTIAL | src/NATS.Server/Subscriptions/SubjectTransform.cs:126 (via Apply()) |
TransformSubject (apply without match check) not separately exposed; Apply() always checks match |
subjectTransform.getRandomPartition() |
subject_transform.go:460 | MISSING | — | Random transform type not implemented in .NET |
subjectTransform.getHashPartition() |
subject_transform.go:469 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:226 (ComputePartition() + Fnv1A32()) |
FNV-1a 32-bit hash ported |
subjectTransform.TransformTokenizedSubject() |
subject_transform.go:482 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:144 (private TransformTokenized()) |
All transform types except Random ported |
subjectTransform.reverse() |
subject_transform.go:638 | MISSING | — | Produces the inverse transform; used for import subject mapping; no .NET equivalent |
subjectInfo() |
subject_transform.go:666 | PORTED | src/NATS.Server/Subscriptions/SubjectTransform.cs:451 (private SubjectInfo()) |
|
ValidateMapping() (in subject_transform.go context) |
sublist.go:1258 | MISSING | — | Also defined via NewSubjectTransform; validates mapping destination with function syntax; no .NET public equivalent |
Keeping This File Updated
After porting work is completed:
- Update status: Change
MISSING → PORTEDorPARTIAL → PORTEDfor each item completed - Add .NET path: Fill in the ".NET Equivalent" column with the actual file:line
- Re-count LOC: Update the LOC numbers in
stillmissing.md:# Re-count .NET source LOC for this module find src/NATS.Server/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 - Add a changelog entry below with date and summary of what was ported
- Update the parity DB if new test mappings were created:
sqlite3 docs/test_parity.db "INSERT INTO test_mappings (go_test_id, dotnet_test_id, confidence, notes) VALUES (?, ?, 'manual', 'ported in YYYY-MM-DD session')"
Change Log
| Date | Change | By |
|---|---|---|
| 2026-02-25 | File created with LLM analysis instructions | auto |
| 2026-02-25 | Full gap inventory populated: 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 |