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); } }
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; } }
public void SerializeToBytesTest(string expectedJson, GraphQLWebSocketRequest request) { var json = Encoding.UTF8.GetString(Serializer.SerializeToBytes(request)).RemoveWhitespace(); json.Should().BeEquivalentTo(expectedJson.RemoveWhitespace()); }