private async Task SendWebSocketRequestAsync(GraphQLWebSocketRequest request)
        {
            try
            {
                if (_internalCancellationToken.IsCancellationRequested)
                {
                    request.SendCanceled();
                    return;
                }

                await InitializeWebSocket();

                var requestBytes = _client.JsonSerializer.SerializeToBytes(request);
                await _clientWebSocket.SendAsync(
                    new ArraySegment <byte>(requestBytes),
                    WebSocketMessageType.Text,
                    true,
                    _internalCancellationToken);

                request.SendCompleted();
            }
            catch (Exception e)
            {
                request.SendFailed(e);
            }
        }
 private async Task SendWebSocketMessageAsync(GraphQLWebSocketRequest request, CancellationToken cancellationToken = default)
 {
     var requestBytes = _client.JsonSerializer.SerializeToBytes(request);
     await _clientWebSocket.SendAsync(
         new ArraySegment <byte>(requestBytes),
         WebSocketMessageType.Text,
         true,
         cancellationToken);
 }
        /// <summary>
        /// Send a regular GraphQL request (query, mutation) via websocket
        /// </summary>
        /// <typeparam name="TResponse">the response type</typeparam>
        /// <param name="request">the <see cref="GraphQLRequest"/> to send</param>
        /// <param name="cancellationToken">the token to cancel the request</param>
        /// <returns></returns>
        public Task <GraphQLResponse <TResponse> > SendRequest <TResponse>(GraphQLRequest request, CancellationToken cancellationToken = default) =>
        Observable.Create <GraphQLResponse <TResponse> >(async observer =>
        {
            await _client.Options.PreprocessRequest(request, _client);
            var websocketRequest = new GraphQLWebSocketRequest
            {
                Id      = Guid.NewGuid().ToString("N"),
                Type    = GraphQLWebSocketMessageType.GQL_START,
                Payload = request
            };
            var observable = IncomingMessageStream
                             .Where(response => response != null && response.Id == websocketRequest.Id)
                             .TakeUntil(response => response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE)
                             .Select(response =>
            {
                Debug.WriteLine($"received response for request {websocketRequest.Id}");
                var typedResponse =
                    _client.JsonSerializer.DeserializeToWebsocketResponse <TResponse>(
                        response.MessageBytes);
                return(typedResponse.Payload);
            });

            try
            {
                // initialize websocket (completes immediately if socket is already open)
                await InitializeWebSocket();
            }
            catch (Exception e)
            {
                // subscribe observer to failed observable
                return(Observable.Throw <GraphQLResponse <TResponse> >(e).Subscribe(observer));
            }

            var disposable = new CompositeDisposable(
                observable.Subscribe(observer)
                );

            Debug.WriteLine($"submitting request {websocketRequest.Id}");
            // send request
            try
            {
                await QueueWebSocketRequest(websocketRequest);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }

            return(disposable);
        })
        // complete sequence on OperationCanceledException, this is triggered by the cancellation token
        .Catch <GraphQLResponse <TResponse>, OperationCanceledException>(exception =>
                                                                         Observable.Empty <GraphQLResponse <TResponse> >())
        .FirstAsync()
        .ToTask(cancellationToken);
        private async Task<Unit> SendWebSocketRequestAsync(GraphQLWebSocketRequest request)
        {
            try
            {
                if (_internalCancellationToken.IsCancellationRequested)
                {
                    request.SendCanceled();
                    return Unit.Default;
                }

                await InitializeWebSocket();
                await SendWebSocketMessageAsync(request, _internalCancellationToken);
                request.SendCompleted();
            }
            catch (Exception e)
            {
                request.SendFailed(e);
            }
            return Unit.Default;
        }
        private async Task _sendWebSocketRequest(GraphQLWebSocketRequest request)
        {
            try {
                if (cancellationTokenSource.Token.IsCancellationRequested)
                {
                    request.SendCanceled();
                    return;
                }

                await InitializeWebSocket().ConfigureAwait(false);

                var requestBytes = Options.JsonSerializer.SerializeToBytes(request);
                await this.clientWebSocket.SendAsync(
                    new ArraySegment <byte>(requestBytes),
                    WebSocketMessageType.Text,
                    true,
                    cancellationTokenSource.Token).ConfigureAwait(false);

                request.SendCompleted();
            }
            catch (Exception e) {
                request.SendFailed(e);
            }
        }
