From 9d3d6aed9a5de20c5bb6e6cb67ea32cb1adc2499 Mon Sep 17 00:00:00 2001 From: Pieter Soudan Date: Sun, 19 Jun 2022 10:09:01 +0200 Subject: [PATCH 01/11] channel subscribed data per subscription support graphql-transport-ws protocol --- Runtime/SimpleGraphQL/GraphQLClient.cs | 25 ++++++++++++++++ Runtime/SimpleGraphQL/HttpUtils.cs | 41 +++++++++++++++++++++----- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/Runtime/SimpleGraphQL/GraphQLClient.cs b/Runtime/SimpleGraphQL/GraphQLClient.cs index c899aa2..fb416ad 100644 --- a/Runtime/SimpleGraphQL/GraphQLClient.cs +++ b/Runtime/SimpleGraphQL/GraphQLClient.cs @@ -112,6 +112,19 @@ public void RegisterListener(Action listener) HttpUtils.SubscriptionDataReceived += listener; } + public void RegisterListener(string id, Action listener) + { + if(!HttpUtils.SubscriptionDataReceivedPerChannel.ContainsKey(id)) { + HttpUtils.SubscriptionDataReceivedPerChannel[id] = null; + } + HttpUtils.SubscriptionDataReceivedPerChannel[id] += listener; + } + + public void RegisterListener(Request request, Action listener) + { + RegisterListener(request.Query.ToMurmur2Hash().ToString(), listener); + } + /// /// Unregisters a listener for subscriptions. /// @@ -121,6 +134,18 @@ public void UnregisterListener(Action listener) HttpUtils.SubscriptionDataReceived -= listener; } + public void UnregisterListener(string id, Action listener) + { + if(HttpUtils.SubscriptionDataReceivedPerChannel.ContainsKey(id)) { + HttpUtils.SubscriptionDataReceivedPerChannel[id] -= listener; + } + } + + public void UnregisterListener(Request request, Action listener) + { + UnregisterListener(request.Query.ToMurmur2Hash().ToString(), listener); + } + /// /// Subscribe to a query in GraphQL. /// diff --git a/Runtime/SimpleGraphQL/HttpUtils.cs b/Runtime/SimpleGraphQL/HttpUtils.cs index c2e2e50..2fa1507 100644 --- a/Runtime/SimpleGraphQL/HttpUtils.cs +++ b/Runtime/SimpleGraphQL/HttpUtils.cs @@ -21,12 +21,14 @@ public static class HttpUtils /// Called when the websocket receives subscription data. /// public static event Action SubscriptionDataReceived; + public static Dictionary> SubscriptionDataReceivedPerChannel; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] public static void PreInit() { _webSocket?.Dispose(); SubscriptionDataReceived = null; + SubscriptionDataReceivedPerChannel = new Dictionary>(); } /// @@ -138,8 +140,16 @@ public static async Task WebSocketConnect( _webSocket = new ClientWebSocket(); _webSocket.Options.AddSubProtocol(protocol); - if (authToken != null) - _webSocket.Options.SetRequestHeader("Authorization", $"{authScheme} {authToken}"); + var payload = protocol == 'graphql-transport-ws' ? new {Authorization = null, "content-type", "application/json"} : new {}; + + if (authToken != null) { + if(protocol == 'graphql-transport-ws') { + // set Authorization as payload + payload.Authorization = $"{authScheme} {authToken}"; + } else { + _webSocket.Options.SetRequestHeader("Authorization", $"{authScheme} {authToken}"); + } + } _webSocket.Options.SetRequestHeader("Content-Type", "application/json"); @@ -156,12 +166,23 @@ public static async Task WebSocketConnect( Debug.Log("Websocket is connecting"); await _webSocket.ConnectAsync(uri, CancellationToken.None); + var json = JsonConvert.SerializeObject( + new + { + type = "connection_init", + payload = payload + }, + Formatting.None, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + } + ); + Debug.Log("Websocket is starting"); // Initialize the socket at the server side await _webSocket.SendAsync( - new ArraySegment( - Encoding.UTF8.GetBytes(@"{""type"":""connection_init"",""payload"": {}}") - ), + new ArraySegment(Encoding.UTF8.GetBytes(json)), WebSocketMessageType.Text, true, CancellationToken.None @@ -210,7 +231,7 @@ public static async Task WebSocketSubscribe(string id, Request request) new { id, - type = "start", + type = _webSocket.SubProtocol == "graphql-transport-ws" ? "subscribe" : "start", payload = new { query = request.Query, @@ -248,8 +269,10 @@ public static async Task WebSocketUnsubscribe(string id) return; } + var type = _webSocket.SubProtocol == "graphql-transport-ws" ? "complete" : "stop"; + await _webSocket.SendAsync( - new ArraySegment(Encoding.UTF8.GetBytes($@"{{""type"":""stop"",""id"":""{id}""}}")), + new ArraySegment(Encoding.UTF8.GetBytes($@"{{""type"":""{type}"",""id"":""{id}""}}")), WebSocketMessageType.Text, true, CancellationToken.None @@ -292,6 +315,7 @@ private static async void WebSocketUpdate() } var msgType = (string)jsonObj["type"]; + var id = (string)jsonObj["id"]; switch (msgType) { case "connection_error": @@ -300,7 +324,7 @@ private static async void WebSocketUpdate() } case "connection_ack": { - Debug.Log("Websocket connection acknowledged."); + Debug.Log($"Websocket connection acknowledged ({id})."); continue; } case "data": @@ -311,6 +335,7 @@ private static async void WebSocketUpdate() if (jToken != null) { SubscriptionDataReceived?.Invoke(jToken.ToString()); + SubscriptionDataReceived?[id]?.Invoke(jToken.ToString()); } continue; From c0e399769e290e7345612bd009124da83957469a Mon Sep 17 00:00:00 2001 From: Pieter Soudan Date: Sun, 19 Jun 2022 10:23:01 +0200 Subject: [PATCH 02/11] some fixes --- Runtime/SimpleGraphQL/HttpUtils.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Runtime/SimpleGraphQL/HttpUtils.cs b/Runtime/SimpleGraphQL/HttpUtils.cs index 2fa1507..be6223e 100644 --- a/Runtime/SimpleGraphQL/HttpUtils.cs +++ b/Runtime/SimpleGraphQL/HttpUtils.cs @@ -140,18 +140,23 @@ public static async Task WebSocketConnect( _webSocket = new ClientWebSocket(); _webSocket.Options.AddSubProtocol(protocol); - var payload = protocol == 'graphql-transport-ws' ? new {Authorization = null, "content-type", "application/json"} : new {}; + var payload = new Dictionary(); + + if(protocol == "graphql-transport-ws") { + payload["content-type"] = "application/json"; + } else { + _webSocket.Options.SetRequestHeader("Content-Type", "application/json"); + } if (authToken != null) { - if(protocol == 'graphql-transport-ws') { + if(protocol == "graphql-transport-ws") { // set Authorization as payload - payload.Authorization = $"{authScheme} {authToken}"; + payload["Authorization"] = $"{authScheme} {authToken}"; } else { _webSocket.Options.SetRequestHeader("Authorization", $"{authScheme} {authToken}"); } } - _webSocket.Options.SetRequestHeader("Content-Type", "application/json"); if (headers != null) { @@ -335,7 +340,7 @@ private static async void WebSocketUpdate() if (jToken != null) { SubscriptionDataReceived?.Invoke(jToken.ToString()); - SubscriptionDataReceived?[id]?.Invoke(jToken.ToString()); + SubscriptionDataReceivedPerChannel?[id]?.Invoke(jToken.ToString()); } continue; @@ -374,4 +379,4 @@ await _webSocket.SendAsync( } } } -} \ No newline at end of file +} From b4a473becf9d0558d1b103dddc1826c23a81f582 Mon Sep 17 00:00:00 2001 From: Skjalg Date: Tue, 28 Jun 2022 09:24:05 +0200 Subject: [PATCH 03/11] added JsonSerializerSettings pass through --- Runtime/SimpleGraphQL/GraphQLClient.cs | 8 ++++++-- Runtime/SimpleGraphQL/HttpUtils.cs | 3 ++- Runtime/SimpleGraphQL/Request.cs | 15 ++++++++++----- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Runtime/SimpleGraphQL/GraphQLClient.cs b/Runtime/SimpleGraphQL/GraphQLClient.cs index fb416ad..e53c970 100644 --- a/Runtime/SimpleGraphQL/GraphQLClient.cs +++ b/Runtime/SimpleGraphQL/GraphQLClient.cs @@ -51,6 +51,7 @@ public GraphQLClient(GraphQLConfig config) /// public async Task Send( Request request, + JsonSerializerSettings serializerSettings = null, Dictionary headers = null, string authToken = null, string authScheme = null @@ -74,6 +75,7 @@ public async Task Send( string postQueryAsync = await HttpUtils.PostRequest( Endpoint, request, + serializerSettings, headers, authToken, authScheme @@ -84,23 +86,25 @@ public async Task Send( public async Task> Send( Request request, + JsonSerializerSettings serializerSettings = null, Dictionary headers = null, string authToken = null, string authScheme = null ) { - string json = await Send(request, headers, authToken, authScheme); + string json = await Send(request, serializerSettings, headers, authToken, authScheme); return JsonConvert.DeserializeObject>(json); } public async Task> Send( Func responseTypeResolver, Request request, + JsonSerializerSettings serializerSettings = null, Dictionary headers = null, string authToken = null, string authScheme = null) { - return await Send(request, headers, authToken, authScheme); + return await Send(request, serializerSettings, headers, authToken, authScheme); } /// diff --git a/Runtime/SimpleGraphQL/HttpUtils.cs b/Runtime/SimpleGraphQL/HttpUtils.cs index be6223e..1b36860 100644 --- a/Runtime/SimpleGraphQL/HttpUtils.cs +++ b/Runtime/SimpleGraphQL/HttpUtils.cs @@ -52,6 +52,7 @@ public static void Dispose() public static async Task PostRequest( string url, Request request, + JsonSerializerSettings serializerSettings = null, Dictionary headers = null, string authToken = null, string authScheme = null @@ -59,7 +60,7 @@ public static async Task PostRequest( { var uri = new Uri(url); - byte[] payload = request.ToBytes(); + byte[] payload = request.ToBytes(serializerSettings); using (var webRequest = new UnityWebRequest(uri, "POST") { diff --git a/Runtime/SimpleGraphQL/Request.cs b/Runtime/SimpleGraphQL/Request.cs index 5b38eef..7ec96a6 100644 --- a/Runtime/SimpleGraphQL/Request.cs +++ b/Runtime/SimpleGraphQL/Request.cs @@ -26,18 +26,23 @@ public override string ToString() [PublicAPI] public static class RequestExtensions { - public static byte[] ToBytes(this Request request) + private static JsonSerializerSettings defaultSerializerSettings = new JsonSerializerSettings {NullValueHandling = NullValueHandling.Ignore}; + public static byte[] ToBytes(this Request request, JsonSerializerSettings serializerSettings = null) { - return Encoding.UTF8.GetBytes(request.ToJson()); + return Encoding.UTF8.GetBytes(request.ToJson(false, serializerSettings)); } - public static string ToJson(this Request request, - bool prettyPrint = false) + public static string ToJson(this Request request, bool prettyPrint = false, JsonSerializerSettings serializerSettings = null) { + if (serializerSettings == null) + { + serializerSettings = defaultSerializerSettings; + } + return JsonConvert.SerializeObject ( request, prettyPrint ? Formatting.Indented : Formatting.None, - new JsonSerializerSettings {NullValueHandling = NullValueHandling.Ignore} + serializerSettings ); } } From 0264a3a4bcb5d7c16f593c6e759bb3ef55009c5e Mon Sep 17 00:00:00 2001 From: Navid Kabir Date: Tue, 28 Jun 2022 08:16:14 -0400 Subject: [PATCH 04/11] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f11bc92..05aa73d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.lastabyss.simplegraphql", - "version": "1.3.2", + "version": "1.4.0", "displayName": "SimpleGraphQL", "description": "A simple graphQL client that allows one to use .graphql files (or code) for queries, mutations, and subscriptions with Unity.", "unity": "2019.4", From c2d9e1a8766206b87bac9109a698002b13867028 Mon Sep 17 00:00:00 2001 From: Navid Kabir Date: Tue, 28 Jun 2022 09:39:08 -0400 Subject: [PATCH 05/11] Code cleanup and documentation update --- README.md | 36 ++++++++++++++++-- Runtime/SimpleGraphQL/GraphQLClient.cs | 12 ++++-- Runtime/SimpleGraphQL/HttpUtils.cs | 52 ++++++++++++++++---------- Runtime/SimpleGraphQL/Request.cs | 11 ++++-- 4 files changed, 79 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index e1523d7..54c8710 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ That being said, this is intended to be a primarily code based package, so keep | WebGL | ✔ | ❌ | This should work with all platforms (Mono/IL2CPP) except for subscriptions on WebGL. -It makes use of UnityWebRequest where possible, but C# WebSockets are the main issue, so subscriptions will not properly work. If you do not need +It makes use of UnityWebRequest where possible, but C# WebSockets are the main issue, so subscriptions will not properly +work. If you do not need subscriptions, WebGL will work just fine. Work may be added to support WebGL in the future, but for now, there is no support. @@ -87,7 +88,8 @@ var response = await client.Send(() => responseType, request); Debug.Log(response.Result.Data.continent.name); ``` -SimpleGraphQL also lets you store queries in .graphql files that you must write yourself. It is up to you to make sure they are valid. Many IDEs support this function natively or through plugins. +SimpleGraphQL also lets you store queries in .graphql files that you must write yourself. It is up to you to make sure +they are valid. Many IDEs support this function natively or through plugins. ## Configuration @@ -210,7 +212,8 @@ public void OnComplete(string result) # Authentication and Headers -> Depending on your authentication method, it is up to you to ensure that your authentication data and headers are set correctly. +> Depending on your authentication method, it is up to you to ensure that your authentication data and headers are set +> correctly. ### Custom headers and auth tokens are natively supported in SimpleGraphQL. They can be passed in as parameters when calling `Subscribe` or `Send`. @@ -256,7 +259,7 @@ mutation UpsertScore($user_id: String!, $level: String!, $score: bigint! $metada } } -subscription GetScoresForLevel($level: String!) { +query ListLevelScores($level: String!) { leaderboards(where: {level: {_eq: $level}}) { user_id level @@ -266,6 +269,31 @@ subscription GetScoresForLevel($level: String!) { } ``` +### Subscriptions.graphql + +```graphql +subscription OnScoresUpdated($level: String!) { + leaderboards(where: {level: {_eq: $level}}) { + user_id + level + score + metadata + } +} + +subscription OnAnyScoresUpdated { + leaderboards { + user_id + level + score + metadata + } +} +``` + +> NOTE: We recommend putting graphQL subscriptions in a separate file. Mixing queries, mutations, and subscriptions +> together in one file may lead to odd/undocumented behavior on various servers. + # Things to Note - During testing, we found that Unity's version of .NET occasionally has issues with HttpClient and WebSocket. If you diff --git a/Runtime/SimpleGraphQL/GraphQLClient.cs b/Runtime/SimpleGraphQL/GraphQLClient.cs index e53c970..3e4625b 100644 --- a/Runtime/SimpleGraphQL/GraphQLClient.cs +++ b/Runtime/SimpleGraphQL/GraphQLClient.cs @@ -45,6 +45,7 @@ public GraphQLClient(GraphQLConfig config) /// Send a query! /// /// The request you are sending. + /// /// Any headers you want to pass /// The authToken /// The authScheme to be used. @@ -118,9 +119,11 @@ public void RegisterListener(Action listener) public void RegisterListener(string id, Action listener) { - if(!HttpUtils.SubscriptionDataReceivedPerChannel.ContainsKey(id)) { - HttpUtils.SubscriptionDataReceivedPerChannel[id] = null; + if (!HttpUtils.SubscriptionDataReceivedPerChannel.ContainsKey(id)) + { + HttpUtils.SubscriptionDataReceivedPerChannel[id] = null; } + HttpUtils.SubscriptionDataReceivedPerChannel[id] += listener; } @@ -140,8 +143,9 @@ public void UnregisterListener(Action listener) public void UnregisterListener(string id, Action listener) { - if(HttpUtils.SubscriptionDataReceivedPerChannel.ContainsKey(id)) { - HttpUtils.SubscriptionDataReceivedPerChannel[id] -= listener; + if (HttpUtils.SubscriptionDataReceivedPerChannel.ContainsKey(id)) + { + HttpUtils.SubscriptionDataReceivedPerChannel[id] -= listener; } } diff --git a/Runtime/SimpleGraphQL/HttpUtils.cs b/Runtime/SimpleGraphQL/HttpUtils.cs index 1b36860..992b330 100644 --- a/Runtime/SimpleGraphQL/HttpUtils.cs +++ b/Runtime/SimpleGraphQL/HttpUtils.cs @@ -21,6 +21,7 @@ public static class HttpUtils /// Called when the websocket receives subscription data. /// public static event Action SubscriptionDataReceived; + public static Dictionary> SubscriptionDataReceivedPerChannel; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] @@ -47,6 +48,7 @@ public static void Dispose() /// The GraphQL request /// The authentication scheme to be used. /// The actual auth token. + /// /// Any headers that should be passed in /// public static async Task PostRequest( @@ -63,13 +65,13 @@ public static async Task PostRequest( byte[] payload = request.ToBytes(serializerSettings); using (var webRequest = new UnityWebRequest(uri, "POST") - { - uploadHandler = new UploadHandlerRaw(payload), - downloadHandler = new DownloadHandlerBuffer(), - disposeCertificateHandlerOnDispose = true, - disposeDownloadHandlerOnDispose = true, - disposeUploadHandlerOnDispose = true - }) + { + uploadHandler = new UploadHandlerRaw(payload), + downloadHandler = new DownloadHandlerBuffer(), + disposeCertificateHandlerOnDispose = true, + disposeDownloadHandlerOnDispose = true, + disposeUploadHandlerOnDispose = true + }) { if (authToken != null) webRequest.SetRequestHeader("Authorization", $"{authScheme} {authToken}"); @@ -143,17 +145,24 @@ public static async Task WebSocketConnect( var payload = new Dictionary(); - if(protocol == "graphql-transport-ws") { - payload["content-type"] = "application/json"; - } else { - _webSocket.Options.SetRequestHeader("Content-Type", "application/json"); + if (protocol == "graphql-transport-ws") + { + payload["content-type"] = "application/json"; + } + else + { + _webSocket.Options.SetRequestHeader("Content-Type", "application/json"); } - if (authToken != null) { - if(protocol == "graphql-transport-ws") { + if (authToken != null) + { + if (protocol == "graphql-transport-ws") + { // set Authorization as payload payload["Authorization"] = $"{authScheme} {authToken}"; - } else { + } + else + { _webSocket.Options.SetRequestHeader("Authorization", $"{authScheme} {authToken}"); } } @@ -172,7 +181,7 @@ public static async Task WebSocketConnect( Debug.Log("Websocket is connecting"); await _webSocket.ConnectAsync(uri, CancellationToken.None); - var json = JsonConvert.SerializeObject( + string json = JsonConvert.SerializeObject( new { type = "connection_init", @@ -275,7 +284,7 @@ public static async Task WebSocketUnsubscribe(string id) return; } - var type = _webSocket.SubProtocol == "graphql-transport-ws" ? "complete" : "stop"; + string type = _webSocket.SubProtocol == "graphql-transport-ws" ? "complete" : "stop"; await _webSocket.SendAsync( new ArraySegment(Encoding.UTF8.GetBytes($@"{{""type"":""{type}"",""id"":""{id}""}}")), @@ -289,8 +298,7 @@ private static async void WebSocketUpdate() { while (true) { - ArraySegment buffer; - buffer = WebSocket.CreateClientBuffer(1024, 1024); + ArraySegment buffer = WebSocket.CreateClientBuffer(1024, 1024); if (buffer.Array == null) { @@ -341,7 +349,11 @@ private static async void WebSocketUpdate() if (jToken != null) { SubscriptionDataReceived?.Invoke(jToken.ToString()); - SubscriptionDataReceivedPerChannel?[id]?.Invoke(jToken.ToString()); + + if (id != null) + { + SubscriptionDataReceivedPerChannel?[id]?.Invoke(jToken.ToString()); + } } continue; @@ -380,4 +392,4 @@ await _webSocket.SendAsync( } } } -} +} \ No newline at end of file diff --git a/Runtime/SimpleGraphQL/Request.cs b/Runtime/SimpleGraphQL/Request.cs index 7ec96a6..3c68bf8 100644 --- a/Runtime/SimpleGraphQL/Request.cs +++ b/Runtime/SimpleGraphQL/Request.cs @@ -26,21 +26,24 @@ public override string ToString() [PublicAPI] public static class RequestExtensions { - private static JsonSerializerSettings defaultSerializerSettings = new JsonSerializerSettings {NullValueHandling = NullValueHandling.Ignore}; + private static JsonSerializerSettings defaultSerializerSettings = new JsonSerializerSettings + { NullValueHandling = NullValueHandling.Ignore }; + public static byte[] ToBytes(this Request request, JsonSerializerSettings serializerSettings = null) { return Encoding.UTF8.GetBytes(request.ToJson(false, serializerSettings)); } - public static string ToJson(this Request request, bool prettyPrint = false, JsonSerializerSettings serializerSettings = null) + public static string ToJson(this Request request, bool prettyPrint = false, + JsonSerializerSettings serializerSettings = null) { if (serializerSettings == null) { serializerSettings = defaultSerializerSettings; } - + return JsonConvert.SerializeObject - ( request, + (request, prettyPrint ? Formatting.Indented : Formatting.None, serializerSettings ); From 88a58ab62162f040267fd3665a08c2394310f198 Mon Sep 17 00:00:00 2001 From: Navid Kabir Date: Sat, 16 Jul 2022 10:56:43 -0700 Subject: [PATCH 06/11] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 54c8710..1359f66 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ public async void QueryOrMutation() {"variable", "value"} }), null, + null, "authToken", "Bearer" ); @@ -303,4 +304,4 @@ subscription OnAnyScoresUpdated { been fixed in a recent .NET version (but we don't have those fixes yet.) \ No newline at end of file +TBA --> From 1e987605ca452ec62dd142b36a26b8f208257819 Mon Sep 17 00:00:00 2001 From: Navid Kabir Date: Sat, 16 Jul 2022 10:57:13 -0700 Subject: [PATCH 07/11] Update package.json Bump major version for breaking changes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05aa73d..e795a88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.lastabyss.simplegraphql", - "version": "1.4.0", + "version": "2.0.0", "displayName": "SimpleGraphQL", "description": "A simple graphQL client that allows one to use .graphql files (or code) for queries, mutations, and subscriptions with Unity.", "unity": "2019.4", From c6118406f5ecc60dc80d173d3bbfe50da7672327 Mon Sep 17 00:00:00 2001 From: Pieter Soudan Date: Tue, 26 Jul 2022 11:18:02 +0200 Subject: [PATCH 08/11] track running subs + close cnx when no subs remain --- Runtime/SimpleGraphQL/GraphQLClient.cs | 74 ++++++++++-------- Runtime/SimpleGraphQL/HttpUtils.cs | 102 +++++++++++++------------ 2 files changed, 99 insertions(+), 77 deletions(-) diff --git a/Runtime/SimpleGraphQL/GraphQLClient.cs b/Runtime/SimpleGraphQL/GraphQLClient.cs index 3e4625b..22bbbc0 100644 --- a/Runtime/SimpleGraphQL/GraphQLClient.cs +++ b/Runtime/SimpleGraphQL/GraphQLClient.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Newtonsoft.Json; +using UnityEngine; namespace SimpleGraphQL { @@ -20,6 +21,9 @@ public class GraphQLClient public string Endpoint; public string AuthScheme; + // track the running subscriptions ids + HashSet RunningSubscriptions; + public GraphQLClient( string endpoint, IEnumerable queries = null, @@ -31,6 +35,7 @@ public GraphQLClient( AuthScheme = authScheme; SearchableQueries = queries?.ToList(); CustomHeaders = headers; + RunningSubscriptions = new HashSet(); } public GraphQLClient(GraphQLConfig config) @@ -39,6 +44,7 @@ public GraphQLClient(GraphQLConfig config) SearchableQueries = config.Files.SelectMany(x => x.Queries).ToList(); CustomHeaders = config.CustomHeaders.ToDictionary(header => header.Key, header => header.Value); AuthScheme = config.AuthScheme; + RunningSubscriptions = new HashSet(); } /// @@ -171,28 +177,7 @@ public async Task Subscribe( string protocol = "graphql-ws" ) { - if (CustomHeaders != null) - { - if (headers == null) headers = new Dictionary(); - - foreach (KeyValuePair header in CustomHeaders) - { - headers.Add(header.Key, header.Value); - } - } - - if (authScheme == null) - { - authScheme = AuthScheme; - } - - if (!HttpUtils.IsWebSocketReady()) - { - // Prepare the socket before continuing. - await HttpUtils.WebSocketConnect(Endpoint, headers, authToken, authScheme, protocol); - } - - return await HttpUtils.WebSocketSubscribe(request.Query.ToMurmur2Hash().ToString(), request); + return await Subscribe(request.Query.ToMurmur2Hash().ToString(), request, headers, authToken, authScheme, protocol); } /// @@ -231,11 +216,27 @@ public async Task Subscribe( if (!HttpUtils.IsWebSocketReady()) { + Debug.Log("websocket not ready: open connection"); // Prepare the socket before continuing. await HttpUtils.WebSocketConnect(Endpoint, headers, authToken, authScheme, protocol); } - return await HttpUtils.WebSocketSubscribe(id, request); + bool success = await HttpUtils.WebSocketSubscribe(id, request); + if (success) + { + RunningSubscriptions.Add(id); + } + else + { + // if no other subscriptions existm close connection again + if (RunningSubscriptions.Count == 0) + { + Debug.Log("No running subscription remain: close connection"); + await HttpUtils.WebSocketDisconnect(); + } + } + return success; + } @@ -245,13 +246,7 @@ public async Task Subscribe( /// public async Task Unsubscribe(Request request) { - if (!HttpUtils.IsWebSocketReady()) - { - // Socket is already apparently closed, so this wouldn't work anyways. - return; - } - - await HttpUtils.WebSocketUnsubscribe(request.Query.ToMurmur2Hash().ToString()); + await Unsubscribe(request.Query.ToMurmur2Hash().ToString()); } /// @@ -266,7 +261,26 @@ public async Task Unsubscribe(string id) return; } + // when unsubscribing an unexisting id (or already unsubscribed) + if (!RunningSubscriptions.Contains(id)) + { + Debug.LogError("Attempted to unsubscribe to a query without subscribing first!"); + return; + } + + // TODO: what if this fails? await HttpUtils.WebSocketUnsubscribe(id); + + RunningSubscriptions.Remove(id); + + // if no active subscriptions remain, stop the connection + // this will also stop the update loop + if (RunningSubscriptions.Count == 0) + { + Debug.Log("No running subscription remain: close connection"); + await HttpUtils.WebSocketDisconnect(); + Debug.Log("connection closed"); + } } /// diff --git a/Runtime/SimpleGraphQL/HttpUtils.cs b/Runtime/SimpleGraphQL/HttpUtils.cs index 992b330..09fa99f 100644 --- a/Runtime/SimpleGraphQL/HttpUtils.cs +++ b/Runtime/SimpleGraphQL/HttpUtils.cs @@ -65,13 +65,13 @@ public static async Task PostRequest( byte[] payload = request.ToBytes(serializerSettings); using (var webRequest = new UnityWebRequest(uri, "POST") - { - uploadHandler = new UploadHandlerRaw(payload), - downloadHandler = new DownloadHandlerBuffer(), - disposeCertificateHandlerOnDispose = true, - disposeDownloadHandlerOnDispose = true, - disposeUploadHandlerOnDispose = true - }) + { + uploadHandler = new UploadHandlerRaw(payload), + downloadHandler = new DownloadHandlerBuffer(), + disposeCertificateHandlerOnDispose = true, + disposeDownloadHandlerOnDispose = true, + disposeUploadHandlerOnDispose = true + }) { if (authToken != null) webRequest.SetRequestHeader("Authorization", $"{authScheme} {authToken}"); @@ -226,6 +226,7 @@ public static async Task WebSocketDisconnect() } await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Socket closed.", CancellationToken.None); + Dispose(); } /// @@ -298,6 +299,13 @@ private static async void WebSocketUpdate() { while (true) { + // break the loop as soon as the websocket was closed + if (!IsWebSocketReady()) + { + Debug.Log("websocket was closed, stop the loop"); + break; + } + ArraySegment buffer = WebSocket.CreateClientBuffer(1024, 1024); if (buffer.Array == null) @@ -333,59 +341,59 @@ private static async void WebSocketUpdate() switch (msgType) { case "connection_error": - { - throw new WebSocketException("Connection error. Error: " + jsonResult); - } + { + throw new WebSocketException("Connection error. Error: " + jsonResult); + } case "connection_ack": - { - Debug.Log($"Websocket connection acknowledged ({id})."); - continue; - } + { + Debug.Log($"Websocket connection acknowledged ({id})."); + continue; + } case "data": case "next": - { - JToken jToken = jsonObj["payload"]; - - if (jToken != null) { - SubscriptionDataReceived?.Invoke(jToken.ToString()); + JToken jToken = jsonObj["payload"]; - if (id != null) + if (jToken != null) { - SubscriptionDataReceivedPerChannel?[id]?.Invoke(jToken.ToString()); + SubscriptionDataReceived?.Invoke(jToken.ToString()); + + if (id != null) + { + SubscriptionDataReceivedPerChannel?[id]?.Invoke(jToken.ToString()); + } } - } - continue; - } + continue; + } case "error": - { - throw new WebSocketException("Handshake error. Error: " + jsonResult); - } + { + throw new WebSocketException("Handshake error. Error: " + jsonResult); + } case "complete": - { - Debug.Log("Server sent complete, it's done sending data."); - break; - } + { + Debug.Log("Server sent complete, it's done sending data."); + continue; + } case "ka": - { - // stayin' alive, stayin' alive - continue; - } + { + // stayin' alive, stayin' alive + continue; + } case "subscription_fail": - { - throw new WebSocketException("Subscription failed. Error: " + jsonResult); - } + { + throw new WebSocketException("Subscription failed. Error: " + jsonResult); + } case "ping": - { - await _webSocket.SendAsync( - new ArraySegment(Encoding.UTF8.GetBytes($@"{{""type"":""pong""}}")), - WebSocketMessageType.Text, - true, - CancellationToken.None - ); - continue; - } + { + await _webSocket.SendAsync( + new ArraySegment(Encoding.UTF8.GetBytes($@"{{""type"":""pong""}}")), + WebSocketMessageType.Text, + true, + CancellationToken.None + ); + continue; + } } break; From 70d0423297bee8d43aa440889b67707b8455ed5a Mon Sep 17 00:00:00 2001 From: Pieter Soudan Date: Tue, 26 Jul 2022 18:07:15 +0200 Subject: [PATCH 09/11] declare RunningSubscriptions as internal --- Runtime/SimpleGraphQL/GraphQLClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Runtime/SimpleGraphQL/GraphQLClient.cs b/Runtime/SimpleGraphQL/GraphQLClient.cs index 22bbbc0..bb5096d 100644 --- a/Runtime/SimpleGraphQL/GraphQLClient.cs +++ b/Runtime/SimpleGraphQL/GraphQLClient.cs @@ -22,7 +22,7 @@ public class GraphQLClient public string AuthScheme; // track the running subscriptions ids - HashSet RunningSubscriptions; + internal HashSet RunningSubscriptions; public GraphQLClient( string endpoint, @@ -228,7 +228,7 @@ public async Task Subscribe( } else { - // if no other subscriptions existm close connection again + // if no other subscriptions exist, close connection again if (RunningSubscriptions.Count == 0) { Debug.Log("No running subscription remain: close connection"); From 6be3bba8b13f7f3daea1740e738fe7c86da57930 Mon Sep 17 00:00:00 2001 From: Navid Kabir Date: Tue, 26 Jul 2022 09:17:49 -0700 Subject: [PATCH 10/11] Bump minor version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e795a88..ea9e05d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.lastabyss.simplegraphql", - "version": "2.0.0", + "version": "2.1.0", "displayName": "SimpleGraphQL", "description": "A simple graphQL client that allows one to use .graphql files (or code) for queries, mutations, and subscriptions with Unity.", "unity": "2019.4", From a0ff45929ed83be7dcc76b7700ab2d86982677eb Mon Sep 17 00:00:00 2001 From: Navid Kabir Date: Sat, 31 Dec 2022 10:35:10 -0500 Subject: [PATCH 11/11] Update README.md Fix incorrect documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1359f66..f5de186 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ var request = new Request }; var responseType = new { continent = new { name = "" } }; var response = await client.Send(() => responseType, request); -Debug.Log(response.Result.Data.continent.name); +Debug.Log(response.Data.continent.name); ``` SimpleGraphQL also lets you store queries in .graphql files that you must write yourself. It is up to you to make sure