using Microsoft.AspNetCore.Http.Extensions; using Shogi.Contracts.Socket; using Shogi.Contracts.Types; using System.Buffers; using System.Net.WebSockets; using System.Text.Json; using static Shogi.UI.Shared.Events; namespace Shogi.UI.Shared; public class ShogiSocket : IDisposable { public event AsyncEventHandler? OnSessionCreated; public event AsyncEventHandler? OnSessionJoined; public event AsyncEventHandler? OnPlayerMoved; private readonly ClientWebSocket socket; private readonly JsonSerializerOptions serializerOptions; private readonly UriBuilder uriBuilder; private readonly CancellationTokenSource cancelToken; private readonly IMemoryOwner memoryOwner; private bool disposedValue; public ShogiSocket(IConfiguration configuration, ClientWebSocket socket, JsonSerializerOptions serializerOptions) { this.socket = socket; this.serializerOptions = serializerOptions; this.uriBuilder = new UriBuilder(configuration["SocketUrl"] ?? throw new InvalidOperationException("SocketUrl configuration is missing.")); this.cancelToken = new CancellationTokenSource(); this.memoryOwner = MemoryPool.Shared.Rent(1024 * 2); } public async Task OpenAsync(string token) { uriBuilder.Query = new QueryBuilder { { "token", token } }.ToQueryString().Value; await socket.ConnectAsync(this.uriBuilder.Uri, cancelToken.Token); // Fire and forget! I'm way too lazy to write my own javascript interop to a web worker. Nooo thanks. _ = Listen().ContinueWith(async antecedent => { this.cancelToken.Cancel(); await this.socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Page was probably closed or refresh.", CancellationToken.None); if (antecedent.Exception != null) { throw antecedent.Exception; } }, TaskContinuationOptions.OnlyOnFaulted); } private async Task Listen() { while (socket.State == WebSocketState.Open && !cancelToken.IsCancellationRequested) { var result = await socket.ReceiveAsync(this.memoryOwner.Memory, cancelToken.Token); var memory = this.memoryOwner.Memory[..result.Count].ToArray(); var action = JsonDocument .Parse(memory) .RootElement .GetProperty(nameof(ISocketMessage.Action)) .Deserialize(); Console.WriteLine($"Socket action: {action}"); switch (action) { case SocketAction.SessionCreated: if (this.OnSessionCreated is not null) { await this.OnSessionCreated(); } break; case SocketAction.SessionJoined: if (this.OnSessionJoined is not null) { var args = JsonSerializer.Deserialize(memory, serializerOptions); await this.OnSessionJoined(args!); } break; case SocketAction.PieceMoved: if (this.OnPlayerMoved is not null) { var args = JsonSerializer.Deserialize(memory, serializerOptions); await this.OnPlayerMoved(args!); } break; default: throw new NotImplementedException($"Socket message for action:{action} is not implemented."); } } await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Socket closed because cancellation token was cancelled.", CancellationToken.None); if (!cancelToken.IsCancellationRequested) { throw new InvalidOperationException("Stopped socket listening without cancelling."); } } protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { cancelToken.Cancel(); socket.Dispose(); memoryOwner.Dispose(); } disposedValue = true; } } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } }