<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/Interfaces/ITransportFactory.cs<|end_filename|>
﻿using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace syp.biz.SockJS.NET.Common.Interfaces
{
    [SuppressMessage("ReSharper", "UnusedMember.Global")]
    public interface ITransportFactory
    {
        string Name { get; }
        bool Enabled { get; set; }
        uint Priority { get; set; }

        Task<ITransport> Build(ITransportConfiguration config);
    }
}

<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Test/Program.cs<|end_filename|>
﻿using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using syp.biz.SockJS.NET.Common.Interfaces;

namespace syp.biz.SockJS.NET.Test
{
    class Program
    {
        static async Task Main(/* string[] args */)
        {
            try
            {
                var testModules = typeof(Program).Assembly.GetTypes()
                    .Where(t => !t.IsAbstract && typeof(ITestModule).IsAssignableFrom(t))
                    .OrderBy(t => t.Name)
                    .ToImmutableArray();

                foreach (var type in testModules)
                {
                    Console.WriteLine($"Testing '{type.Name}'...");
                    var module = (ITestModule) Activator.CreateInstance(type)!;
                    await module.Execute();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            finally
            {
                Console.ReadLine();
            }
        }
    }

    internal interface ITestModule
    {
        Task Execute();
    }

    internal class ConsoleLogger : ILogger
    {
        [DebuggerStepThrough, DebuggerNonUserCode]
        public void Debug(string message) => Console.WriteLine($"{DateTime.Now:s} [DBG] {message}");

        [DebuggerStepThrough, DebuggerNonUserCode]
        public void Info(string message) => Console.WriteLine($"{DateTime.Now:s} [INF] {message}");

        [DebuggerStepThrough, DebuggerNonUserCode]
        public void Error(string message) => Console.WriteLine($"{DateTime.Now:s} [ERR] {message}");
    }
}


<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/DTO/InfoDto.cs<|end_filename|>
﻿#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
using Newtonsoft.Json;

namespace syp.biz.SockJS.NET.Common.DTO
{
    public class InfoDto
    {
        [JsonProperty(PropertyName = "entropy")]
        public long Entropy { get; set; }

        [JsonProperty(PropertyName = "origins")]
        public string[] Origins { get; set; }

        [JsonProperty(PropertyName = "cookie_needed")]
        public bool CookieNeeded { get; set; }

        [JsonProperty(PropertyName = "websocket")]
        public bool WebSocket { get; set; }

        [JsonIgnore]
        public long RoundTripTime { get; set; }
        /*
{
    "entropy":296547767,
    "origins":["*:*"],
    "cookie_needed":true,
    "websocket":true
}
         */
    }
}

<|start_filename|>server/index.js<|end_filename|>
const http = require("http");
const sockjs = require("sockjs");
const node_static = require('node-static');
let shutdown = false;

const echo = sockjs.createServer({ sockjs_url: 'http://cdn.jsdelivr.net/sockjs/1.0.1/sockjs.min.js' });
echo.on('connection', conn => {
    console.log("connection", conn.protocol, conn.url, conn.id);
    if (conn.headers) console.log(Object.entries(conn.headers).map(([h,v]) => `\t${h}:${v}`).join("\n"));
    conn.on('data', message => {
        console.log("data", ">>>", message);
        if (message === "shutdown") {
            message = "ok";
            console.log("data", "<<<", message);
            conn.write(message);
            shutdown = true;
        } else {
            console.log("data", "<<<", message);
            conn.write(message);
        }
    });
    conn.on('close', () => {
        console.log("close");
        if (shutdown) {
            console.log("exiting");
            process.exit(0);
        }
    });
});

const static_directory = new node_static.Server(__dirname);
const server = http.createServer();
server.addListener('request', (req, res) => {
    console.log("request", req.method, req.url);
    static_directory.serve(req, res);
});
server.addListener('upgrade', (req, res) => {
    console.log("upgrade");
    res.end();
});
echo.installHandlers(server, { prefix: '/echo' });
server.listen(9999, '0.0.0.0');

<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/Interfaces/ITransport.cs<|end_filename|>
﻿using System;
using System.Threading;
using System.Threading.Tasks;

namespace syp.biz.SockJS.NET.Common.Interfaces
{
    public interface ITransport : IDisposable
    {
        event EventHandler<string> Message;
        event EventHandler Disconnected;
        
