diff --git a/SVSim.BattleNode/Reliability/OutboundSequencer.cs b/SVSim.BattleNode/Reliability/OutboundSequencer.cs index da7d7a3..79cdbb6 100644 --- a/SVSim.BattleNode/Reliability/OutboundSequencer.cs +++ b/SVSim.BattleNode/Reliability/OutboundSequencer.cs @@ -24,4 +24,13 @@ public sealed class OutboundSequencer public MsgEnvelope WrapNoStock(MsgEnvelope envelope) => envelope with { PlaySeq = null }; + + /// + /// Drop all archived envelopes. Called from BattleSession's terminate cascade so + /// the archive — the heavy state — is released the moment the battle ends, rather + /// than waiting for the participant to be GC'd. _next is left untouched: + /// a participant emitting after Clear is a bug, not a recovery case, but the seq + /// stream stays monotonic so a stray emit doesn't silently re-use a playSeq value. + /// + public void Clear() => _archive.Clear(); } diff --git a/SVSim.UnitTests/BattleNode/Reliability/OutboundSequencerTests.cs b/SVSim.UnitTests/BattleNode/Reliability/OutboundSequencerTests.cs index d27c570..c20e337 100644 --- a/SVSim.UnitTests/BattleNode/Reliability/OutboundSequencerTests.cs +++ b/SVSim.UnitTests/BattleNode/Reliability/OutboundSequencerTests.cs @@ -49,4 +49,41 @@ public class OutboundSequencerTests Assert.That(seq.Archive.Keys, Is.EquivalentTo(new[] { 1L, 2L })); Assert.That(seq.Archive[1L].Uri, Is.EqualTo(NetworkBattleUri.Matched)); } + + [Test] + public void Clear_empties_archive() + { + var seq = new OutboundSequencer(); + seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.Matched)); + seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.BattleStart)); + + seq.Clear(); + + Assert.That(seq.Archive, Is.Empty); + } + + [Test] + public void Clear_does_not_reset_next_seq() + { + // A post-Clear emit is a bug per the design (terminate has already fired), + // but the impl must keep the seq stream monotonic if it does happen — no + // silent re-use of playSeq=1 against a different envelope. + var seq = new OutboundSequencer(); + seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.Matched)); // playSeq=1 + seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.BattleStart)); // playSeq=2 + + seq.Clear(); + var post = seq.AssignAndArchive(MakeEnvelope(NetworkBattleUri.Deal)); + + Assert.That(post.PlaySeq, Is.EqualTo(3), "_next must continue, not reset, after Clear."); + } + + [Test] + public void Clear_on_empty_sequencer_is_noop() + { + var seq = new OutboundSequencer(); + + Assert.DoesNotThrow(() => seq.Clear()); + Assert.That(seq.Archive, Is.Empty); + } }