Example #6
0
        public byte[] SerializeToBytes(GraphQLWebSocketRequest request)
        {
            var json = JsonConvert.SerializeObject(request, JsonSerializerSettings);

            return(Encoding.UTF8.GetBytes(json));
        }
        /// <summary>
        /// Create a new subscription stream
        /// </summary>
        /// <typeparam name="TResponse">the response type</typeparam>
        /// <param name="request">the <see cref="GraphQLRequest"/> to start the subscription</param>
        /// <param name="exceptionHandler">Optional: exception handler for handling exceptions within the receive pipeline</param>
        /// <returns>a <see cref="IObservable{TResponse}"/> which represents the subscription</returns>
        public IObservable <GraphQLResponse <TResponse> > CreateSubscriptionStream <TResponse>(GraphQLRequest request, Action <Exception> exceptionHandler = null) =>
        Observable.Defer(() =>
                         Observable.Create <GraphQLResponse <TResponse> >(async observer =>
        {
            Debug.WriteLine($"Create observable thread id: {Thread.CurrentThread.ManagedThreadId}");
            await _client.Options.PreprocessRequest(request, _client);
            var startRequest = new GraphQLWebSocketRequest
            {
                Id      = Guid.NewGuid().ToString("N"),
                Type    = GraphQLWebSocketMessageType.GQL_START,
                Payload = request
            };
            var closeRequest = new GraphQLWebSocketRequest
            {
                Id   = startRequest.Id,
                Type = GraphQLWebSocketMessageType.GQL_STOP
            };
            var initRequest = new GraphQLWebSocketRequest
            {
                Id   = startRequest.Id,
                Type = GraphQLWebSocketMessageType.GQL_CONNECTION_INIT,
            };

            var observable = Observable.Create <GraphQLResponse <TResponse> >(o =>
                                                                              IncomingMessageStream
                                                                              // ignore null values and messages for other requests
                                                                              .Where(response => response != null && response.Id == startRequest.Id)
                                                                              .Subscribe(response =>
            {
                // terminate the sequence when a 'complete' message is received
                if (response.Type == GraphQLWebSocketMessageType.GQL_COMPLETE)
                {
                    Debug.WriteLine($"received 'complete' message on subscription {startRequest.Id}");
                    o.OnCompleted();
                    return;
                }

                // post the GraphQLResponse to the stream (even if a GraphQL error occurred)
                Debug.WriteLine($"received payload on subscription {startRequest.Id} (thread {Thread.CurrentThread.ManagedThreadId})");
                var typedResponse =
                    _client.JsonSerializer.DeserializeToWebsocketResponse <TResponse>(
                        response.MessageBytes);
                o.OnNext(typedResponse.Payload);

                // in case of a GraphQL error, terminate the sequence after the response has been posted
                if (response.Type == GraphQLWebSocketMessageType.GQL_ERROR)
                {
                    Debug.WriteLine($"terminating subscription {startRequest.Id} because of a GraphQL error");
                    o.OnCompleted();
                }
            },
                                                                                         e =>
            {
                Debug.WriteLine($"response stream for subscription {startRequest.Id} failed: {e}");
                o.OnError(e);
            },
                                                                                         () =>
            {
                Debug.WriteLine($"response stream for subscription {startRequest.Id} completed");
                o.OnCompleted();
            })
                                                                              );

            try
            {
                // initialize websocket (completes immediately if socket is already open)
                await InitializeWebSocket();
            }
            catch (Exception e)
            {
                // subscribe observer to failed observable
                return(Observable.Throw <GraphQLResponse <TResponse> >(e).Subscribe(observer));
            }

            var disposable = new CompositeDisposable(
                observable.Subscribe(observer),
                Disposable.Create(async() =>
            {
                // only try to send close request on open websocket
                if (WebSocketState != WebSocketState.Open)
                {
                    return;
                }

                try
                {
                    Debug.WriteLine($"sending close message on subscription {startRequest.Id}");
                    await QueueWebSocketRequest(closeRequest);
                }
                // do not break on disposing
                catch (OperationCanceledException) { }
            })
                );

            // send connection init
            Debug.WriteLine($"sending connection init on subscription {startRequest.Id}");
            try
            {
                await QueueWebSocketRequest(initRequest);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }

            Debug.WriteLine($"sending initial message on subscription {startRequest.Id}");
            // send subscription request
            try
            {
                await QueueWebSocketRequest(startRequest);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }

            return(disposable);
        }))
        // complete sequence on OperationCanceledException, this is triggered by the cancellation token
        .Catch <GraphQLResponse <TResponse>, OperationCanceledException>(exception =>
                                                                         Observable.Empty <GraphQLResponse <TResponse> >())
        // wrap results
        .Select(response => new Tuple <GraphQLResponse <TResponse>, Exception>(response, null))
        // do exception handling
        .Catch <Tuple <GraphQLResponse <TResponse>, Exception>, Exception>(e =>
        {
            try
            {
                if (exceptionHandler == null)
                {
                    // if the external handler is not set, propagate all exceptions except WebSocketExceptions
                    // this will ensure that the client tries to re-establish subscriptions on connection loss
                    if (!(e is WebSocketException))
                    {
                        throw e;
                    }
                }
                else
                {
                    // exceptions thrown by the handler will propagate to OnError()
                    exceptionHandler?.Invoke(e);
                }

                // throw exception on the observable to be caught by Retry() or complete sequence if cancellation was requested
                if (_internalCancellationToken.IsCancellationRequested)
                {
                    return(Observable.Empty <Tuple <GraphQLResponse <TResponse>, Exception> >());
                }
                else
                {
                    Debug.WriteLine($"Catch handler thread id: {Thread.CurrentThread.ManagedThreadId}");
                    return(Observable.Throw <Tuple <GraphQLResponse <TResponse>, Exception> >(e));
                }
            }
            catch (Exception exception)
            {
                // wrap all other exceptions to be propagated behind retry
                return(Observable.Return(new Tuple <GraphQLResponse <TResponse>, Exception>(null, exception)));
            }
        })
        // attempt to recreate the websocket for rethrown exceptions
        .Retry()
        // unwrap and push results or throw wrapped exceptions
        .SelectMany(t =>
        {
            Debug.WriteLine($"unwrap exception thread id: {Thread.CurrentThread.ManagedThreadId}");
            // if the result contains an exception, throw it on the observable
            if (t.Item2 != null)
            {
                return(Observable.Throw <GraphQLResponse <TResponse> >(t.Item2));
            }

            return(t.Item1 == null
                        ? Observable.Empty <GraphQLResponse <TResponse> >()
                        : Observable.Return(t.Item1));
        })
        // transform to hot observable and auto-connect
        .Publish().RefCount();
 private Task QueueWebSocketRequest(GraphQLWebSocketRequest request)
 {
     _requestSubject.OnNext(request);
     return(request.SendTask());
 }
 public Task SendWebSocketRequest(GraphQLWebSocketRequest request)
 {
     requestSubject.OnNext(request);
     return(request.SendTask());
 }
        private async Task ConnectAsync(CancellationToken token)
        {
            try
            {
                await BackOff();

                _stateSubject.OnNext(GraphQLWebsocketConnectionState.Connecting);
                Debug.WriteLine($"opening websocket {_clientWebSocket.GetHashCode()} (thread {Thread.CurrentThread.ManagedThreadId})");
                await _clientWebSocket.ConnectAsync(_webSocketUri, token);

                _stateSubject.OnNext(GraphQLWebsocketConnectionState.Connected);
                Debug.WriteLine($"connection established on websocket {_clientWebSocket.GetHashCode()}, invoking Options.OnWebsocketConnected()");
                await(Options.OnWebsocketConnected?.Invoke(_client) ?? Task.CompletedTask);
                Debug.WriteLine($"invoking Options.OnWebsocketConnected() on websocket {_clientWebSocket.GetHashCode()}");
                _connectionAttempt = 1;

                // create receiving observable
                _incomingMessages = Observable
                                    .Defer(() => GetReceiveTask().ToObservable())
                                    .Repeat()
                                    // complete sequence on OperationCanceledException, this is triggered by the cancellation token on disposal
                                    .Catch <WebsocketMessageWrapper, OperationCanceledException>(exception => Observable.Empty <WebsocketMessageWrapper>())
                                    .Publish();

                // subscribe maintenance
                var maintenanceSubscription = _incomingMessages.Subscribe(_ => { }, ex =>
                {
                    Debug.WriteLine($"incoming message stream {_incomingMessages.GetHashCode()} received an error: {ex}");
                    _exceptionSubject.OnNext(ex);
                    _incomingMessagesConnection?.Dispose();
                    _stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected);
                },
                                                                          () =>
                {
                    Debug.WriteLine($"incoming message stream {_incomingMessages.GetHashCode()} completed");
                    _incomingMessagesConnection?.Dispose();
                    _stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected);
                });


                // connect observable
                var connection = _incomingMessages.Connect();
                Debug.WriteLine($"new incoming message stream {_incomingMessages.GetHashCode()} created");

                _incomingMessagesConnection = new CompositeDisposable(maintenanceSubscription, connection);

                var initRequest = new GraphQLWebSocketRequest
                {
                    Type    = GraphQLWebSocketMessageType.GQL_CONNECTION_INIT,
                    Payload = Options.ConfigureWebSocketConnectionInitPayload(Options)
                };

                // setup task to await connection_ack message
                var ackTask = _incomingMessages
                              .Where(response => response != null)
                              .TakeUntil(response => response.Type == GraphQLWebSocketMessageType.GQL_CONNECTION_ACK ||
                                         response.Type == GraphQLWebSocketMessageType.GQL_CONNECTION_ERROR)
                              .FirstAsync()
                              .ToTask();

                // send connection init
                Debug.WriteLine($"sending connection init message");
                await SendWebSocketMessageAsync(initRequest);

                var response = await ackTask;

                if (response.Type == GraphQLWebSocketMessageType.GQL_CONNECTION_ACK)
                {
                    Debug.WriteLine($"connection acknowledged: {Encoding.UTF8.GetString(response.MessageBytes)}");
                }
                else
                {
                    var errorPayload = Encoding.UTF8.GetString(response.MessageBytes);
                    Debug.WriteLine($"connection error received: {errorPayload}");
                    throw new GraphQLWebsocketConnectionException(errorPayload);
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine($"failed to establish websocket connection");
                _stateSubject.OnNext(GraphQLWebsocketConnectionState.Disconnected);
                _exceptionSubject.OnNext(e);
                throw;
            }
        }
Example #11
0
        public void SerializeToBytesTest(string expectedJson, GraphQLWebSocketRequest request)
        {
            var json = Encoding.UTF8.GetString(Serializer.SerializeToBytes(request)).RemoveWhitespace();

            json.Should().BeEquivalentTo(expectedJson.RemoveWhitespace());
        }