        Task Connect(CancellationToken token);
        Task Disconnect();

        Task Send(string data, CancellationToken token);
    }
}

<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/Interfaces/ILogger.cs<|end_filename|>
﻿namespace syp.biz.SockJS.NET.Common.Interfaces
{
    public interface ILogger
    {
        void Debug(string message);
        void Info(string message);
        void Error(string message);
    }
}

<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/Extensions/StringExtensions.cs<|end_filename|>
﻿using System.Diagnostics.CodeAnalysis;

namespace syp.biz.SockJS.NET.Common.Extensions
{
    [SuppressMessage("ReSharper", "UnusedType.Global")]
    [SuppressMessage("ReSharper", "UnusedMember.Global")]
    public static class StringExtensions
    {
        /// <summary>
        /// Indicates whether a specified string is null, empty, or consists only of white-space characters.
        /// </summary>
        /// <param name="value">The string to test.</param>
        /// <returns>
        /// <c>true</c> if <paramref name="value"/> is <c>null</c> or <see cref="string.Empty"/>,
        /// or if <paramref name="value"/> consists exclusively of white-space characters.
        /// </returns>
        /// <seealso cref="string.IsNullOrWhiteSpace"/>
        public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string value) => string.IsNullOrWhiteSpace(value);

        /// <summary>
        /// Indicates whether the specified string is null or an <see cref="F:System.String.Empty"></see> string.
        /// </summary>
        /// <param name="value">The string to test.</param>
        /// <returns>
        /// <c>true</c> if the <paramref name="value">value</paramref> parameter is null or an empty string (""); otherwise, <c>false</c>.
        /// </returns>
        /// <seealso cref="string.IsNullOrEmpty"/>
        public static bool IsNullOrEmpty([NotNullWhen(false)] this string value) => string.IsNullOrEmpty(value);
    }
}


<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/Extensions/NameValueCollectionExtensions.cs<|end_filename|>
﻿using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;

namespace syp.biz.SockJS.NET.Common.Extensions
{
    public static class NameValueCollectionExtensions
    {
        public static IEnumerable<(string name, string value)> AsEnumerable(this NameValueCollection collection) => collection
                .Cast<string>()
                .Select(name => (name, collection[name]));
    }
}


<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Client/Implementations/Transports/SystemWebSocketTransport.cs<|end_filename|>
﻿using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using syp.biz.SockJS.NET.Common.Extensions;
using syp.biz.SockJS.NET.Common.Interfaces;

namespace syp.biz.SockJS.NET.Client.Implementations.Transports
{
    [SuppressMessage("ReSharper", "UnusedType.Global")]
    internal class SystemWebSocketTransportFactory : ITransportFactory
    {
        #region Implementation of ITransportFactory
        public string Name => "websocket-system";
        public bool Enabled { get; set; } = CheckIfWebSocketIsSupported();
        public uint Priority { get; set; } = 100;
        public Task<ITransport> Build(ITransportConfiguration config)
        {
            config.Logger.Debug($"{nameof(this.Build)}: '{this.Name}' transport");
            var transport = new SystemWebSocketTransport(config);
            return Task.FromResult<ITransport>(transport);
        }
        #endregion Implementation of ITransportFactory

        private static bool CheckIfWebSocketIsSupported()
        {
            try
            {
                using var socket = new ClientWebSocket();
                return true;
            }
            catch (PlatformNotSupportedException)
            {
                return false;
            }
        }
    }

    internal class SystemWebSocketTransport : ITransport
    {
        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
        private readonly ITransportConfiguration _config;
        private readonly ILogger _log;
        private readonly ClientWebSocket _socket;

        public SystemWebSocketTransport(ITransportConfiguration config)
        {
            this._config = config;
            this._log = config.Logger;
            this._socket = new ClientWebSocket();
            this.Configure(this._socket.Options, config);
        }

        #region Implementation of IDisposable
        public void Dispose()
        {
            this._cts.Dispose();
            this._socket.Dispose();
        }
        #endregion Implementation of IDisposable

        #region Implementation of ITransport
        public event EventHandler<string>? Message;
        public event EventHandler? Disconnected;

        public async Task Connect(CancellationToken token)
        {
            var endpoint = this.BuildEndpoint();
            this._log.Info($"{nameof(this.Connect)}: {endpoint}");

            await this._socket.ConnectAsync(endpoint, token);
            _ = Task.Factory.StartNew(this.ReceiveLoop, this._cts, TaskCreationOptions.LongRunning).ConfigureAwait(false);
        }

