~ruther/NosSmooth.Comms

5164e0aff7ef406c32e42516ce4790920af2ab83 — František Boháček 2 years ago e9cd466
feat: add abstractions and core of communication
25 files changed, 1243 insertions(+), 0 deletions(-)

M NosSmooth.Comms.sln
A src/Core/NosSmooth.Comms.Abstractions/IClient.cs
A src/Core/NosSmooth.Comms.Abstractions/IConnection.cs
A src/Core/NosSmooth.Comms.Abstractions/IServer.cs
A src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs
A src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs
A src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs
A src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs
A src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs
A src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs
A src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs
A src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj
A src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs
A src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs
A src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs
A src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs
A src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs
A src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs
A src/Core/NosSmooth.Comms.Core/MessageHandler.cs
A src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj
A src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs
A src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs
A src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs
A src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs
A src/Core/NosSmooth.Comms.Core/ServerManager.cs
M NosSmooth.Comms.sln => NosSmooth.Comms.sln +66 -0
@@ 1,8 1,74 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Abstractions", "src\Core\NosSmooth.Comms.Abstractions\NosSmooth.Comms.Abstractions.csproj", "{D3B6C5ED-9291-4215-8F8A-F3530849B896}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Local", "src\Local\NosSmooth.Comms.Local\NosSmooth.Comms.Local.csproj", "{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.NamedPipes", "src\Core\NosSmooth.Comms.NamedPipes\NosSmooth.Comms.NamedPipes.csproj", "{9621D790-97FC-4E01-BB0C-CA1F33B4C934}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Injector", "src\Local\NosSmooth.Comms.Injector\NosSmooth.Comms.Injector.csproj", "{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{EFE65101-4414-4966-813D-90FE3736F6B1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Local", "Local", "{1AD3F38F-67A8-472D-8AF3-875C6F93EC16}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{711B1BE4-9AE3-4A9D-A5F2-3434D057870E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsolePacketLogger", "src\Samples\ConsolePacketLogger\ConsolePacketLogger.csproj", "{A154A209-B86F-4180-B329-9B51F5FCD99F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.LocalData", "src\Local\NosSmooth.Comms.LocalData\NosSmooth.Comms.LocalData.csproj", "{8749D2C0-6253-4A88-BA38-55B7C10D074B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Core", "src\Core\NosSmooth.Comms.Core\NosSmooth.Comms.Core.csproj", "{70275C91-1114-4673-8F9B-B0C311BFE337}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Tcp", "src\Core\NosSmooth.Comms.Tcp\NosSmooth.Comms.Tcp.csproj", "{04F43EA9-BC90-446F-8272-90705CAE9C27}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{D3B6C5ED-9291-4215-8F8A-F3530849B896}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{D3B6C5ED-9291-4215-8F8A-F3530849B896}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{D3B6C5ED-9291-4215-8F8A-F3530849B896}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{D3B6C5ED-9291-4215-8F8A-F3530849B896}.Release|Any CPU.Build.0 = Release|Any CPU
		{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}.Release|Any CPU.Build.0 = Release|Any CPU
		{9621D790-97FC-4E01-BB0C-CA1F33B4C934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{9621D790-97FC-4E01-BB0C-CA1F33B4C934}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{9621D790-97FC-4E01-BB0C-CA1F33B4C934}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{9621D790-97FC-4E01-BB0C-CA1F33B4C934}.Release|Any CPU.Build.0 = Release|Any CPU
		{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}.Release|Any CPU.Build.0 = Release|Any CPU
		{A154A209-B86F-4180-B329-9B51F5FCD99F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{A154A209-B86F-4180-B329-9B51F5FCD99F}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{A154A209-B86F-4180-B329-9B51F5FCD99F}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{A154A209-B86F-4180-B329-9B51F5FCD99F}.Release|Any CPU.Build.0 = Release|Any CPU
		{8749D2C0-6253-4A88-BA38-55B7C10D074B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{8749D2C0-6253-4A88-BA38-55B7C10D074B}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{8749D2C0-6253-4A88-BA38-55B7C10D074B}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{8749D2C0-6253-4A88-BA38-55B7C10D074B}.Release|Any CPU.Build.0 = Release|Any CPU
		{70275C91-1114-4673-8F9B-B0C311BFE337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{70275C91-1114-4673-8F9B-B0C311BFE337}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{70275C91-1114-4673-8F9B-B0C311BFE337}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{70275C91-1114-4673-8F9B-B0C311BFE337}.Release|Any CPU.Build.0 = Release|Any CPU
		{04F43EA9-BC90-446F-8272-90705CAE9C27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{04F43EA9-BC90-446F-8272-90705CAE9C27}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{04F43EA9-BC90-446F-8272-90705CAE9C27}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{04F43EA9-BC90-446F-8272-90705CAE9C27}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(NestedProjects) = preSolution
		{D3B6C5ED-9291-4215-8F8A-F3530849B896} = {EFE65101-4414-4966-813D-90FE3736F6B1}
		{9621D790-97FC-4E01-BB0C-CA1F33B4C934} = {EFE65101-4414-4966-813D-90FE3736F6B1}
		{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4} = {1AD3F38F-67A8-472D-8AF3-875C6F93EC16}
		{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC} = {1AD3F38F-67A8-472D-8AF3-875C6F93EC16}
		{A154A209-B86F-4180-B329-9B51F5FCD99F} = {711B1BE4-9AE3-4A9D-A5F2-3434D057870E}
		{8749D2C0-6253-4A88-BA38-55B7C10D074B} = {1AD3F38F-67A8-472D-8AF3-875C6F93EC16}
		{70275C91-1114-4673-8F9B-B0C311BFE337} = {EFE65101-4414-4966-813D-90FE3736F6B1}
		{04F43EA9-BC90-446F-8272-90705CAE9C27} = {EFE65101-4414-4966-813D-90FE3736F6B1}
	EndGlobalSection
EndGlobal

A src/Core/NosSmooth.Comms.Abstractions/IClient.cs => src/Core/NosSmooth.Comms.Abstractions/IClient.cs +23 -0
@@ 0,0 1,23 @@
//
//  IClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Data;
using Remora.Results;

namespace NosSmooth.Comms.Data;

/// <summary>
/// An abstraction for a client connection.
/// </summary>
public interface IClient : IConnection
{
    /// <summary>
    /// Connect to the server.
    /// </summary>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> ConnectAsync(CancellationToken ct = default);
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/IConnection.cs => src/Core/NosSmooth.Comms.Abstractions/IConnection.cs +35 -0
@@ 0,0 1,35 @@
//
//  IConnection.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Data;

namespace NosSmooth.Comms.Data;

/// <summary>
/// A connection, either a client or a server connection.
/// </summary>
public interface IConnection
{
    /// <summary>
    /// Gets the state of the connection.
    /// </summary>
    public ConnectionState State { get; }

    /// <summary>
    /// Gets the stream used for reading the data received.
    /// </summary>
    public Stream ReadStream { get; }

    /// <summary>
    /// Gets the stream used for writing data.
    /// </summary>
    public Stream WriteStream { get; }

    /// <summary>
    /// Disconnect, close the connection.
    /// </summary>
    public void Disconnect();
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/IServer.cs => src/Core/NosSmooth.Comms.Abstractions/IServer.cs +40 -0
@@ 0,0 1,40 @@
//
//  IServer.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.ComponentModel;
using Remora.Results;

namespace NosSmooth.Comms.Data;

/// <summary>
/// An abstraction for a server.
/// </summary>
public interface IServer
{
    /// <summary>
    /// Gets the clients connected to the server.
    /// </summary>
    public IReadOnlyList<IConnection> Clients { get; }

    /// <summary>
    /// Listen for a new connection and wait for it.
    /// </summary>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A client connection, returned after the client has connected.</returns>
    public Task<Result<IConnection>> WaitForConnectionAsync(CancellationToken ct = default);

    /// <summary>
    /// Start the server.
    /// </summary>
    /// <param name="stopToken">The token used for stopping the server. <see cref="CloseAsync"/> may also be used.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> ListenAsync(CancellationToken stopToken = default);

    /// <summary>
    /// Close all connections, stop listening.
    /// </summary>
    public void Close();
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs +11 -0
@@ 0,0 1,11 @@
//
//  CommandMessage.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Core.Commands;

namespace NosSmooth.Comms.Data.Messages;

public record CommandMessage(ICommand Command);
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs +9 -0
@@ 0,0 1,9 @@
//
//  HandshakeRequest.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Comms.Data.Messages;

public record HandshakeRequest(bool SendRawPackets, bool SendDeserializedPackets);
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs +9 -0
@@ 0,0 1,9 @@
//
//  HandshakeResponse.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Comms.Data.Messages;

public record HandshakeResponse(long? CharacterId, string? CharacterName);
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs +9 -0
@@ 0,0 1,9 @@
//
//  MessageWrapper.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Comms.Data.Messages;

public record MessageWrapper<TMessage>(long ProtocolVersion, long MessageId, TMessage Data);
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs +12 -0
@@ 0,0 1,12 @@
//
//  PacketMessage.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Packets;
using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Comms.Data.Messages;

public record PacketMessage(PacketSource Source, IPacket Packet);
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs +11 -0
@@ 0,0 1,11 @@
//
//  RawPacketMessage.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.PacketSerializer.Abstractions.Attributes;

namespace NosSmooth.Comms.Data.Messages;

public record RawPacketMessage(PacketSource Source, string Packet);
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs +11 -0
@@ 0,0 1,11 @@
//
//  ResponseResult.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Remora.Results;

namespace NosSmooth.Comms.Data.Messages;

public record ResponseResult(long MessageId, Result Result);
\ No newline at end of file

A src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj => src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj +22 -0
@@ 0,0 1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <RootNamespace>NosSmooth.Comms.Data</RootNamespace>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
      <PackageReference Include="NosSmooth.Packets" Version="3.5.0" />
      <PackageReference Include="NosSmooth.PacketSerializer.Abstractions" Version="1.3.0" />
    </ItemGroup>

    <ItemGroup>
      <Reference Include="NosSmooth.Core">
        <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.core\3.3.1\lib\net7.0\NosSmooth.Core.dll</HintPath>
      </Reference>
    </ItemGroup>

</Project>

A src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs => src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs +24 -0
@@ 0,0 1,24 @@
//
//  IMessageResponder.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Remora.Results;

namespace NosSmooth.Comms.Data.Responders;

/// <summary>
/// A responder to a received message from client or server.
/// </summary>
/// <typeparam name="TMessage">The type of the message to respond to.</typeparam>
public interface IMessageResponder<TMessage>
{
    /// <summary>
    /// Respond to the given message.
    /// </summary>
    /// <param name="message">The message received.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> Respond(TMessage message, CancellationToken ct = default);
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs => src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs +81 -0
@@ 0,0 1,81 @@
//
//  ClientNostaleClient.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Packets;
using NosSmooth.PacketSerializer;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using Remora.Results;

namespace NosSmooth.Comms.Core;

/// <summary>
/// A NosTale client using <see cref="IClient"/>.
/// </summary>
public class ClientNostaleClient : INostaleClient
{
    private readonly ConnectionHandler _connection;

    /// <summary>
    /// Initializes a new instance of the <see cref="ClientNostaleClient"/> class.
    /// </summary>
    /// <param name="connection">The connection handler.</param>
    public ClientNostaleClient
        (ConnectionHandler connection)
    {
        _connection = connection;
    }

    /// <inheritdoc />
    public Task<Result> RunAsync(CancellationToken stopRequested = default)
    {
        _connection.StartHandler(stopRequested);
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public async Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new PacketMessage(PacketSource.Client, packet), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
    }

    /// <inheritdoc />
    public async Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new RawPacketMessage(PacketSource.Client, packetString), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
    }

    /// <inheritdoc />
    public async Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new RawPacketMessage(PacketSource.Server, packetString), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
    }

    /// <inheritdoc />
    public async Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new PacketMessage(PacketSource.Server, packet), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
    }

    /// <inheritdoc />
    public async Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new CommandMessage(command), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
    }
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs => src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs +184 -0
@@ 0,0 1,184 @@
//
//  ConnectionHandler.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Core.Contracts;
using NosSmooth.Core.Extensions;
using Remora.Results;

namespace NosSmooth.Comms.Core;

/// <summary>
/// Manages a connection, calls message handler when message is received.
/// Serializes and deserializes the messages from the stream.
/// </summary>
public class ConnectionHandler
{
    private readonly Contractor? _contractor;
    private readonly IConnection _connection;
    private readonly MessageHandler _messageHandler;
    private readonly MessagePackSerializerOptions _options;
    private readonly ILogger<ConnectionHandler> _logger;
    private long _messageId = 1;
    private Task<Result>? _task;

    /// <summary>
    /// Initializes a new instance of the <see cref="ConnectionHandler"/> class.
    /// </summary>
    /// <param name="contractor">The contractor.</param>
    /// <param name="connection">The connection.</param>
    /// <param name="messageHandler">The message handler.</param>
    /// <param name="options">The options.</param>
    /// <param name="logger">The logger.</param>
    public ConnectionHandler
    (
        Contractor? contractor,
        IConnection connection,
        MessageHandler messageHandler,
        IOptions<NosSmoothMessageSerializerOptions> options,
        ILogger<ConnectionHandler> logger
    )
    {
        _contractor = contractor;
        _connection = connection;
        _messageHandler = messageHandler;
        _options = options.Value;
        _logger = logger;
    }

    /// <summary>
    /// Gets the connection.
    /// </summary>
    public IConnection Connection => _connection;

    /// <summary>
    /// Run the handler and await the task.
    /// </summary>
    /// <param name="stopToken">The token used for stopping the handler and disconnecting the connection.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> RunHandlerAsync(CancellationToken stopToken)
    {
        StartHandler(stopToken);
        return _task!;
    }

    /// <summary>
    /// Start the connection handler task, do not wait for it.
    /// </summary>
    /// <param name="stopToken">The token used for stopping/disconnecting the connection and handling.</param>
    public void StartHandler(CancellationToken stopToken)
    {
        if (_task is not null)
        {
            return;
        }

        _task = HandlerTask(stopToken);
    }

    private async Task<Result> HandlerTask(CancellationToken ct)
    {
        using var reader = new MessagePackStreamReader(_connection.ReadStream, true);
        while (!ct.IsCancellationRequested)
        {
            try
            {
                var read = await reader.ReadAsync(ct);
                if (!read.HasValue)
                {
                    _logger.LogWarning("Message not read? ...");
                    continue;
                }

                var message = MessagePackSerializer.Typeless.Deserialize
                    (read.Value, _options, ct);
                var result = await _messageHandler.HandleMessageAsync(this, message, ct);

                if (!result.IsSuccess)
                {
                    _logger.LogResultError(result);
                }
            }
            catch (Exception e)
            {
                _logger.LogError(e, "An exception was thrown during deserialization of a message.");
            }
        }

        _connection.Disconnect();
        return Result.FromSuccess();
    }

    /// <summary>
    /// Create a contract for sending a message,
    /// <see cref="ResponseResult"/> will be returned back.
    /// </summary>
    /// <param name="message">The message.</param>
    /// <typeparam name="TMessage">The type of the message.</typeparam>
    /// <returns>A contract representing send message operation.</returns>
    /// <exception cref="InvalidOperationException">Thrown in case contract is created on the server. Clients do not send responses.</exception>
    public IContract<Result, DefaultStates> ContractSendMessage<TMessage>(TMessage message)
    {
        if (_contractor is null)
        {
            throw new InvalidOperationException
            (
                "Contracting is not supported, the other side does not send responses. Only server sends responses back."
            );
        }

        long messageId = 0;
        return new ContractBuilder<Result, DefaultStates, NoErrors>(_contractor, DefaultStates.None)
            .SetMoveAction
            (
                DefaultStates.None,
                async (a, ct) =>
                {
                    var result = await SendMessageAsync(message, ct);
                    if (!result.IsDefined(out messageId))
                    {
                        return Result<bool>.FromError(result);
                    }

                    return true;
                },
                DefaultStates.Requested
            )
            .SetMoveFilter<ResponseResult>
                (DefaultStates.Requested, (r) => r.MessageId == messageId, DefaultStates.ResponseObtained)
            .SetFillData<ResponseResult>(DefaultStates.ResponseObtained, r => r.Result)
            .Build();
    }

    /// <summary>
    /// Send  message to the other end.
    /// </summary>
    /// <param name="message">The message to send. It will be wrapped before sending.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <typeparam name="TMessage">Type of the message to send.</typeparam>
    /// <returns>The id of the message sent or an error.</returns>
    public async Task<Result<long>> SendMessageAsync<TMessage>(TMessage message, CancellationToken ct = default)
    {
        var messageId = _messageId++;
        var messageWrapper = new MessageWrapper<TMessage>(1, messageId, message);

        try
        {
            await MessagePackSerializer.Typeless.SerializeAsync(_connection.WriteStream, messageWrapper, _options, ct);
            await _connection.WriteStream.FlushAsync(ct);
        }
        catch (Exception e)
        {
            return e;
        }

        return messageId;
    }
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs => src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs +25 -0
@@ 0,0 1,25 @@
//
//  ConnectionInjector.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Comms.Data;

namespace NosSmooth.Comms.Core;

/// <summary>
/// Injects connection and connection handler into dependency injection.
/// </summary>
public class ConnectionInjector
{
    /// <summary>
    /// Gets or sets the connection.
    /// </summary>
    public IConnection? Connection { get; set; }

    /// <summary>
    /// Gets or sets the connection handler.
    /// </summary>
    public ConnectionHandler? ConnectionHandler { get; set; }
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs => src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs +166 -0
@@ 0,0 1,166 @@
//
//  ServiceCollectionExtensions.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Comms.Core.Responders;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Responders;
using NosSmooth.Core.Client;

namespace NosSmooth.Comms.Core.Extensions;

/// <summary>
/// Extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Adds server handling (<see cref="MessageHandler"/> and <see cref="ServerManager"/>).
    /// </summary>
    /// <remarks>
    /// The specific server has to be added separately as <see cref="IServer"/>.
    /// </remarks>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The same service collection.</returns>
    public static IServiceCollection AddServerHandling(this IServiceCollection serviceCollection)
        => serviceCollection
            .AddNosSmoothResolverOptions()
            .AddSingleton<MessageHandler>(p => new MessageHandler(p, true))
            .AddSingleton<ServerManager>()
            .AddInjecting();

    /// <summary>
    /// Adds handling for a single client.
    /// </summary>
    /// <remarks>
    /// The specific client has to be added separately as <see cref="IConnection"/>.
    /// </remarks>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The same service collection.</returns>
    public static IServiceCollection AddSingleClientHandling(this IServiceCollection serviceCollection)
        => serviceCollection
            .AddInjecting()
            .AddNosSmoothResolverOptions()
            .AddMessageResponder<ResponseResultResponder>()
            .AddSingleton<NostaleClientResolver>()
            .AddSingleton<IConnection>(p => p.GetRequiredService<IClient>())
            .AddSingleton<MessageHandler>(p => new MessageHandler(p, false))
            .AddScoped<ConnectionHandler>()
            .AddScoped<INostaleClient, ClientNostaleClient>();

    /// <summary>
    /// Add handling for multiple clients.
    /// </summary>
    /// <remarks>
    /// The clients should not be inside of the provider.
    /// Initialize clients outside of the provider and use the
    /// provider for injecting connection handler and nostale client.
    /// Nostale client will be created automatically if connection is injected successfully.
    /// Connection will be injected when calling message handler with the specific connection.
    ///
    /// Connection may be injected by setting <see cref="ConnectionInjector"/> properties in a scope.
    /// </remarks>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The same service collection.</returns>
    public static IServiceCollection AddMultiClientHandling(this IServiceCollection serviceCollection)
        => serviceCollection
            .AddNosSmoothResolverOptions()
            .AddSingleton<NostaleClientResolver>()
            .AddSingleton<MessageHandler>(p => new MessageHandler(p, false))
            .AddInjecting()
            .AddScoped<INostaleClient>
                (p => p.GetRequiredService<NostaleClientResolver>().Resolve(p.GetRequiredService<ConnectionHandler>()));

    /// <summary>
    /// Add <see cref="NosSmoothMessageSerializerOptions"/> with default NosSmooth options.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The same service collection.</returns>
    public static IServiceCollection AddNosSmoothResolverOptions(this IServiceCollection serviceCollection)
        => serviceCollection
            .Configure<NosSmoothMessageSerializerOptions>
                (o => o.Options = o.Options.WithResolver(NosSmoothResolver.Instance));

    /// <summary>
    /// Adds a message responder.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <typeparam name="TResponder">The type of the responder.</typeparam>
    /// <returns>The same service collection.</returns>
    public static IServiceCollection AddMessageResponder<TResponder>(this IServiceCollection serviceCollection)
    {
        return serviceCollection.AddMessageResponder(typeof(TResponder));
    }

    /// <summary>
    /// Adds a message responder.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <param name="responderType">The type of the responder.</param>
    /// <returns>The same service collection.</returns>
    public static IServiceCollection AddMessageResponder(this IServiceCollection serviceCollection, Type responderType)
    {
        if (serviceCollection.Any(x => x.ImplementationType == responderType))
        { // already added... assuming every packet responder was added even though that may not be the case.
            return serviceCollection;
        }

        if (!responderType.GetInterfaces().Any
            (
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMessageResponder<>)
            ))
        {
            throw new ArgumentException
            (
                $"{nameof(responderType)} should implement IMessageResponder.",
                nameof(responderType)
            );
        }

        var responderTypeInterfaces = responderType.GetInterfaces();
        var responderInterfaces = responderTypeInterfaces.Where
        (
            r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(IMessageResponder<>)
        );

        foreach (var responderInterface in responderInterfaces)
        {
            serviceCollection.AddScoped(responderInterface, responderType);
        }

        return serviceCollection;
    }

    private static IServiceCollection AddInjecting(this IServiceCollection serviceCollection)
        => serviceCollection
            .AddScoped<ConnectionInjector>()
            .AddScoped<ConnectionHandler>
            (
                p =>
                {
                    var handler = p.GetRequiredService<ConnectionInjector>().ConnectionHandler;
                    if (handler is null)
                    {
                        throw new InvalidOperationException("Connection handler was requested, but is not injected.");
                    }

                    return handler;
                }
            )
            .AddScoped<IConnection>
            (
                p =>
                {
                    var connection = p.GetRequiredService<ConnectionInjector>().Connection;
                    if (connection is null)
                    {
                        throw new InvalidOperationException("Connection was requested, but is not injected.");
                    }

                    return connection;
                }
            );
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs => src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs +46 -0
@@ 0,0 1,46 @@
//
//  NameStringFormatter.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Text;
using MessagePack;
using MessagePack.Formatters;
using NosSmooth.PacketSerializer.Abstractions.Common;

namespace NosSmooth.Comms.Core.Formatters;

/// <summary>
/// A formatter for <see cref="NameString"/>.
/// </summary>
public class NameStringFormatter : IMessagePackFormatter<NameString?>
{
    /// <inheritdoc />
    public void Serialize(ref MessagePackWriter writer, NameString? value, MessagePackSerializerOptions options)
    {
        if (value is null)
        {
            writer.WriteNil();
            return;
        }

        var bytes = Encoding.UTF8.GetBytes(value.Name);
        writer.WriteString(bytes);
    }

    /// <inheritdoc />
    public NameString? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
    {
        if (reader.TryReadNil())
        {
            return null;
        }

        options.Security.DepthStep(ref reader);
        var name = reader.ReadString();

        reader.Depth--;
        return NameString.FromString(name);
    }
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/MessageHandler.cs => src/Core/NosSmooth.Comms.Core/MessageHandler.cs +112 -0
@@ 0,0 1,112 @@
//
//  MessageHandler.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
using Remora.Results;

namespace NosSmooth.Comms.Core;

/// <summary>
/// An executor of message responders.
/// </summary>
public class MessageHandler
{
    private readonly IServiceProvider _services;
    private readonly bool _respond;

    /// <summary>
    /// Initializes a new instance of the <see cref="MessageHandler"/> class.
    /// </summary>
    /// <param name="services">The services.</param>
    /// <param name="respond">Whether to respond to the messages.</param>
    public MessageHandler(IServiceProvider services, bool respond)
    {
        _services = services;
        _respond = respond;
    }

    /// <summary>
    /// Handle the given message, call responders.
    /// </summary>
    /// <param name="connection">The connection the message comes from.</param>
    /// <param name="wrappedMessage">The message to handle.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public async Task<Result> HandleMessageAsync
        (ConnectionHandler connection, object wrappedMessage, CancellationToken ct)
    {
        var wrappedType = wrappedMessage.GetType();
        if (!wrappedType.IsGenericType)
        {
            return new GenericError($"Message type is not MessageWrapper<>, but {wrappedType.FullName}");
        }

        if (wrappedType.GetGenericTypeDefinition() != typeof(MessageWrapper<>))
        {
            return new GenericError($"Message type is not MessageWrapper<>, but {wrappedType.FullName}");
        }

        var messageType = wrappedType.GetGenericArguments().First();

        var handleMessageMethod = GetType().GetMethod
            (nameof(GenericHandleMessageAsync), BindingFlags.NonPublic | BindingFlags.Instance)!.MakeGenericMethod
            (new[] { messageType });

        var task = (Task<Result>)handleMessageMethod.Invoke(this, new[] { connection, wrappedMessage, ct })!;
        return await task;
    }

    private async Task<Result> GenericHandleMessageAsync<TMessage>
        (ConnectionHandler connection, MessageWrapper<TMessage> wrappedMessage, CancellationToken ct)
    {
        var data = wrappedMessage.Data;

        await using var scope = _services.CreateAsyncScope();
        var injector = scope.ServiceProvider.GetRequiredService<ConnectionInjector>();
        injector.ConnectionHandler = connection;
        injector.Connection = connection.Connection;

        var responders = scope.ServiceProvider
            .GetServices<IMessageResponder<TMessage>>()
            .Select(x => x.Respond(data, ct));

        var results = (await Task.WhenAll(responders))
            .Where(x => !x.IsSuccess)
            .Cast<IResult>()
            .ToList();

        var result = results.Count switch
        {
            0 => Result.FromSuccess(),
            1 => (Result)results[0],
            _ => new AggregateError(results)
        };

        if (_respond && wrappedMessage.Data is not ResponseResult)
        {
            var response = new ResponseResult(wrappedMessage.MessageId, result);
            var sentMessageResult = await connection.SendMessageAsync(response, ct);
            if (!sentMessageResult.IsSuccess)
            {
                results.Add(sentMessageResult);
                result = results.Count switch
                {
                    0 => Result.FromSuccess(),
                    1 => (Result)results[0],
                    _ => new AggregateError(results)
                };
            }
        }

        return result;
    }
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj => src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj +33 -0
@@ 0,0 1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
      <Reference Include="MessagePack">
        <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\messagepack\2.4.59\lib\net6.0\MessagePack.dll</HintPath>
      </Reference>
      <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
        <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\7.0.0\lib\net7.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
      </Reference>
      <Reference Include="Microsoft.Extensions.Logging.Abstractions">
        <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\microsoft.extensions.logging.abstractions\7.0.0\lib\net7.0\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
      </Reference>
      <Reference Include="NosSmooth.PacketSerializer.Abstractions">
        <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.packetserializer.abstractions\1.3.0\lib\net7.0\NosSmooth.PacketSerializer.Abstractions.dll</HintPath>
      </Reference>
    </ItemGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
      <PackageReference Include="NosSmooth.Core" Version="3.3.1" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\NosSmooth.Comms.Abstractions\NosSmooth.Comms.Abstractions.csproj" />
    </ItemGroup>

</Project>

A src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs => src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs +30 -0
@@ 0,0 1,30 @@
//
//  NosSmoothMessageSerializerOptions.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using MessagePack;

namespace NosSmooth.Comms.Core;

/// <summary>
/// Contains options for MessagePack.
/// </summary>
public class NosSmoothMessageSerializerOptions
{
    /// <summary>
    /// Gets or sets the message pack options.
    /// </summary>
    public MessagePackSerializerOptions Options { get; set; } = MessagePackSerializer.Typeless.DefaultOptions;

    /// <summary>
    /// Obtain the options.
    /// </summary>
    /// <param name="options">The options wrapper.</param>
    /// <returns>The options.</returns>
    public static implicit operator MessagePackSerializerOptions(NosSmoothMessageSerializerOptions options)
    {
        return options.Options;
    }
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs => src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs +31 -0
@@ 0,0 1,31 @@
//
//  NosSmoothResolver.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using MessagePack;
using NosSmooth.Comms.Core.Formatters;

namespace NosSmooth.Comms.Core;

/// <summary>
/// A class for obtaining MessagePack formatter resolver.
/// </summary>
public class NosSmoothResolver
{
    /// <summary>
    /// Gets a formatter resolver for NosSmooth messages.
    /// </summary>
    public static IFormatterResolver Instance => MessagePack.Resolvers.CompositeResolver.Create
    (
        new[]
        {
            new NameStringFormatter()
        },
        new[]
        {
            MessagePackSerializer.Typeless.DefaultOptions.Resolver,
        }
    );
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs => src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs +60 -0
@@ 0,0 1,60 @@
//
//  NostaleClientResolver.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Comms.Data;
using NosSmooth.Core.Client;

namespace NosSmooth.Comms.Core;

/// <summary>
/// Resolves <see cref="IConnection"/>s into <see cref="INostaleClient"/>s.
/// </summary>
/// <remarks>
/// Clients will be connected in case the client is not registered yet.
/// If you wish to register the client yourself, use <see cref="RegisterClient"/>.
/// </remarks>
public class NostaleClientResolver
{
    private readonly IServiceProvider _services;
    private readonly ConcurrentDictionary<IConnection, INostaleClient> _clients;

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleClientResolver"/> class.
    /// </summary>
    /// <param name="services">The service provider.</param>
    public NostaleClientResolver(IServiceProvider services)
    {
        _services = services;
        _clients = new ConcurrentDictionary<IConnection, INostaleClient>();
    }

    /// <summary>
    /// Resolve the connection handler into nostale client.
    /// </summary>
    /// <param name="connection">The connection.</param>
    /// <returns>The resolved client.</returns>
    public INostaleClient Resolve(ConnectionHandler connection)
    {
        if (!_clients.ContainsKey(connection.Connection))
        {
            RegisterClient(connection, ActivatorUtilities.CreateInstance<ClientNostaleClient>(_services, connection));
        }

        return _clients[connection.Connection];
    }

    /// <summary>
    /// Register the given client for the given connection.
    /// </summary>
    /// <param name="connection">The connection handler.</param>
    /// <param name="client">The client to register for the given handler.</param>
    public void RegisterClient(ConnectionHandler connection, INostaleClient client)
    {
        _clients[connection.Connection] = client;
    }
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs => src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs +34 -0
@@ 0,0 1,34 @@
//
//  ResponseResultResponder.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
using NosSmooth.Core.Contracts;
using Remora.Results;

namespace NosSmooth.Comms.Core.Responders;

/// <summary>
/// Responds to <see cref="ResponseResult"/> by updating contractor with the response.
/// </summary>
public class ResponseResultResponder : IMessageResponder<ResponseResult>
{
    private readonly Contractor _contractor;

    /// <summary>
    /// Initializes a new instance of the <see cref="ResponseResultResponder"/> class.
    /// </summary>
    /// <param name="contractor">The contractor.</param>
    public ResponseResultResponder(Contractor contractor)
    {
        _contractor = contractor;

    }

    /// <inheritdoc />
    public Task<Result> Respond(ResponseResult message, CancellationToken ct = default)
        => _contractor.Update(message, ct);
}
\ No newline at end of file

A src/Core/NosSmooth.Comms.Core/ServerManager.cs => src/Core/NosSmooth.Comms.Core/ServerManager.cs +159 -0
@@ 0,0 1,159 @@
//
//  ServerManager.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Core.Extensions;
using Remora.Results;

namespace NosSmooth.Comms.Core;

/// <summary>
/// Manages a server, awaits connections, handles messages.
/// </summary>
public class ServerManager
{
    private readonly IServer _server;
    private readonly MessageHandler _messageHandler;
    private readonly IOptions<NosSmoothMessageSerializerOptions> _options;
    private readonly ILogger<ServerManager> _logger;
    private readonly ILogger<ConnectionHandler> _handlerLogger;
    private readonly List<ConnectionHandler> _connectionHandlers;
    private Task<Result>? _task;
    private CancellationTokenSource? _ctSource;

    /// <summary>
    /// Initializes a new instance of the <see cref="ServerManager"/> class.
    /// </summary>
    /// <param name="server">The server to manage.</param>
    /// <param name="messageHandler">The message handler.</param>
    /// <param name="options">The options.</param>
    /// <param name="logger">The logger.</param>
    /// <param name="handlerLogger">The logger for message handler.</param>
    public ServerManager
    (
        IServer server,
        MessageHandler messageHandler,
        IOptions<NosSmoothMessageSerializerOptions> options,
        ILogger<ServerManager> logger,
        ILogger<ConnectionHandler> handlerLogger
    )
    {
        _server = server;
        _connectionHandlers = new List<ConnectionHandler>();
        _messageHandler = messageHandler;
        _options = options;
        _logger = logger;
        _handlerLogger = handlerLogger;
    }

    /// <summary>
    /// Run the manager and await the task.
    /// </summary>
    /// <param name="stopToken">The token used for stopping the handler and disconnecting the connection.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> RunManagerAsync(CancellationToken stopToken)
    {
        StartManager(stopToken);
        return _task!;
    }

    /// <summary>
    /// Broadcast the given message to all clients.
    /// </summary>
    /// <param name="message">The message to broadcast.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <typeparam name="TMessage">The type of the message.</typeparam>
    /// <returns>A result that may or may not have succeeded.</returns>
    public async Task<Result> BroadcastAsync<TMessage>(TMessage message, CancellationToken ct = default)
    {
        var errors = new List<IResult>();
        foreach (var handler in _connectionHandlers)
        {
            var result = await handler.SendMessageAsync<TMessage>(message, ct);
            if (!result.IsSuccess)
            {
                errors.Add(Result.FromError(result));
            }
        }

        return errors.Count switch
        {
            0 => Result.FromSuccess(),
            1 => (Result)errors[0],
            _ => new AggregateError(errors)
        };
    }

    /// <summary>
    /// Run the handler without awaiting the task.
    /// </summary>
    /// <param name="stopToken">The token used for stopping the handler and disconnecting the connection.</param>
    public void StartManager(CancellationToken stopToken = default)
    {
        if (_task is not null)
        {
            return;
        }

        _ctSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken);
        _task = ManagerTask();
    }

    /// <summary>
    /// Request stop the server.
    /// </summary>
    public void RequestStop()
    {
        _ctSource?.Cancel();
    }

    private async Task<Result> ManagerTask()
    {
        if (_ctSource is null)
        {
            throw new InvalidOperationException("The ct source is not initialized.");
        }

        await _server.ListenAsync(_ctSource!.Token);

        while (!_ctSource.IsCancellationRequested)
        {
            var connectionResult = await _server.WaitForConnectionAsync(_ctSource.Token);
            if (!connectionResult.IsDefined(out var connection))
            {
                _logger.LogResultError(connectionResult);
                continue;
            }

            var handler = new ConnectionHandler(null, connection, _messageHandler, _options, _handlerLogger);
            _connectionHandlers.Add(handler);

            handler.StartHandler(_ctSource.Token);
        }

        List<IResult> errors = new List<IResult>();
        foreach (var handler in _connectionHandlers)
        {
            var handlerResult = await handler.RunHandlerAsync(_ctSource.Token);

            if (!handlerResult.IsSuccess)
            {
                errors.Add(handlerResult);
            }
        }

        _server.Close();
        return errors.Count switch
        {
            0 => Result.FromSuccess(),
            1 => (Result)errors[0],
            _ => new AggregateError(errors)
        };
    }
}
\ No newline at end of file

Do not follow this link