From 3ace39d606c7d8a7c5ebbd7c13aed15f6e185058 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 07:06:09 -0500 Subject: [PATCH 1/6] feat(batch2): verify parser remainder features --- .../ZB.MOM.NatsNet.Server/ClientConnection.cs | 52 +++++++++ .../Protocol/ProtocolParserTests.cs | 108 +++++++++++++++++- porting.db | Bin 6352896 -> 6356992 bytes 3 files changed, 159 insertions(+), 1 deletion(-) diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs index 00a9682..f35f978 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/ClientConnection.cs @@ -1158,6 +1158,58 @@ public sealed partial class ClientConnection TraceInOp("PRE", pre); } + // ========================================================================= + // Parser compatibility wrappers (features 2588, 2590, 2591) + // ========================================================================= + + /// + /// Parses protocol bytes using the shared parser state for this connection. + /// Mirrors Go client.parse. + /// + internal Exception? Parse(byte[] buf, IProtocolHandler handler) + { + ArgumentNullException.ThrowIfNull(buf); + ArgumentNullException.ThrowIfNull(handler); + + ParseCtx.Kind = Kind; + ParseCtx.HasHeaders = Headers; + if (_mcl > 0) + ParseCtx.MaxControlLine = _mcl; + if (_mpay != 0) + ParseCtx.MaxPayload = _mpay; + + return ProtocolParser.Parse(ParseCtx, handler, buf); + } + + /// + /// Checks max control line enforcement for the current connection kind. + /// Mirrors Go client.overMaxControlLineLimit. + /// + internal Exception? OverMaxControlLineLimit(byte[] arg, int mcl, IProtocolHandler handler) + { + ArgumentNullException.ThrowIfNull(arg); + ArgumentNullException.ThrowIfNull(handler); + + ParseCtx.Kind = Kind; + return ProtocolParser.OverMaxControlLineLimit(ParseCtx, handler, arg, mcl); + } + + /// + /// Re-processes stored pub args for split-buffer message payload handling. + /// Mirrors Go client.clonePubArg. + /// + internal Exception? ClonePubArg(IProtocolHandler handler, bool lmsg) + { + ArgumentNullException.ThrowIfNull(handler); + + ParseCtx.Kind = Kind; + ParseCtx.HasHeaders = Headers; + if (_mpay != 0) + ParseCtx.MaxPayload = _mpay; + + return ProtocolParser.ClonePubArg(ParseCtx, handler, lmsg); + } + /// /// Generates the INFO JSON bytes sent to the client on connect. /// Stub — full implementation in session 09. diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProtocolParserTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProtocolParserTests.cs index ebc9e82..70fb822 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProtocolParserTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Protocol/ProtocolParserTests.cs @@ -13,6 +13,7 @@ using System.Text; using Shouldly; +using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Protocol; @@ -754,6 +755,110 @@ public class ProtocolParserTests } } + [Fact] + public void ParsePub_SplitPayload_ClonesPubArgAndCompletesMessage() + { + var c = DummyClient(); + var h = DummyHandler(); + + var first = "PUB foo 5\r\nhe"u8.ToArray(); + ProtocolParser.Parse(c, h, first).ShouldBeNull(); + c.State.ShouldBe(ParserState.MsgPayload); + c.ArgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 5"); + c.MsgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.MsgBuf!).ShouldBe("he"); + h.InboundMessages.Count.ShouldBe(0); + + var second = "llo\r\n"u8.ToArray(); + ProtocolParser.Parse(c, h, second).ShouldBeNull(); + c.State.ShouldBe(ParserState.OpStart); + h.InboundMessages.Count.ShouldBe(1); + Encoding.ASCII.GetString(h.InboundMessages[0]).ShouldBe("hello\r\n"); + } + + [Fact] + public void ClonePubArg_ClientBranch_ReprocessesPubArgs() + { + var c = DummyClient(); + var h = DummyHandler(); + c.Pa.Arg = "foo 5"u8.ToArray(); + c.Pa.HeaderSize = -1; + + var err = ProtocolParser.ClonePubArg(c, h, lmsg: false); + + err.ShouldBeNull(); + c.ArgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("foo 5"); + Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo"); + c.Pa.Size.ShouldBe(5); + } + + [Fact] + public void ClonePubArg_RouterLmsgBranch_ReprocessesRoutedOriginArgs() + { + var c = DummyRouteClient(); + var h = DummyHandler(); + c.Pa.Arg = "$G foo 5"u8.ToArray(); + c.Pa.HeaderSize = -1; + + var err = ProtocolParser.ClonePubArg(c, h, lmsg: true); + + err.ShouldBeNull(); + c.ArgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("$G foo 5"); + Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G"); + Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo"); + c.Pa.Size.ShouldBe(5); + } + + [Fact] + public void ClonePubArg_LeafHeaderBranch_ReprocessesLeafHeaderArgs() + { + var c = new ParseContext { Kind = ClientKind.Leaf, MaxPayload = -1 }; + var h = DummyHandler(); + c.Pa.Arg = "$G foo 2 5"u8.ToArray(); + c.Pa.HeaderSize = 2; + + var err = ProtocolParser.ClonePubArg(c, h, lmsg: false); + + err.ShouldBeNull(); + c.ArgBuf.ShouldNotBeNull(); + Encoding.ASCII.GetString(c.ArgBuf!).ShouldBe("$G foo 2 5"); + Encoding.ASCII.GetString(c.Pa.Account!).ShouldBe("$G"); + Encoding.ASCII.GetString(c.Pa.Subject!).ShouldBe("foo"); + c.Pa.HeaderSize.ShouldBe(2); + c.Pa.Size.ShouldBe(5); + } + + [Fact] + public void ClientConnection_ExposesParserCompatibilityMethods() + { + var flags = System.Reflection.BindingFlags.Instance + | System.Reflection.BindingFlags.NonPublic + | System.Reflection.BindingFlags.Public; + var methods = typeof(ClientConnection).GetMethods(flags); + + methods.Any(method => + method.Name == "Parse" && + method.GetParameters().Length == 2 && + method.GetParameters()[0].ParameterType == typeof(byte[]) && + method.GetParameters()[1].ParameterType == typeof(IProtocolHandler)).ShouldBeTrue(); + + methods.Any(method => + method.Name == "OverMaxControlLineLimit" && + method.GetParameters().Length == 3 && + method.GetParameters()[0].ParameterType == typeof(byte[]) && + method.GetParameters()[1].ParameterType == typeof(int) && + method.GetParameters()[2].ParameterType == typeof(IProtocolHandler)).ShouldBeTrue(); + + methods.Any(method => + method.Name == "ClonePubArg" && + method.GetParameters().Length == 2 && + method.GetParameters()[0].ParameterType == typeof(IProtocolHandler) && + method.GetParameters()[1].ParameterType == typeof(bool)).ShouldBeTrue(); + } + // ===================================================================== // TestProtocolHandler — stub handler for tests // ===================================================================== @@ -770,6 +875,7 @@ public class ProtocolParserTests public int PingCount { get; private set; } public int PongCount { get; private set; } public byte[]? ConnectArgs { get; private set; } + public List InboundMessages { get; } = []; public Exception? ProcessConnect(byte[] arg) { ConnectArgs = arg; return null; } public Exception? ProcessInfo(byte[] arg) => null; @@ -786,7 +892,7 @@ public class ProtocolParserTests public Exception? ProcessLeafUnsub(byte[] arg) => null; public Exception? ProcessAccountSub(byte[] arg) => null; public void ProcessAccountUnsub(byte[] arg) { } - public void ProcessInboundMsg(byte[] msg) { } + public void ProcessInboundMsg(byte[] msg) => InboundMessages.Add(msg); public bool SelectMappedSubject() => false; public void TraceInOp(string name, byte[]? arg) { } public void TraceMsg(byte[] msg) { } diff --git a/porting.db b/porting.db index a8ce8d99ec89b4405d6d96bf0c89c0570c4d5e03..424aa6511a5b2ea2cdf59f0233dadc8baab27c5c 100644 GIT binary patch delta 1703 zcmciBOKclO7zgm(wKs`3YsZe0v`uaAI*r>r>U!6Y*fAls2`!~Ok~ZN{QkSglbul=$ zwcdaVhuBTQ2Xi12(%cY5a4b?K(4LSua3NArflGu?Rn(j+h=hbx2_(edDUJh|UaB5` z(#+0C^Z#bQsoq{QRjXG_9~@+-DXPQ1E*__+uP-G6^cQPa1IE<=J@O^(GB&p8pIDdG zUfY{wDJg zWAl;g;r&VDx0_7N+Cfq5kBvZm{1yWyWBiu+_oU4ZOa1l1M>~ZIqVH`&tWgpX|g?Uf8E0g&7bq8s^xQe+_C3B zGZ#s0FCDB~oOW<}1czW4d~g_!zzB@O7>vVF z@WU|(fCMrGAp~KFzyw4g29pqn$6yK`hiNzt2}r^dFauA*2{;K)!6|qePQx?sEIbF# z!z|3f88{2`umCT>i|`V>4Cmk#ScLP2#hLnxHw{qNNT--ON?#+g?CceFwW3#oQgFgA1^vPM?HG(lqV+{@3NFB_#-exWv+sJX zcoHn1pnA^QKDBOIc|Kuy=TgV`1GQ6)aCb9mjdj-|?W_V(it->SUXl;5%0WqPJ!It8 zS1SHoq4buRc}>l{Q7+|+x|mjTB~2Au{S_C>idIoIu~oa6TPYMSwTne*N-?`rftvka z33yRq#0s@yS@-7h+|M-|IM++0cjST9SnSb93$~)wBHisP!|(n1yq{cnSMFb(n0(Z! k#u{?F<+#?pbHYRFx#$CWt~S1nh1;SFxm!27h$xD#6oS;Osx2rk1VQ}Tg^M5jzjG>;8NO7x;YQObf;ZE~!vz^*asq@;3#~;)Ck9=4xk9!8))mVdrMZ_BNkGv*VwKx1Pd#RZ$D%EJBja?Mq zO=W;lEu?&=iPt<%4-BP;%C}^Ps2fTbqkGH2I0a0lfA#33f~n-_>O;AMDqn+Ms+@Dl z^vYC%^fc{qRl^YltY8B>sNeu6tp44giEzZTxFrALnncMI4~eL*XBbxtG~?B8Q-X0n zSzGiec869b_4_osQub6;pAKGV0Uxx29|Eur*24y9gLVi)2XsOggus9>n6MEd&<#Bh zg Date: Sat, 28 Feb 2026 07:07:03 -0500 Subject: [PATCH 2/6] docs(CLAUDE.md): add tmux-cli + codex (ccc) usage instructions Document the workflow for delegating tasks to Codex via tmux-cli instead of using the Codex MCP server for long-running commands. --- CLAUDE.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 31ce8b4..7d8caf7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,64 @@ All .NET code must follow the rules in [`docs/standards/dotnet-standards.md`](do - **Logging**: `Microsoft.Extensions.Logging` with Serilog provider; use `LogContext.PushProperty` for contextual enrichment - **Naming**: PascalCase for all public members; `ZB.MOM.NatsNet.Server.[Module]` namespace hierarchy +## Using Codex via tmux-cli + +Use `tmux-cli` to launch and interact with OpenAI Codex (`ccc` alias) in a separate tmux pane. This lets you delegate tasks to Codex without blocking your current session. + +**Do NOT use the Codex MCP server (`mcp__codex__codex`) for long-running commands.** Use `tmux-cli` + `ccc` instead — it runs in a separate pane, won't time out, and lets you capture output when ready. + +### The `ccc` alias + +`ccc` runs `codex --dangerously-bypass-approvals-and-sandbox` (with Node/nvm auto-setup). Use it for tasks you want Codex to handle autonomously. + +### Workflow + +**Always launch a shell pane first** — if a command errors without a shell, the pane closes and output is lost: + +```bash +# 1. Launch a shell (returns pane ID, e.g. "2") +tmux-cli launch "zsh" + +# 2. Send a codex command to that pane +tmux-cli send 'ccc "your prompt here"' --pane=2 + +# 3. Wait for codex to finish (no output for N seconds = idle) +tmux-cli wait_idle --pane=2 --idle-time=10.0 + +# 4. Capture the output +tmux-cli capture --pane=2 + +# 5. Clean up when done +tmux-cli kill --pane=2 +``` + +### Key tmux-cli commands + +| Command | Purpose | +|---------|---------| +| `tmux-cli launch "zsh"` | Start a shell pane (do this FIRST) | +| `tmux-cli send "cmd" --pane=N` | Send text + Enter to pane N | +| `tmux-cli send "text" --pane=N --enter=False` | Send text without pressing Enter | +| `tmux-cli send "text" --pane=N --delay-enter=0.5` | Custom delay before Enter (default 1.5s) | +| `tmux-cli wait_idle --pane=N --idle-time=3.0` | Wait until pane has no output for N seconds | +| `tmux-cli capture --pane=N` | Capture current pane output | +| `tmux-cli list_panes` | List all panes (JSON with IDs and status) | +| `tmux-cli kill --pane=N` | Kill a pane (cannot kill your own) | +| `tmux-cli interrupt --pane=N` | Send Ctrl+C to a pane | +| `tmux-cli escape --pane=N` | Send Escape to a pane | + +### Pane identifiers + +- Just the pane number: `2` (current window) +- Full format: `session:window.pane` (e.g. `myapp:1.2`) + +### Tips + +- Use `wait_idle` instead of polling with repeated `capture` calls +- Save the pane ID returned by `launch` for subsequent commands +- Use `capture` to check state before sending input +- For long-running Codex tasks, increase `--idle-time` (e.g. `--idle-time=15.0`) + ## Reports - `reports/current.md` always has the latest porting status. From 1a0ac59df8fafe080d654e0d058a94bbdbb44e62 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 07:11:42 -0500 Subject: [PATCH 3/6] feat(batch2): verify sublist helper remainder features --- .../DataStructures/SubscriptionIndex.cs | 71 +++++++++--------- .../DataStructures/SubscriptionIndexTests.cs | 45 +++++++++++ porting.db | Bin 6356992 -> 6356992 bytes reports/current.md | 37 +++++++++ reports/report_38b6fc8.md | 37 +++++++++ 5 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 reports/current.md create mode 100644 reports/report_38b6fc8.md diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/SubscriptionIndex.cs b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/SubscriptionIndex.cs index 8cb1bdc..0a5d6db 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/SubscriptionIndex.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/Internal/DataStructures/SubscriptionIndex.cs @@ -58,7 +58,7 @@ public sealed class SubscriptionIndex private long _cacheHits; private long _inserts; private long _removes; - private SublistLevel _root; + private Level _root; private Dictionary? _cache; private int _ccSweep; private NotifyMaps? _notify; @@ -70,7 +70,7 @@ public sealed class SubscriptionIndex private SubscriptionIndex(bool enableCache) { - _root = new SublistLevel(); + _root = new Level(); _cache = enableCache ? new Dictionary() : null; } @@ -116,7 +116,7 @@ public sealed class SubscriptionIndex try { bool sfwc = false, haswc = false, isnew = false; - SublistNode? n = null; + Node? n = null; var l = _root; var start = 0; @@ -135,7 +135,7 @@ public sealed class SubscriptionIndex { if (!l.Nodes.TryGetValue(t, out n)) { - n = new SublistNode(); + n = new Node(); l.Nodes[t] = n; } } @@ -148,7 +148,7 @@ public sealed class SubscriptionIndex haswc = true; if (n == null) { - n = new SublistNode(); + n = new Node(); l.Pwc = n; } break; @@ -158,21 +158,21 @@ public sealed class SubscriptionIndex sfwc = true; if (n == null) { - n = new SublistNode(); + n = new Node(); l.Fwc = n; } break; default: if (!l.Nodes.TryGetValue(t, out n)) { - n = new SublistNode(); + n = new Node(); l.Nodes[t] = n; } break; } } - n.Next ??= new SublistLevel(); + n.Next ??= new Level(); l = n.Next; start = i + 1; @@ -446,7 +446,7 @@ public sealed class SubscriptionIndex try { bool sfwc = false, haswc = false; - SublistNode? n = null; + Node? n = null; var l = _root; var lnts = new LevelNodeToken[32]; @@ -535,7 +535,7 @@ public sealed class SubscriptionIndex } } - private static (bool found, bool last) RemoveFromNode(SublistNode? n, Subscription sub) + private static (bool found, bool last) RemoveFromNode(Node? n, Subscription sub) { if (n == null) return (false, true); @@ -743,9 +743,9 @@ public sealed class SubscriptionIndex // Private: Trie matching (matchLevel) // ------------------------------------------------------------------------- - private static void MatchLevel(SublistLevel? l, string[] toks, SubscriptionIndexResult results) + private static void MatchLevel(Level? l, string[] toks, SubscriptionIndexResult results) { - SublistNode? pwc = null, n = null; + Node? pwc = null, n = null; for (int i = 0; i < toks.Length; i++) { if (l == null) return; @@ -760,9 +760,9 @@ public sealed class SubscriptionIndex if (pwc != null) AddNodeToResults(pwc, results); } - private static bool MatchLevelForAny(SublistLevel? l, ReadOnlySpan toks, ref int np, ref int nq) + private static bool MatchLevelForAny(Level? l, ReadOnlySpan toks, ref int np, ref int nq) { - SublistNode? pwc = null, n = null; + Node? pwc = null, n = null; for (int i = 0; i < toks.Length; i++) { if (l == null) return false; @@ -804,7 +804,7 @@ public sealed class SubscriptionIndex // Private: Reverse match // ------------------------------------------------------------------------- - private static void ReverseMatchLevel(SublistLevel? l, ReadOnlySpan toks, SublistNode? n, SubscriptionIndexResult results) + private static void ReverseMatchLevel(Level? l, ReadOnlySpan toks, Node? n, SubscriptionIndexResult results) { if (l == null) return; for (int i = 0; i < toks.Length; i++) @@ -848,7 +848,7 @@ public sealed class SubscriptionIndex if (n != null) AddNodeToResults(n, results); } - private static void GetAllNodes(SublistLevel? l, SubscriptionIndexResult results) + private static void GetAllNodes(Level? l, SubscriptionIndexResult results) { if (l == null) return; if (l.Pwc != null) AddNodeToResults(l.Pwc, results); @@ -864,7 +864,7 @@ public sealed class SubscriptionIndex // Private: addNodeToResults // ------------------------------------------------------------------------- - private static void AddNodeToResults(SublistNode n, SubscriptionIndexResult results) + private static void AddNodeToResults(Node n, SubscriptionIndexResult results) { // Plain subscriptions. if (n.PList != null) @@ -940,7 +940,7 @@ public sealed class SubscriptionIndex } } - private static void AddNodeToSubsLocal(SublistNode n, List subs, bool includeLeafHubs) + private static void AddNodeToSubsLocal(Node n, List subs, bool includeLeafHubs) { if (n.PList != null) { @@ -960,7 +960,7 @@ public sealed class SubscriptionIndex } } - private static void CollectLocalSubs(SublistLevel? l, List subs, bool includeLeafHubs) + private static void CollectLocalSubs(Level? l, List subs, bool includeLeafHubs) { if (l == null) return; foreach (var n in l.Nodes.Values) @@ -972,7 +972,7 @@ public sealed class SubscriptionIndex if (l.Fwc != null) { AddNodeToSubsLocal(l.Fwc, subs, includeLeafHubs); CollectLocalSubs(l.Fwc.Next, subs, includeLeafHubs); } } - private static void AddAllNodeToSubs(SublistNode n, List subs) + private static void AddAllNodeToSubs(Node n, List subs) { if (n.PList != null) subs.AddRange(n.PList); @@ -986,7 +986,7 @@ public sealed class SubscriptionIndex subs.Add(sub); } - private static void CollectAllSubs(SublistLevel? l, List subs) + private static void CollectAllSubs(Level? l, List subs) { if (l == null) return; foreach (var n in l.Nodes.Values) @@ -1204,7 +1204,7 @@ public sealed class SubscriptionIndex // Private: visitLevel (depth calculation for tests) // ------------------------------------------------------------------------- - private static int VisitLevel(SublistLevel? l, int depth) + private static int VisitLevel(Level? l, int depth) { if (l == null || l.NumNodes() == 0) return depth; depth++; @@ -1529,18 +1529,18 @@ public sealed class SubscriptionIndex } // ------------------------------------------------------------------------- - // Nested types: SublistNode, SublistLevel, NotifyMaps + // Nested types: Node, Level, NotifyMaps // ------------------------------------------------------------------------- - internal sealed class SublistNode + internal sealed class Node { public readonly Dictionary PSubs = new(ReferenceEqualityComparer.Instance); public Dictionary>? QSubs; public List? PList; - public SublistLevel? Next; + public Level? Next; /// Factory method matching Go's newNode(). - public static SublistNode NewNode() => new(); + public static Node NewNode() => new(); public bool IsEmpty() { @@ -1549,14 +1549,14 @@ public sealed class SubscriptionIndex } } - internal sealed class SublistLevel + internal sealed class Level { - public readonly Dictionary Nodes = new(); - public SublistNode? Pwc; - public SublistNode? Fwc; + public readonly Dictionary Nodes = new(); + public Node? Pwc; + public Node? Fwc; /// Factory method matching Go's newLevel(). - public static SublistLevel NewLevel() => new(); + public static Level NewLevel() => new(); public int NumNodes() { @@ -1566,8 +1566,9 @@ public sealed class SubscriptionIndex return num; } - public void PruneNode(SublistNode n, string t) + public void PruneNode(Node? n, string t) { + if (n == null) return; if (ReferenceEquals(n, Fwc)) Fwc = null; else if (ReferenceEquals(n, Pwc)) Pwc = null; else Nodes.Remove(t); @@ -1582,11 +1583,11 @@ public sealed class SubscriptionIndex private readonly struct LevelNodeToken { - public readonly SublistLevel Level; - public readonly SublistNode Node; + public readonly Level Level; + public readonly Node Node; public readonly string Token; - public LevelNodeToken(SublistLevel level, SublistNode node, string token) + public LevelNodeToken(Level level, Node node, string token) { Level = level; Node = node; diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/DataStructures/SubscriptionIndexTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/DataStructures/SubscriptionIndexTests.cs index fe11062..cdbc26e 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/DataStructures/SubscriptionIndexTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/Internal/DataStructures/SubscriptionIndexTests.cs @@ -219,6 +219,51 @@ public class SubscriptionIndexTests s.NumLevels().ShouldBe(0); } + [Fact] + public void PruneNode_NullNode_DoesNotMutateLiteralNodes() + { + var level = new SubscriptionIndex.Level(); + var literal = new SubscriptionIndex.Node(); + level.Nodes["foo"] = literal; + level.Fwc = new SubscriptionIndex.Node(); + level.Pwc = new SubscriptionIndex.Node(); + + level.PruneNode(null!, "foo"); + + level.Nodes.Count.ShouldBe(1); + level.Nodes["foo"].ShouldBeSameAs(literal); + } + + [Fact] + public void IsEmpty_WithAndWithoutChildren_TracksNodeEmptiness() + { + var node = new SubscriptionIndex.Node(); + node.IsEmpty().ShouldBeTrue(); + + node.Next = new SubscriptionIndex.Level(); + node.Next.Nodes["bar"] = new SubscriptionIndex.Node(); + node.IsEmpty().ShouldBeFalse(); + + node.Next.Nodes.Clear(); + node.IsEmpty().ShouldBeTrue(); + } + + [Fact] + public void NumNodes_WithLiteralAndWildcardEntries_CountsAllBranches() + { + var level = new SubscriptionIndex.Level(); + level.Nodes["foo"] = new SubscriptionIndex.Node(); + level.Pwc = new SubscriptionIndex.Node(); + level.Fwc = new SubscriptionIndex.Node(); + + level.NumNodes().ShouldBe(3); + + level.PruneNode(level.Pwc, SubscriptionIndex.Pwcs); + level.PruneNode(level.Fwc, SubscriptionIndex.Fwcs); + level.PruneNode(level.Nodes["foo"], "foo"); + level.NumNodes().ShouldBe(0); + } + [Theory] // T:2983, T:2984 [InlineData(true)] [InlineData(false)] diff --git a/porting.db b/porting.db index 424aa6511a5b2ea2cdf59f0233dadc8baab27c5c..6bbe23bec6f13c1c4c597054a54204115de6401d 100644 GIT binary patch delta 1701 zcmciC$xjne90%~uw532h!=fM-tAm1jnFXk{Zp8(~eP3~D>8nnpi!;*}4NFPRR?SiX) zB6I6_zi9T0%(NRU*I_<*%LHySuc^I{J!p3JvAY@bX+QhP$+XfZm)Y8Ku7hv2jMa@) zn9E>QIJ8RB@&UG4uvjdsA|y&QjYPN(R11cy3@WLobASaa*uV~X;DCH6fI@JB3plfL zfIn3=xo8Dn!8N;PJC_w)$$y^jb+qK2v|nVmFg=$0w#PQ7wT7;DGiT`87ra5Wydn=3 zU-C2P-734?E=z&BdK!MoXX(9e>qN>7@>5546eK-t z3+rG#Y=Dih2{ywP*b3WVJM4g+&(Cu`XmZA;(1@0N=as{1-3$GWv zFOc&MhiNiz*fWlbtiLzlw0Kxg*5S#P)kR6jNNGgp=(RCBE9qKsd+E`Qsj%LrsYydk z#9QJK(ox%{2jm8y+?I$X+lfJ9p?E6PE=1cC89_HlQV5f1LL4w zgitD?8YCiw^x-p#r1-p&;#C?2SqbE>WgVjV5v}xxY#G}0VTbqV&tug${M%SEVm0PE zR`Pwz(liv8-Sk{c{&mm%|1_N9&o1hngK#Ceo|R?Aznf?7U!@x%%~0d}1U(gQS9Rm- zfZDzcE|bs>42?OX8v6wt-G}i7vH8|+}h1__b67SIDC@d_m@nLYDr8_hLzV3ro#4A zSgCrW>c#flvQqz6S(dIz^|o6Ki<_duzB{RQDfXu+^_NFU$?Pk&#;=!(lq3Jnz+jJF z!qWe)V(Cw-jb)~0slD+V@&5jJuQ~i*4XxJLNP3k9CtPqN2f4_D4i5~N_DEVh4wM@! zo*8{PZ!Y(1PP6--Yu@?I8PuZQTG3-ZQ~Q(`j#=4Ks64eRKZwtUoP93_t2WZR;s zmOq|o9yz`!k}^AEmB?UL1pj^QS#e)Z)VVGBGb=*USD@*sB{8$)bw->E&WuAwrqu?! zsm#}{xW2o;xEJh~0pE%mdSqxt>R{qlqHic+j<1b}Yh%>V#xfN?tiyV2Kt49Yk4@N& z0u-VM#jsFvBa8!q88+HnO}aShkeflhRx+s;(P eqVvLKL>%5$@!b4q8b-tp#5!vXJMugBPWuBf5)2Ul diff --git a/reports/current.md b/reports/current.md new file mode 100644 index 0000000..506c986 --- /dev/null +++ b/reports/current.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-28 12:11:43 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2361 | +| n_a | 24 | +| stub | 1 | +| verified | 1287 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2091 | +| n_a | 187 | +| verified | 979 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2489/6942 items complete (35.9%)** diff --git a/reports/report_38b6fc8.md b/reports/report_38b6fc8.md new file mode 100644 index 0000000..506c986 --- /dev/null +++ b/reports/report_38b6fc8.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-28 12:11:43 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2361 | +| n_a | 24 | +| stub | 1 | +| verified | 1287 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2091 | +| n_a | 187 | +| verified | 979 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2489/6942 items complete (35.9%)** From c2a73b49f7b3075429ea5298d1726d739aa1e88c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 07:17:01 -0500 Subject: [PATCH 4/6] feat(batch2): verify memstore remainder features --- .../JetStream/MemStore.cs | 50 ++++++++++--- .../JetStream/JetStreamMemoryStoreTests.cs | 67 +++++++++++++++++- .../JetStream/StorageEngineTests.cs | 2 +- porting.db | Bin 6356992 -> 6356992 bytes reports/current.md | 8 +-- reports/report_1a0ac59.md | 37 ++++++++++ 6 files changed, 147 insertions(+), 17 deletions(-) create mode 100644 reports/report_1a0ac59.md diff --git a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MemStore.cs b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MemStore.cs index ff828cb..e92739f 100644 --- a/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MemStore.cs +++ b/dotnet/src/ZB.MOM.NatsNet.Server/JetStream/MemStore.cs @@ -103,6 +103,23 @@ public sealed class JetStreamMemStore : IStreamStore _scheduling = new MsgScheduling(RunMsgScheduling); } + /// + /// Factory that mirrors Go newMemStore mapping semantics. + /// + public static JetStreamMemStore NewMemStore(StreamConfig cfg) + { + var ms = new JetStreamMemStore(cfg); + + if (cfg.FirstSeq > 0) + { + var (_, err) = ms.PurgeInternal(cfg.FirstSeq); + if (err != null) + throw err; + } + + return ms; + } + // ----------------------------------------------------------------------- // IStreamStore — store / load // ----------------------------------------------------------------------- @@ -1133,17 +1150,7 @@ public sealed class JetStreamMemStore : IStreamStore _mu.EnterReadLock(); try { - if (_msgs == null || _msgs.Count == 0) return (Array.Empty(), null); - var seqs = new List(_fss.Size()); - _fss.IterFast((subj, ss) => - { - if (ss.LastNeedsUpdate) - RecalculateForSubj(Encoding.UTF8.GetString(subj), ss); - seqs.Add(ss.Last); - return true; - }); - seqs.Sort(); - return (seqs.ToArray(), null); + return AllLastSeqsLocked(); } finally { @@ -1151,6 +1158,27 @@ public sealed class JetStreamMemStore : IStreamStore } } + /// + /// Returns sorted per-subject last sequences without taking locks. + /// Mirrors Go allLastSeqsLocked. + /// + private (ulong[] Seqs, Exception? Error) AllLastSeqsLocked() + { + if (_msgs == null || _msgs.Count == 0) return (Array.Empty(), null); + + var seqs = new List(_fss.Size()); + _fss.IterFast((subj, ss) => + { + if (ss.LastNeedsUpdate) + RecalculateForSubj(Encoding.UTF8.GetString(subj), ss); + seqs.Add(ss.Last); + return true; + }); + + seqs.Sort(); + return (seqs.ToArray(), null); + } + /// public (ulong[] Seqs, Exception? Error) MultiLastSeqs(string[] filters, ulong maxSeq, int maxAllowed) { diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamMemoryStoreTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamMemoryStoreTests.cs index 0d7ca72..e39670b 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamMemoryStoreTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/JetStreamMemoryStoreTests.cs @@ -32,7 +32,7 @@ public class JetStreamMemoryStoreTests private static JetStreamMemStore NewMemStore(StreamConfig? cfg = null) { cfg ??= new StreamConfig { Storage = StorageType.MemoryStorage, Name = "test" }; - return new JetStreamMemStore(cfg); + return JetStreamMemStore.NewMemStore(cfg); } private static byte[] Bytes(string s) => Encoding.UTF8.GetBytes(s); @@ -41,6 +41,71 @@ public class JetStreamMemoryStoreTests private static ulong MsgSize(string subj, byte[]? hdr, byte[]? msg) => (ulong)(subj.Length + (hdr?.Length ?? 0) + (msg?.Length ?? 0) + 16); + [Fact] + public void NewMemStore_FactoryMethod_InitializesFirstSequence() + { + var method = typeof(JetStreamMemStore).GetMethod( + "NewMemStore", + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); + method.ShouldNotBeNull(); + + var cfg = new StreamConfig + { + Name = "factory-init", + Storage = StorageType.MemoryStorage, + FirstSeq = 1000, + }; + + var created = method!.Invoke(null, new object?[] { cfg }); + created.ShouldBeOfType(); + + var ms = (JetStreamMemStore)created!; + var state = ms.State(); + state.FirstSeq.ShouldBe(1000UL); + state.LastSeq.ShouldBe(999UL); + ms.Stop(); + } + + [Fact] + public void AllLastSeqsLocked_MatchesPublicAllLastSeqsOrdering() + { + var cfg = new StreamConfig + { + Name = "locked-all-last-seqs", + Subjects = new[] { "*.*" }, + MaxMsgsPer = 50, + Storage = StorageType.MemoryStorage, + }; + var ms = NewMemStore(cfg); + + var subjs = new[] { "foo.foo", "foo.bar", "foo.baz", "bar.foo", "bar.bar", "bar.baz" }; + var msg = Bytes("abc"); + var rng = new Random(11); + + for (var i = 0; i < 1_000; i++) + { + var subj = subjs[rng.Next(subjs.Length)]; + ms.StoreMsg(subj, null, msg, 0); + } + + var method = typeof(JetStreamMemStore).GetMethod( + "AllLastSeqsLocked", + System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + method.ShouldNotBeNull(); + + var result = method!.Invoke(ms, Array.Empty()); + result.ShouldNotBeNull(); + var (lockedSeqs, lockedErr) = ((ValueTuple)result!); + + var (publicSeqs, publicErr) = ms.AllLastSeqs(); + + lockedErr.ShouldBeNull(); + publicErr.ShouldBeNull(); + lockedSeqs.SequenceEqual(publicSeqs).ShouldBeTrue(); + + ms.Stop(); + } + // ----------------------------------------------------------------------- // TestMemStoreBasics (T:2023) // ----------------------------------------------------------------------- diff --git a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/StorageEngineTests.cs b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/StorageEngineTests.cs index 4198b4c..13f5095 100644 --- a/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/StorageEngineTests.cs +++ b/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/StorageEngineTests.cs @@ -33,7 +33,7 @@ public class StorageEngineTests private static JetStreamMemStore NewMemStore(StreamConfig cfg) { cfg.Storage = StorageType.MemoryStorage; - return new JetStreamMemStore(cfg); + return JetStreamMemStore.NewMemStore(cfg); } private static byte[] Bytes(string s) => Encoding.UTF8.GetBytes(s); diff --git a/porting.db b/porting.db index 6bbe23bec6f13c1c4c597054a54204115de6401d..301f66ec07c2a8501c2eb755a7791b8b3885632d 100644 GIT binary patch delta 1039 zcmbu-TS!xJ90%~7|8|a>m+d&WW$Em=bl%eKoOx?rnq5pS)2Vqkop#irPT7t@K5bM8 z8uSwUp$IY}BuIRyPEX;3Q1~Q0Nl;kyEWLD5U-~L!MUTP%1OMM2zI^$?KN`KEjl%K+ z7xU`+f=gX+F*(nP-l?vAX1rs%*Tkryn^96tYn#a%YW9YAUhgn6j0x01J7 zUEDj3kv{Gq#X7?#^F#g6L9*H{2CbDul<7ge)7Yzt-@jRAZA%2SpaWL5Epaz9sky*p zr&D*hWZJOCCD13O84kLV$+E0i;+0BYtaCPjurW3D3uj`PKAQWL>!Rg5oR3PoNp`w& zpG%>OJ6!hfOtC6_<3_cF@z2g}a}Wo5FhD#cKq4f8QJvd1&DeC?d&{~-BG6o(HxqJ~ zdU@VK$St*(=kGCeH<=&Nkwr~D-D>3}I-J#GvRe#BP5Mn2~|)HC&33ba0*Vt88{2Ia1QFA9va|0T!2Qn2u;uoerSPKXoE}8 z4js@5UC<3Z&#^MLQY!^GBUOfAMoNgGLtiN1uH_-BSKgQ!>H)HsQI%I>mt zD)WgrSZmBRUmdTp6Z^D1X7voq0cAQY)Bf^qI}KeE&3vOg(W*>_<)Wd8R}{FsW#Wup7!0<{zNq0=}spSLa*y^ z5?UtXRmo*@!X>olGqbBEJn+H?KLQ9M1WV1XhNt7k%D=~LX_1sE+m6b~ylu%r+m5M{ zZ9mXmZ^eTA6|pA_pQVk VFMLbBw0Fv*l>SZJXI4rB-oHFC{Zjw{ diff --git a/reports/current.md b/reports/current.md index 506c986..5cf2338 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-28 12:11:43 UTC +Generated: 2026-02-28 12:17:02 UTC ## Modules (12 total) @@ -12,10 +12,10 @@ Generated: 2026-02-28 12:11:43 UTC | Status | Count | |--------|-------| -| deferred | 2361 | +| deferred | 2359 | | n_a | 24 | | stub | 1 | -| verified | 1287 | +| verified | 1289 | ## Unit Tests (3257 total) @@ -34,4 +34,4 @@ Generated: 2026-02-28 12:11:43 UTC ## Overall Progress -**2489/6942 items complete (35.9%)** +**2491/6942 items complete (35.9%)** diff --git a/reports/report_1a0ac59.md b/reports/report_1a0ac59.md new file mode 100644 index 0000000..5cf2338 --- /dev/null +++ b/reports/report_1a0ac59.md @@ -0,0 +1,37 @@ +# NATS .NET Porting Status Report + +Generated: 2026-02-28 12:17:02 UTC + +## Modules (12 total) + +| Status | Count | +|--------|-------| +| verified | 12 | + +## Features (3673 total) + +| Status | Count | +|--------|-------| +| deferred | 2359 | +| n_a | 24 | +| stub | 1 | +| verified | 1289 | + +## Unit Tests (3257 total) + +| Status | Count | +|--------|-------| +| deferred | 2091 | +| n_a | 187 | +| verified | 979 | + +## Library Mappings (36 total) + +| Status | Count | +|--------|-------| +| mapped | 36 | + + +## Overall Progress + +**2491/6942 items complete (35.9%)** From 6bf0f86f2f4cf4b85673ca0a9862b03fa9b1b64d Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 28 Feb 2026 07:17:22 -0500 Subject: [PATCH 5/6] chore(batch2): complete parser-sublist-memstore remainder batch --- porting.db | Bin 6356992 -> 6356992 bytes reports/current.md | 2 +- reports/report_c2a73b4.md | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 reports/report_c2a73b4.md diff --git a/porting.db b/porting.db index 301f66ec07c2a8501c2eb755a7791b8b3885632d..21c728ded7b331121b362522c258a7e050a2602a 100644 GIT binary patch delta 371 zcmXxdJ5Lh<0KoBk^l`oG-BDkqV&#njDlH)L@KN3rDB1!l;t51MG(qS<;?RV2_@@ae z16$W7BqU5OCS4r(0BG+Mu+o9Gi3^1|F#P6UHjAA74-p9&2TokLQShLm;YG*5qz)gq zxXm5zat}ZC1h`KF4+!#*Mw)1*g;v^Vr-Mg4rjstZdBRhk(L;zZz4Xz~a|U?9AVUl@ z!b@K9nm0rkWsGqqm}H7+W{5J&9P`9j;4SZnlVFi0mU+(yKC;3mR{8wPl8W9lp-Zm` zdd;ku)sj)PY|}3Yf|^cleM@b5<)HX=`+Isbxs#Mi)t{EHZYnQ`zr}!%HxtICVd)h; zrOVz8?XMPB&((~odomuMa;z-5|G9s-Mc1J#=DctoIQtxb9Gmi$yeEgHBPm=67gr;y oCTYTQR1}q9VYh5eD>ql+v~2a4_D`&HBWGJV`%BJVJF?e9*KweJw*UYD delta 370 zcmXZVH%J2k06@_zDVJRC&aN@`-i^JN*n6*urw)pUAfn zI*H(r%}qoEhYxT2;V^ReFGM6{Y}j$&M8Sm{6%QIZ21z87LMmyblR+j~c*({`4!Pu! zPXUD#QA`P?lu=Fvl~hqp4Ykx!kDmq_X`-1HT4|%54m# Date: Sat, 28 Feb 2026 07:18:21 -0500 Subject: [PATCH 6/6] chore(reports): refresh current status timestamp --- reports/current.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reports/current.md b/reports/current.md index 7a4589d..3a94d48 100644 --- a/reports/current.md +++ b/reports/current.md @@ -1,6 +1,6 @@ # NATS .NET Porting Status Report -Generated: 2026-02-28 12:17:23 UTC +Generated: 2026-02-28 12:18:22 UTC ## Modules (12 total)