        public Task Connect() => this.Connect(CancellationToken.None);

        public async Task Disconnect()
        {
            this._log.Info(nameof(this.Disconnect));
            if (this._socket.State == WebSocketState.Closed) return;
            await this._socket.CloseAsync(WebSocketCloseStatus.Empty, "", this._cts.Token);
            this.Disconnected?.Invoke(this, EventArgs.Empty);
        }

        public Task Send(string data) => this.Send(data, CancellationToken.None);

        public async Task Send(string data, CancellationToken token)
        {
            this._log.Debug($"{nameof(this.Send)}: {data}");
            if (data is null) return;

            this.VerifyOpen();
            var buffer = Encoding.UTF8.GetBytes(data).AsMemory();
            await this._socket.SendAsync(buffer, WebSocketMessageType.Text, true, token);
        }
        #endregion Implementation of ITransport

        private Uri BuildEndpoint()
        {
            var endpoint = new UriBuilder(this._config.BaseEndpoint);
            endpoint.Scheme = endpoint.Scheme == "https" ? "wss" : "ws";
            endpoint.AddPath("/websocket");
            return endpoint.Uri;
        }

        private async void ReceiveLoop(object obj)
        {
            try
            {
                this._log.Debug(nameof(this.ReceiveLoop));
                
                var builder = new StringBuilder();
                var buffer = new ArraySegment<byte>(new byte[1024]);

                while (!this._cts.IsCancellationRequested && this._socket.State == WebSocketState.Open)
                {
                    WebSocketReceiveResult result;
                    do
                    {
                        result = await this._socket.ReceiveAsync(buffer, this._cts.Token);
                        var data = Encoding.UTF8.GetString(buffer.AsSpan(buffer.Offset, result.Count));
                        builder.Append(data);
                    } while (!result.EndOfMessage);

                    var message = builder.ToString();
                    builder.Clear();

                    if (!message.IsNullOrWhiteSpace()) Task.Run(() => this.Message?.Invoke(this, message)).IgnoreAwait();
                    if (result.MessageType != WebSocketMessageType.Close) continue;

                    this._log.Error("Server sent close message");
                    await this.Disconnect();
                    return;
                }
            }
            catch (Exception ex)
            {
                this._log.Error($"{nameof(this.ReceiveLoop)}: {ex}");
                await this.Disconnect();
            }
        }

        private void VerifyOpen()
        {
            var state = this._socket.State;
            if (state != WebSocketState.Open) throw new Exception($"Invalid socket state '{state}");
        }

