using System.Collections.Concurrent;
using SVSim.BattleNode.Sessions.Participants;
namespace SVSim.BattleNode.Hosting;
///
/// In-process . Backed by a ConcurrentDictionary of slots
/// keyed by BattleId. Each slot holds the first arriver's RealParticipant and a
/// TaskCompletionSource that gets set when the second arriver Pairs (or cancelled
/// on timeout / abort).
///
public sealed class WaitingRoom : IWaitingRoom
{
private readonly ConcurrentDictionary _rooms = new();
public RealParticipant? Pair(string battleId, RealParticipant self)
{
if (!_rooms.TryRemove(battleId, out var slot)) return null;
// Hand `self` (second arriver) to the first arriver's ParkAsync...
slot.SecondArriverTcs.TrySetResult(self);
// ...and return the first arriver to the second arriver's handler.
return slot.FirstArriver;
}
public async Task ParkAsync(string battleId, RealParticipant self,
TimeSpan timeout, CancellationToken ct)
{
var slot = new Slot(self);
if (!_rooms.TryAdd(battleId, slot))
{
// Race: a concurrent Park already created a slot for the same BattleId.
// The bridge mints a fresh BattleId per registration, so this is rare;
// caller can re-Pair as insurance.
return null;
}
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(timeout);
using var reg = timeoutCts.Token.Register(() => slot.SecondArriverTcs.TrySetCanceled());
try
{
return await slot.SecondArriverTcs.Task;
}
catch (OperationCanceledException)
{
Evict(battleId);
return null;
}
}
public void Evict(string battleId) => _rooms.TryRemove(battleId, out _);
private sealed class Slot
{
public RealParticipant FirstArriver { get; }
public TaskCompletionSource SecondArriverTcs { get; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public Slot(RealParticipant first) => FirstArriver = first;
}
}