using System.Collections.Concurrent; namespace EmpyrionBridge; // Lazy connection pool keyed by host:port. Reuses an EPM TCP connection // across HTTP requests; drops it after IdleTtl with no traffic. public class BridgePool : IAsyncDisposable { public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(8); public TimeSpan IdleTtl { get; init; } = TimeSpan.FromMinutes(5); readonly ConcurrentDictionary _entries = new(); readonly ILoggerFactory _logFactory; readonly ILogger _log; readonly Timer _reaper; public BridgePool(ILoggerFactory logFactory) { _logFactory = logFactory; _log = logFactory.CreateLogger(); _reaper = new Timer(Reap, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); } public async Task GetAsync(string host, int port) { var key = $"{host}:{port}"; Entry e; lock (_entries) { if (!_entries.TryGetValue(key, out e)) { e = new Entry { Conn = new BridgeConnection(host, port, _logFactory.CreateLogger("EPM")) }; _entries[key] = e; } e.LastUsed = DateTime.UtcNow; } if (!e.Conn.IsConnected) { await e.ConnectGate.WaitAsync(); try { if (!e.Conn.IsConnected) { await e.Conn.ConnectAsync(ConnectTimeout); } } finally { e.ConnectGate.Release(); } } return e.Conn; } void Reap(object _) { var cutoff = DateTime.UtcNow - IdleTtl; foreach (var kv in _entries.ToArray()) { if (kv.Value.LastUsed < cutoff) { if (_entries.TryRemove(kv.Key, out var e)) { _log.LogInformation("reaping idle connection {Key}", kv.Key); e.Conn.Dispose(); } } } } public async ValueTask DisposeAsync() { await _reaper.DisposeAsync(); foreach (var e in _entries.Values) e.Conn.Dispose(); _entries.Clear(); } class Entry { public BridgeConnection Conn = null!; public DateTime LastUsed = DateTime.UtcNow; public SemaphoreSlim ConnectGate = new(1, 1); } }