        private void Configure(ClientWebSocketOptions options, ITransportConfiguration config)
        {
            config.DefaultRequestHeaders.AsEnumerable().ForEach(i => options.SetRequestHeader(i.name, i.value));
            if (!(config.Proxy is null)) options.Proxy = config.Proxy;
            if (!(config.Cookies is null)) options.Cookies = config.Cookies;
            if (!(config.RemoteCertificateValidator is null)) options.RemoteCertificateValidationCallback = config.RemoteCertificateValidator;
            if (!(config.ClientCertificates is null)) options.ClientCertificates = config.ClientCertificates;
            if (config.KeepAliveInterval.HasValue) options.KeepAliveInterval = config.KeepAliveInterval.Value;
            if (config.Credentials is null) return;
            options.Credentials = config.Credentials;
            options.UseDefaultCredentials = false;
        }
    }
}


<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Client/Configuration.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using syp.biz.SockJS.NET.Common.Interfaces;

namespace syp.biz.SockJS.NET.Client
{
    [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
    public class Configuration
    {
        public ICollection<ITransportFactory>? TransportFactories { get; set; }
        public Uri? BaseEndpoint { get; set; }
        public WebHeaderCollection? DefaultHeaders { get; set; }
        public ILogger? Logger { get; set; }
        public TimeSpan? InfoReceiverTimeout { get; set; }
        public ICredentials? Credentials { get; set; }
        public IWebProxy? Proxy { get; set; }
        public X509CertificateCollection? ClientCertificates { get; set; }
        public RemoteCertificateValidationCallback? RemoteCertificateValidator { get; set; }
        public CookieContainer? Cookies { get; set; }
        public TimeSpan? KeepAliveInterval { get; set; }

        internal Factory.ReadOnlySockJsConfiguration AsReadonly() => new Factory.ReadOnlySockJsConfiguration(this);

        public static class Factory
        {
            public static Configuration BuildDefault(string baseEndpoint)
            {
                return BuildDefault(new Uri(baseEndpoint ?? throw new ArgumentNullException(nameof(baseEndpoint))));
            }

            public static Configuration BuildDefault(Uri baseEndpoint)
            {
                if (baseEndpoint is null) throw new ArgumentNullException(nameof(baseEndpoint));

                return new Configuration
                {
                    BaseEndpoint = baseEndpoint,
                    TransportFactories = ReflectTransportFactories(),
                    DefaultHeaders = new WebHeaderCollection(),
                    Logger = new Implementations.NullLogger(),
                    InfoReceiverTimeout = TimeSpan.FromSeconds(8),
                    Credentials = null,
                    Proxy = null,
                    ClientCertificates = null,
                    RemoteCertificateValidator = null,
                    Cookies = null,
                    KeepAliveInterval = null,
                };
            }

            private static ICollection<ITransportFactory> ReflectTransportFactories()
            {
                var factories = typeof(SockJS)
                    .Assembly
                    .DefinedTypes
                    .Where(t => !t.IsAbstract)
                    .Where(t => typeof(ITransportFactory).IsAssignableFrom(t))
                    .Select(Activator.CreateInstance)
                    .OfType<ITransportFactory>()
                    .OrderByDescending(f => f.Priority)
                    .ToArray();
                return factories;
            }

            internal class ReadOnlySockJsConfiguration
            {
                public ReadOnlySockJsConfiguration(Configuration config)
                {
                    this.TransportFactories = config.TransportFactories ?? ReflectTransportFactories();
                    this.BaseEndpoint = config.BaseEndpoint ?? throw new ArgumentNullException(nameof(Configuration.BaseEndpoint));
                    this.DefaultHeaders = config.DefaultHeaders ?? new WebHeaderCollection();
                    this.Logger = config.Logger ?? new Implementations.NullLogger();
                    this.InfoReceiverTimeout = config.InfoReceiverTimeout ?? TimeSpan.FromSeconds(8);
                    this.Credentials = config.Credentials;
                    this.Proxy = config.Proxy;
                    this.ClientCertificates = config.ClientCertificates;
                    this.RemoteCertificateValidator = config.RemoteCertificateValidator;
                    this.Cookies = config.Cookies;
                    this.KeepAliveInterval = config.KeepAliveInterval;
                }

                public ICollection<ITransportFactory> TransportFactories { get; }
                public Uri BaseEndpoint { get; }
                public WebHeaderCollection DefaultHeaders { get; }
                public ILogger Logger { get; }
                public TimeSpan InfoReceiverTimeout { get; }
                public ICredentials? Credentials { get; }
                public IWebProxy? Proxy { get; }
                public X509CertificateCollection? ClientCertificates { get; }
                public RemoteCertificateValidationCallback? RemoteCertificateValidator { get; }
                public CookieContainer? Cookies { get; }
                public TimeSpan? KeepAliveInterval { get; }
            }
        }
    }
}

<|start_filename|>server/package-lock.json<|end_filename|>
{
  "name": "stomp-server",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "colors": {
      "version": "1.3.2",
      "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.2.tgz",
      "integrity": "<KEY>
    },
    "faye-websocket": {
      "version": "0.10.0",
      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
      "integrity": "sha1-TkkvjQTftviQA1B/<KEY>Q=",
      "requires": {
        "websocket-driver": ">=0.5.1"
      }
    },
    "http-parser-js": {
      "version": "0.5.0",
      "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.0.tgz",
      "integrity": "<KEY>
    },
    "mime": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
      "integrity": "<KEY>
    },
    "minimist": {
      "version": "0.0.10",
      "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
      "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
    },
    "node-static": {
      "version": "0.7.11",
      "resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz",
      "integrity": "<KEY>
      "requires": {
        "colors": ">=0.6.0",
        "mime": "^1.2.9",
        "optimist": ">=0.3.4"
      }
    },
    "optimist": {
      "version": "0.6.1",
      "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
      "requires": {
        "minimist": "~0.0.1",
        "wordwrap": "~0.0.2"
      }
    },
    "sockjs": {
      "version": "0.3.19",
      "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
      "integrity": "<KEY>
      "requires": {
        "faye-websocket": "^0.10.0",
        "uuid": "^3.0.1"
      }
    },
    "uuid": {
      "version": "3.3.2",
      "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
      "integrity": "<KEY>
    },
    "websocket-driver": {
      "version": "0.7.0",
      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
      "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=",
      "requires": {
        "http-parser-js": ">=0.4.0",
        "websocket-extensions": ">=0.1.1"
      }
    },
    "websocket-extensions": {
      "version": "0.1.3",
      "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
      "integrity": "<KEY>
    },
    "wordwrap": {
      "version": "0.0.3",
      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
      "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
    }
  }
}


