RegisterPending → TryRegisterPending (TryAdd instead of indexer) so battle-id collisions return false instead of silently evicting a live battle. MatchingBridge retries with fresh IDs on collision (max 5). Before registering, EvictStaleForViewer removes any stale pending battle the viewer left behind, enforcing the one-pending-per-viewer invariant that was previously comment-asserted. Store tests switched to per-test local stores to fix a race under the assembly-wide ParallelScope.All. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
33 lines
1.2 KiB
C#
33 lines
1.2 KiB
C#
using System.Collections.Concurrent;
|
|
|
|
namespace SVSim.BattleNode.Sessions;
|
|
|
|
public sealed class InMemoryBattleSessionStore : IBattleSessionStore
|
|
{
|
|
private readonly ConcurrentDictionary<string, PendingBattle> _pending = new();
|
|
|
|
public bool TryRegisterPending(PendingBattle battle) =>
|
|
_pending.TryAdd(battle.BattleId, battle);
|
|
|
|
public PendingBattle? TryGetPending(string battleId) =>
|
|
_pending.TryGetValue(battleId, out var b) ? b : null;
|
|
|
|
public PendingBattle? TryFindPendingForViewer(long viewerId)
|
|
{
|
|
// Linear scan — _pending is bounded by concurrent in-flight matches (low
|
|
// double digits at most), so this stays cheap. Returns whichever match the
|
|
// dictionary's enumerator yields first; in practice a viewer has at most one
|
|
// pending battle since each /do_matching either pairs/falls-back the existing
|
|
// slot or parks without registering.
|
|
foreach (var b in _pending.Values)
|
|
{
|
|
if (b.P1.ViewerId == viewerId) return b;
|
|
if (b.P2 is not null && b.P2.ViewerId == viewerId) return b;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public bool RemovePending(string battleId) =>
|
|
_pending.TryRemove(battleId, out _);
|
|
}
|