using System.Net.WebSockets; using System.Text.Json; using Serilog; namespace Myriad.Gateway; public class ShardConnection: IAsyncDisposable { private readonly ILogger _logger; private readonly ShardPacketSerializer _serializer; private ClientWebSocket? _client; private int _id; public ShardConnection(JsonSerializerOptions jsonSerializerOptions, ILogger logger, int id) { _logger = logger.ForContext(); _serializer = new ShardPacketSerializer(jsonSerializerOptions); _id = id; } public WebSocketState State => _client?.State ?? WebSocketState.Closed; public WebSocketCloseStatus? CloseStatus => _client?.CloseStatus; public string? CloseStatusDescription => _client?.CloseStatusDescription; public async ValueTask DisposeAsync() { await CloseInner(WebSocketCloseStatus.NormalClosure, null); _client?.Dispose(); } public async Task Connect(string url, CancellationToken ct) { _client?.Dispose(); _client = new ClientWebSocket(); await _client.ConnectAsync(GetConnectionUri(url), ct); } public async Task Disconnect(WebSocketCloseStatus closeStatus, string? reason) { await CloseInner(closeStatus, reason); } public async Task Send(GatewayPacket packet) { // from `ManagedWebSocket.s_validSendStates` if (_client is not { State: WebSocketState.Open or WebSocketState.CloseReceived }) return; try { await _serializer.WritePacket(_client, packet); } catch (Exception e) { _logger.Error(e, "Shard {ShardId}: Error sending WebSocket message"); } } public async Task Read() { // from `ManagedWebSocket.s_validReceiveStates` if (_client is not { State: WebSocketState.Open or WebSocketState.CloseSent }) return null; try { var (_, packet) = await _serializer.ReadPacket(_client); return packet; } catch (Exception e) { _logger.Error(e, "Shard {ShardId}: Error reading from WebSocket"); // force close so we can "reset" await CloseInner(WebSocketCloseStatus.NormalClosure, null); } return null; } private Uri GetConnectionUri(string baseUri) => new UriBuilder(baseUri) { Query = "v=10&encoding=json" }.Uri; private async Task CloseInner(WebSocketCloseStatus closeStatus, string? description) { if (_client == null) return; var client = _client; _client = null; // from `ManagedWebSocket.s_validCloseStates` if (client.State is WebSocketState.Open or WebSocketState.CloseReceived or WebSocketState.CloseSent) { // Close with timeout, mostly to work around https://github.com/dotnet/runtime/issues/51590 var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); try { await client.CloseAsync(closeStatus, description, cts.Token); } catch (Exception e) { _logger.Error(e, "Shard {ShardId}: Error closing WebSocket connection"); } } // This shouldn't need to be wrapped in a try/catch but doing it anyway :/ try { client.Dispose(); } catch (Exception e) { _logger.Error(e, "Shard {ShardId}: Error disposing WebSocket connection"); } } }