<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/Utils/Random.cs<|end_filename|>
﻿using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;

namespace syp.biz.SockJS.NET.Common.Utils
{
    [SuppressMessage("ReSharper", "UnusedType.Global")]
    [SuppressMessage("ReSharper", "UnusedMember.Global")]
    public static class Random
    {
        private const string RandomStringChars = "abcdefghijklmnopqrstuvwxyz012345";

        public static string GetString(int length)
        {
            if (length < 1) throw new ArgumentOutOfRangeException(nameof(length), "Length must be at least 1");

            var buffer = new byte[length];
            var charsLength = RandomStringChars.Length;

            System.Security.Cryptography.RandomNumberGenerator.Create().GetBytes(buffer);

            var chars = buffer
                .Select(b => b % charsLength) // relative position (index) of random byte in RandomStringChars
                .Select(idx => RandomStringChars[idx]); // select char for index

            return new StringBuilder(length).Append(chars).ToString();
        }

        [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
        public static long GetNumber(long max) => (long) Math.Floor(new System.Random(DateTime.UtcNow.Millisecond).NextDouble() * max);

        public static string GetNumberString(long max)
        {
            var t = (max - 1).ToString().Length;
            var num = GetNumber(max);
            return num.ToString().PadLeft(t, '0');
        }
    }
}


<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/Interfaces/ITransportConfiguration.cs<|end_filename|>
﻿using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using syp.biz.SockJS.NET.Common.DTO;

namespace syp.biz.SockJS.NET.Common.Interfaces
{
    public interface ITransportConfiguration
    {
        Uri BaseEndpoint { get; }
        ILogger Logger { get; }
        InfoDto Info { get; }
        WebHeaderCollection DefaultRequestHeaders { get; }
        ICredentials? Credentials { get; }
        IWebProxy? Proxy { get; }
        X509CertificateCollection? ClientCertificates { get; }
        RemoteCertificateValidationCallback? RemoteCertificateValidator { get; }
        CookieContainer? Cookies { get; }
        TimeSpan? KeepAliveInterval { get; }
    }
}

<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Common/Exceptions/InvalidAccessException.cs<|end_filename|>
﻿using System;
using System.Runtime.Serialization;

namespace syp.biz.SockJS.NET.Common.Exceptions
{
    [Serializable]
    public class InvalidAccessException : Exception
    {
        public InvalidAccessException() { }

        public InvalidAccessException(string message) : base(message) { }

        public InvalidAccessException(string message, Exception inner) : base(message, inner) { }

        protected InvalidAccessException(SerializationInfo info, StreamingContext context) : base(info, context) { }
    }

    [Serializable]
    public class InvalidStateException : Exception
    {
        public InvalidStateException() { }

        public InvalidStateException(string message) : base(message) { }

        public InvalidStateException(string message, Exception inner) : base(message, inner) { }

        protected InvalidStateException(SerializationInfo info, StreamingContext context) : base(info, context) { }
    }
}


<|start_filename|>syp.biz/SockJS.NET/syp.biz.SockJS.NET.Client/SockJS.cs<|end_filename|>
﻿using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using syp.biz.SockJS.NET.Client.Implementations;
using syp.biz.SockJS.NET.Common.DTO;
using syp.biz.SockJS.NET.Common.Interfaces;

namespace syp.biz.SockJS.NET.Client
{
    [SuppressMessage("ReSharper", "InconsistentNaming")]
    public class SockJS : IClient
    {
        private readonly SemaphoreSlim _sync = new SemaphoreSlim(1, 1);
        private readonly Configuration.Factory.ReadOnlySockJsConfiguration _config;
        private readonly ILogger _log;
        private ITransport? _transport;
        private ConnectionState _state = ConnectionState.Initial;

