Files
SVSimServer/SVSim.BattleNode/Reliability/OutboundSequencer.cs
gamer147 9ff8948903 docs(battlenode): document four latent low-tier hygiene hazards
Comment-only; behavior-preserving; 231 BattleNode tests green.

- OutboundSequencer._archive: name the unbounded-per-match growth + ack-prune point.
- NodeCrypto.BuildAes: SECURITY remarks on key-derived IV reuse + base64 entropy loss;
  warn against caching the session key.
- MatchContext/BattlePlayer: FOOTGUN notes on reference-based record equality over the deck list.
- RecordTokensFrom: TRUST note on isSelf/idx overwrite; name the idx>deckCount guard for
  untrusted peers (not added — trusted-LAN today).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:11:13 -04:00

46 lines
2.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using SVSim.BattleNode.Protocol;
namespace SVSim.BattleNode.Reliability;
/// <summary>
/// Per-session outbound ledger. Assigns monotonic playSeq to ordered pushes and archives
/// them for future Resume retransmit (v2). No-stock control pushes (BattleFinish/JudgeResult/Resume)
/// are wrapped with no playSeq and skip the archive.
/// </summary>
public sealed class OutboundSequencer
{
/// <summary>First playSeq assigned. Starts at 1, not 0 — 0 is reserved for no-stock /
/// unsequenced pushes (which carry a null PlaySeq via <see cref="WrapNoStock"/>).</summary>
private const long FirstPlaySeq = 1;
private long _next = FirstPlaySeq;
// Holds every ordered (stocked) push for the WHOLE match — there is no per-ack pruning, so it
// grows with battle length × concurrent battles. Bounded only by Clear() in the terminate cascade.
// Fine at current scale; if battles get long or concurrency scales, prune entries below the peer's
// ack watermark here (contrast the inbound side, which is bounded by InboundTracker.WindowSize).
private readonly Dictionary<long, MsgEnvelope> _archive = new();
public IReadOnlyDictionary<long, MsgEnvelope> Archive => _archive;
public MsgEnvelope AssignAndArchive(MsgEnvelope envelope)
{
var seq = _next++;
var stamped = envelope with { PlaySeq = seq };
_archive[seq] = stamped;
return stamped;
}
public MsgEnvelope WrapNoStock(MsgEnvelope envelope) =>
envelope with { PlaySeq = null };
/// <summary>
/// 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. <c>_next</c> 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.
/// </summary>
public void Clear() => _archive.Clear();
}