From 3ade8ff4f5854ac7de1f38d6f81a9a7620ad7f8d Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 31 May 2026 22:00:40 -0400 Subject: [PATCH] feat(battle-node): in-memory IBattleSessionStore + PendingBattle --- .../Sessions/IBattleSessionStore.cs | 13 ++++++ .../Sessions/InMemoryBattleSessionStore.cs | 17 ++++++++ SVSim.BattleNode/Sessions/PendingBattle.cs | 7 +++ .../InMemoryBattleSessionStoreTests.cs | 43 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 SVSim.BattleNode/Sessions/IBattleSessionStore.cs create mode 100644 SVSim.BattleNode/Sessions/InMemoryBattleSessionStore.cs create mode 100644 SVSim.BattleNode/Sessions/PendingBattle.cs create mode 100644 SVSim.UnitTests/BattleNode/Sessions/InMemoryBattleSessionStoreTests.cs diff --git a/SVSim.BattleNode/Sessions/IBattleSessionStore.cs b/SVSim.BattleNode/Sessions/IBattleSessionStore.cs new file mode 100644 index 0000000..541ead7 --- /dev/null +++ b/SVSim.BattleNode/Sessions/IBattleSessionStore.cs @@ -0,0 +1,13 @@ +namespace SVSim.BattleNode.Sessions; + +public interface IBattleSessionStore +{ + /// Register a battle minted by the matching bridge, awaiting a WS connect. + void RegisterPending(PendingBattle battle); + + /// Look up the pending battle. Returns null if not present. + PendingBattle? TryGetPending(string battleId); + + /// Mark a battle as no longer pending (e.g. on successful connect or explicit close). + bool RemovePending(string battleId); +} diff --git a/SVSim.BattleNode/Sessions/InMemoryBattleSessionStore.cs b/SVSim.BattleNode/Sessions/InMemoryBattleSessionStore.cs new file mode 100644 index 0000000..2f14512 --- /dev/null +++ b/SVSim.BattleNode/Sessions/InMemoryBattleSessionStore.cs @@ -0,0 +1,17 @@ +using System.Collections.Concurrent; + +namespace SVSim.BattleNode.Sessions; + +public sealed class InMemoryBattleSessionStore : IBattleSessionStore +{ + private readonly ConcurrentDictionary _pending = new(); + + public void RegisterPending(PendingBattle battle) => + _pending[battle.BattleId] = battle; + + public PendingBattle? TryGetPending(string battleId) => + _pending.TryGetValue(battleId, out var b) ? b : null; + + public bool RemovePending(string battleId) => + _pending.TryRemove(battleId, out _); +} diff --git a/SVSim.BattleNode/Sessions/PendingBattle.cs b/SVSim.BattleNode/Sessions/PendingBattle.cs new file mode 100644 index 0000000..28dbb88 --- /dev/null +++ b/SVSim.BattleNode/Sessions/PendingBattle.cs @@ -0,0 +1,7 @@ +namespace SVSim.BattleNode.Sessions; + +/// +/// Sparse pre-connect record: enough to validate the incoming WS connect and resolve +/// the viewer. Full BattleSession is created on connect. +/// +public sealed record PendingBattle(string BattleId, long ViewerId); diff --git a/SVSim.UnitTests/BattleNode/Sessions/InMemoryBattleSessionStoreTests.cs b/SVSim.UnitTests/BattleNode/Sessions/InMemoryBattleSessionStoreTests.cs new file mode 100644 index 0000000..7582ea0 --- /dev/null +++ b/SVSim.UnitTests/BattleNode/Sessions/InMemoryBattleSessionStoreTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using SVSim.BattleNode.Sessions; + +namespace SVSim.UnitTests.BattleNode.Sessions; + +[TestFixture] +public class InMemoryBattleSessionStoreTests +{ + private InMemoryBattleSessionStore _store = null!; + + [SetUp] public void Setup() => _store = new InMemoryBattleSessionStore(); + + [Test] + public void RegisterThenGet_ReturnsRegisteredBattle() + { + var battle = new PendingBattle("bid-1", 906243102); + _store.RegisterPending(battle); + + Assert.That(_store.TryGetPending("bid-1"), Is.EqualTo(battle)); + } + + [Test] + public void Get_UnknownBattleId_ReturnsNull() + { + Assert.That(_store.TryGetPending("nope"), Is.Null); + } + + [Test] + public void Remove_ReturnsTrueWhenPresent_FalseWhenAbsent() + { + _store.RegisterPending(new PendingBattle("bid", 1)); + Assert.That(_store.RemovePending("bid"), Is.True); + Assert.That(_store.RemovePending("bid"), Is.False); + } + + [Test] + public void Register_DuplicateBattleId_OverwritesPrior() + { + _store.RegisterPending(new PendingBattle("bid", 1)); + _store.RegisterPending(new PendingBattle("bid", 2)); + Assert.That(_store.TryGetPending("bid")!.ViewerId, Is.EqualTo(2)); + } +}