Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscription listeners per subscription + support for graphql-transport-ws subprotocol #6

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
41 changes: 35 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -84,10 +85,11 @@ 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 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

Expand Down Expand Up @@ -124,6 +126,7 @@ public async void QueryOrMutation()
{"variable", "value"}
}),
null,
null,
"authToken",
"Bearer"
);
Expand Down Expand Up @@ -210,7 +213,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`.

Expand Down Expand Up @@ -256,16 +260,41 @@ 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
score
metadata
}
}
```

### 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
Expand All @@ -275,4 +304,4 @@ subscription GetScoresForLevel($level: String!) {
been fixed in a recent .NET version (but we don't have those fixes yet.)

<!-- ## Auth with Hasura
TBA -->
TBA -->
111 changes: 79 additions & 32 deletions Runtime/SimpleGraphQL/GraphQLClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using Newtonsoft.Json;
using UnityEngine;

namespace SimpleGraphQL
{
Expand All @@ -20,6 +21,9 @@ public class GraphQLClient
public string Endpoint;
public string AuthScheme;

// track the running subscriptions ids
internal HashSet<string> RunningSubscriptions;

public GraphQLClient(
string endpoint,
IEnumerable<Query> queries = null,
Expand All @@ -31,6 +35,7 @@ public GraphQLClient(
AuthScheme = authScheme;
SearchableQueries = queries?.ToList();
CustomHeaders = headers;
RunningSubscriptions = new HashSet<string>();
}

public GraphQLClient(GraphQLConfig config)
Expand All @@ -39,18 +44,21 @@ 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<string>();
}

/// <summary>
/// Send a query!
/// </summary>
/// <param name="request">The request you are sending.</param>
/// <param name="serializerSettings"></param>
/// <param name="headers">Any headers you want to pass</param>
/// <param name="authToken">The authToken</param>
/// <param name="authScheme">The authScheme to be used.</param>
/// <returns></returns>
public async Task<string> Send(
Request request,
JsonSerializerSettings serializerSettings = null,
Dictionary<string, string> headers = null,
string authToken = null,
string authScheme = null
Expand All @@ -74,6 +82,7 @@ public async Task<string> Send(
string postQueryAsync = await HttpUtils.PostRequest(
Endpoint,
request,
serializerSettings,
headers,
authToken,
authScheme
Expand All @@ -84,23 +93,25 @@ public async Task<string> Send(

public async Task<Response<TResponse>> Send<TResponse>(
Request request,
JsonSerializerSettings serializerSettings = null,
Dictionary<string, string> 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<Response<TResponse>>(json);
}

public async Task<Response<TResponse>> Send<TResponse>(
Func<TResponse> responseTypeResolver,
Request request,
JsonSerializerSettings serializerSettings = null,
Dictionary<string, string> headers = null,
string authToken = null,
string authScheme = null)
{
return await Send<TResponse>(request, headers, authToken, authScheme);
return await Send<TResponse>(request, serializerSettings, headers, authToken, authScheme);
}

/// <summary>
Expand All @@ -112,6 +123,21 @@ public void RegisterListener(Action<string> listener)
HttpUtils.SubscriptionDataReceived += listener;
}

public void RegisterListener(string id, Action<string> listener)
{
if (!HttpUtils.SubscriptionDataReceivedPerChannel.ContainsKey(id))
{
HttpUtils.SubscriptionDataReceivedPerChannel[id] = null;
}

HttpUtils.SubscriptionDataReceivedPerChannel[id] += listener;
}

public void RegisterListener(Request request, Action<string> listener)
{
RegisterListener(request.Query.ToMurmur2Hash().ToString(), listener);
}

/// <summary>
/// Unregisters a listener for subscriptions.
/// </summary>
Expand All @@ -121,6 +147,19 @@ public void UnregisterListener(Action<string> listener)
HttpUtils.SubscriptionDataReceived -= listener;
}

public void UnregisterListener(string id, Action<string> listener)
{
if (HttpUtils.SubscriptionDataReceivedPerChannel.ContainsKey(id))
{
HttpUtils.SubscriptionDataReceivedPerChannel[id] -= listener;
}
}

public void UnregisterListener(Request request, Action<string> listener)
{
UnregisterListener(request.Query.ToMurmur2Hash().ToString(), listener);
}

/// <summary>
/// Subscribe to a query in GraphQL.
/// </summary>
Expand All @@ -138,28 +177,7 @@ public async Task<bool> Subscribe(
string protocol = "graphql-ws"
)
{
if (CustomHeaders != null)
{
if (headers == null) headers = new Dictionary<string, string>();

foreach (KeyValuePair<string, string> 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);
}

/// <summary>
Expand Down Expand Up @@ -198,11 +216,27 @@ public async Task<bool> 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 exist, close connection again
if (RunningSubscriptions.Count == 0)
{
Debug.Log("No running subscription remain: close connection");
await HttpUtils.WebSocketDisconnect();
}
}
return success;

}


Expand All @@ -212,13 +246,7 @@ public async Task<bool> Subscribe(
/// <param name="request"></param>
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());
}

/// <summary>
Expand All @@ -233,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");
}
}

/// <summary>
Expand Down
Loading