        [SuppressMessage("ReSharper", "UnusedMember.Global")]
        public SockJS(string baseEndpoint) : this(Configuration.Factory.BuildDefault(baseEndpoint)) {}
        
        [SuppressMessage("ReSharper", "UnusedMember.Global")]
        public SockJS(Uri baseEndpoint) : this(Configuration.Factory.BuildDefault(baseEndpoint)) {}

        public SockJS(Configuration config)
        {
            this._config = config?.AsReadonly() ?? throw new ArgumentNullException(nameof(config));
            this._log = this._config.Logger;
        }

        #region Implementation of IDisposable
        public void Dispose()
        {
            throw new NotImplementedException();
        }
        #endregion Implementation of IDisposable

        #region Implementation of IClient
        public event EventHandler? Connected;
        public event EventHandler? Disconnected;
        public event EventHandler<string>? Message;

        public ConnectionState State
        {
            get => this._state;
            private set
            {
                var current = this._state;
                if (current == value) return;
                this._log.Debug($"{nameof(this.State)}: {current} -> {value}");
                this._state = value;
            }
        }

        public async Task Connect(CancellationToken token)
        {
            this._log.Info(nameof(Connect));
            try
            {
                await this._sync.WaitAsync(token);
                if (this.State != ConnectionState.Initial) throw new Exception($"Cannot connect while state is '{this.State}'");
                this.State = ConnectionState.Connecting;

                var info = await new InfoReceiver(this._config).GetInfo();

                ITransport? selectedTransport = null;
                var factories = this._config.TransportFactories.Where(t => t.Enabled).ToArray();
                this._log.Debug($"{nameof(Connect)}: Transports: {factories.Length}/{this._config.TransportFactories.Count} (enabled/total)");

                foreach (var factory in factories)
                {
                    selectedTransport = await this.TryTransport(factory, info, token);
                    if (selectedTransport is null) continue;
                    break;
                }

                this._transport = selectedTransport ?? throw new Exception("No available transports");
                this._transport.Message += this.TransportOnMessage;
                this._transport.Disconnected += this.TransportOnDisconnected;
                this.State = ConnectionState.Established;
                this.Connected?.Invoke(this, EventArgs.Empty);
            }
            catch (Exception e)
            {
                this._log.Error($"{nameof(Connect)}: {e.Message}");
                this._log.Debug($"{nameof(Connect)}: {e}");
                this.State = ConnectionState.Error;
                throw;
            }
            finally
            {
                this._sync.Release();
            }
        }

        public Task Connect() => this.Connect(CancellationToken.None);

        public async Task Disconnect()
        {
            this._log.Info(nameof(this.Disconnect));
            this.VerifyEstablished();
            await this._transport!.Disconnect();
        }

        public Task Send(string data) => this.Send(data, CancellationToken.None);

        public async Task Send(string data, CancellationToken token)
        {
            this.VerifyEstablished();
            await this._transport!.Send(data, token);
        }

        #endregion Implementation of IClient

        private async Task<ITransport?> TryTransport(
            ITransportFactory factory,
            InfoDto info,
            CancellationToken token)
        {
            try
            {
                this._log.Debug($"{nameof(this.TryTransport)}: {factory.Name}");
                var transport = await factory.Build(new TransportConfiguration(this._config, info));
                await transport.Connect(token);
                this._log.Info($"{nameof(this.TryTransport)}: {factory.Name} - Success");
                return transport;
            }
            catch (Exception e)
            {
                this._log.Error($"{nameof(this.TryTransport)}: {factory.Name} - Failed: {e.Message}");
                this._log.Error($"{nameof(this.TryTransport)}: {e}");
                return null;
            }
        }

        private void TransportOnMessage(object sender, string message)
        {
            this._log.Debug($"{nameof(this.TransportOnMessage)}: {message}");
            this.Message?.Invoke(this, message);
        }

        private void TransportOnDisconnected(object sender, EventArgs e)
        {
            this._log.Debug($"{nameof(this.TransportOnDisconnected)}");
            this.Disconnected?.Invoke(this, EventArgs.Empty);
        }

        private void VerifyEstablished()
        {
            if (this._transport is null || this.State != ConnectionState.Established)
                throw new InvalidOperationException("Connection not established");
        }
    }
}