/// <inheritdoc/>
        public async Task <GatewayConnectionResult> DisconnectAsync
        (
            bool reconnectionIntended,
            CancellationToken ct = default
        )
        {
            if (_clientWebSocket is null)
            {
                return(GatewayConnectionResult.FromError("The transport service is not connected."));
            }

            switch (_clientWebSocket.State)
            {
            case WebSocketState.Open:
            case WebSocketState.CloseReceived:
            case WebSocketState.CloseSent:
            {
                try
                {
                    // 1012 is used here instead of normal closure, because close codes 1000 and 1001 don't
                    // allow for reconnection. 1012 is referenced in the websocket protocol as "Service restart",
                    // which makes sense for our use case.
                    var closeCode = reconnectionIntended
                            ? (WebSocketCloseStatus)1012
                            : WebSocketCloseStatus.NormalClosure;

                    await _clientWebSocket.CloseAsync
                    (
                        closeCode,
                        "Terminating connection by user request.",
                        ct
                    );
                }
                catch (WebSocketException)
                {
                    // Most likely due to some kind of premature or forced disconnection; we'll live with it
                }

                break;
            }
            }

            _clientWebSocket.Dispose();
            _clientWebSocket = null;

            this.IsConnected = false;
            return(GatewayConnectionResult.FromSuccess());
        }
        /// <inheritdoc />
        public async Task <GatewayConnectionResult> ConnectAsync(Uri endpoint, CancellationToken ct = default)
        {
            if (_clientWebSocket is not null)
            {
                return(GatewayConnectionResult.FromError("The transport service is already connected."));
            }

            var socket = _services.GetRequiredService <ClientWebSocket>();

            try
            {
                await socket.ConnectAsync(endpoint, ct);

                switch (socket.State)
                {
                case WebSocketState.Open:
                case WebSocketState.Connecting:
                {
                    break;
                }

                default:
                {
                    socket.Dispose();
                    return(GatewayConnectionResult.FromError("Failed to connect to the endpoint."));
                }
                }
            }
            catch (Exception e)
            {
                socket.Dispose();
                return(GatewayConnectionResult.FromError(e));
            }

            _clientWebSocket = socket;

            this.IsConnected = true;
            return(GatewayConnectionResult.FromSuccess());
        }
Example #3
0
        /// <summary>
        /// Starts and connects the gateway client.
        /// </summary>
        /// <remarks>
        /// This task will not complete until cancelled (or faulted), maintaining the connection for the duration of it.
        ///
        /// If the gateway client encounters a fatal problem during the execution of this task, it will return with a
        /// failed result. If a shutdown is requested, it will gracefully terminate the connection and return a
        /// successful result.
        /// </remarks>
        /// <param name="stopRequested">A token by which the caller can request this method to stop.</param>
        /// <returns>A gateway connection result which may or may not have succeeded.</returns>
        public async Task <GatewayConnectionResult> RunAsync(CancellationToken stopRequested)
        {
            try
            {
                if (_connectionStatus != GatewayConnectionStatus.Offline)
                {
                    return(GatewayConnectionResult.FromError("Already connected."));
                }

                // Until cancellation has been requested or we hit a fatal error, reconnections should be attempted.
                _disconnectRequestedSource.Dispose();
                _disconnectRequestedSource = new CancellationTokenSource();

                while (!stopRequested.IsCancellationRequested)
                {
                    var iterationResult = await RunConnectionIterationAsync(stopRequested);

                    if (iterationResult.IsSuccess)
                    {
                        continue;
                    }

                    // Something has gone wrong. Close the socket, and handle it
                    // Terminate the send and receive tasks
                    _disconnectRequestedSource.Cancel();

                    // The results of the send and receive tasks are discarded here, because the iteration result will
                    // contain whichever of them failed if any of them did
                    _ = await _sendTask;
                    _ = await _receiveTask;

                    if (_transportService.IsConnected)
                    {
                        var disconnectResult = await _transportService.DisconnectAsync(stopRequested.IsCancellationRequested, stopRequested);

                        if (!disconnectResult.IsSuccess)
                        {
                            // Couldn't disconnect cleanly :(
                            return(disconnectResult);
                        }
                    }

                    // Finish up the responders
                    foreach (var runningResponder in _runningResponderDispatches)
                    {
                        await FinalizeResponderDispatchAsync(runningResponder);
                    }

                    if (stopRequested.IsCancellationRequested)
                    {
                        // The user requested a termination, and we don't intend to reconnect.
                        return(iterationResult);
                    }

                    if (ShouldReconnect(iterationResult, out var shouldTerminate, out var withNewSession))
                    {
                        if (withNewSession)
                        {
                            _sessionID        = null;
                            _connectionStatus = GatewayConnectionStatus.Disconnected;
                        }
                        else
                        {
                            _connectionStatus = GatewayConnectionStatus.Disconnected;
                        }
                    }
                    else if (shouldTerminate)
                    {
                        return(iterationResult);
                    }

                    // This token's been cancelled, we'll need a new one to reconnect.
                    _disconnectRequestedSource.Dispose();
                    _disconnectRequestedSource = new();
                }