Files
Shogi/Shogi.UI/Shared/ShogiSocket.cs
2024-01-29 13:00:20 -06:00

132 lines
5.1 KiB
C#

using Microsoft.AspNetCore.WebUtilities;
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<SessionJoinedByPlayerSocketMessage>? OnSessionJoined;
public event AsyncEventHandler<PlayerHasMovedMessage>? OnPlayerMoved;
private ClientWebSocket socket;
private readonly JsonSerializerOptions serializerOptions;
private readonly string baseUrl;
private readonly CancellationTokenSource cancelToken;
private readonly IMemoryOwner<byte> memoryOwner;
private bool disposedValue;
public ShogiSocket(IConfiguration configuration, JsonSerializerOptions serializerOptions)
{
this.socket = new ClientWebSocket();
this.serializerOptions = serializerOptions;
this.baseUrl = configuration["SocketUrl"] ?? throw new InvalidOperationException("SocketUrl configuration is missing.");
this.cancelToken = new CancellationTokenSource();
this.memoryOwner = MemoryPool<byte>.Shared.Rent(1024 * 2);
}
public async Task OpenAsync(string token)
{
if (this.socket.State == WebSocketState.Open)
{
await this.socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing before opening a new connection.", CancellationToken.None);
}
if (this.socket.State == WebSocketState.Closed)
{
this.socket.Dispose();
this.socket = new ClientWebSocket(); // Because you can't reopen a closed socket.
}
else
{
Console.WriteLine("Opening socket and existing socket state is " + this.socket.State.ToString());
}
var uri = new Uri(QueryHelpers.AddQueryString(this.baseUrl, "token", token), UriKind.Absolute);
await socket.ConnectAsync(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)
.ConfigureAwait(false);
}
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<SocketAction>();
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<SessionJoinedByPlayerSocketMessage>(memory, serializerOptions);
await this.OnSessionJoined(args!);
}
break;
case SocketAction.PieceMoved:
if (this.OnPlayerMoved is not null)
{
var args = JsonSerializer.Deserialize<PlayerHasMovedMessage>(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)
{
//socket.Dispose(); // This is handled by the DI container.
cancelToken.Cancel();
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);
}
}