Open MohamedSabthar opened 2 years ago
Good proposal.
I am thinking if it should be Subscriber
or Subscription
. I like the later. We can also consider ResponseStream
as mentioned in the spec. In that case, unsubscribe
should be cancel
.
Also, I wonder if close
should be, unsubscribeAll
assuming unsubscribe
closes the socket connection as well.
Ballerina streams
has a close method in that case do we need this intermediate client object.
The following changes are being introduced to the proposal, as discussed with @shafreenAnfar and @ThisaruGuruge:
Subscriber
client object, the function execute
returns a stream
.
remote isolated function execute(string document,
map<anydata>? variables = (),
string? operationName = (),
map<string|string[]>? headers = (),
typedesc<GenericResponseWithErrors|record{}|json|**stream**> targetType = <>)
returns targetType|ClientError = @java:Method {
'class: "io.ballerina.stdlib.graphql.runtime.client.QueryExecutor",
name: "execute"
} external;
unsubscribe
method of the Subscriber
client can be implemented with the stream.close()
method.close
method of graphql:Client
has been renamed to closeSubscriptions
.following is an example after above updates
import ballerina/io;
import ballerina/graphql;
public function main() returns error? {
// Initialize client - this internally initialize a websocket client with ws://localhost:4000/graphql endpoint
graphql:Client ep = check new ("http://localhost:4000//graphql", websocketConfig = {});
string document = string `subscription { totalDonations }`;
// execute operation from the return type binding it knows the operation in subscription (if type is stream then it's a subscription)
stream<Response> responseStream = check ep->execute(document);
// process stream
responseStream.forEach(r => io:println(r.totalDonations));
// close the stream which also send graphql-ws `complete` messsage to the service for this operation
check responseStream.close();
// close the websocket connection
ep->closeSubscriptions();
}
This is deferred due to other priorities.
Is there a specific reason to not have three separate methods for the three operations (e.g., query
, mutate
, and subscribe
) instead of or in addition to execute
? With the addition of subscription support, doesn't the return type get a bit complicated, especially given that the return type depends on the type of operation?
Summary
The ballerina graphql package provides client implementation to execute query and mutation operations by connecting to the server. But the subscription operation is not supported by the client. This proposal is to add subscription support to the existing client implementation.
Goals
Motivation
Ballerina doesn’t provide a simpler way to execute subscription operations by connecting to a graphql service. This is currently handled manually by creating a WebSocket client and writing the graphql subprotocols (graphql-ws/graphql-transport-ws) related code. Having the subscription support in the graphql client will reduce writing boilerplate subprotocol-related code and allow users to interact with the services with simple API.
Description
Similar to query operations, subscription operations enable the user to fetch data. Unlike queries, subscriptions are long-lasting operations that can change their result over time. They can maintain an active connection to the GraphQL server (most commonly via WebSocket), enabling the server to push updates to the subscription's result. This subscription operation is implemented in the ballerina graphql service using the graphql-ws subprotocol specification; the same protocol will be used to implement subscription operation on the client side. The following section describes the API changes to the existing
graphql:Client
to incorporate subscription operation.API changes
ClientConfiguration changes
The
ClientConfiguration
is updated to have both HTTP configuration and WebSocket configuration.Client changes
The client keeps a
wsClient
as its field and utilizes it when executing subscription operations. The client stores the id(specified in the spec) to Subscriber mapping in a ballerina map calledsubscribers
. This map allows the client to handle multiplexing between subscription responses. More details regarding theSubscriber
and the generation of unique id are described in later sections of this proposal.Apart from the above-described fields, the client keeps the
websocketConfig
andwsServiceUrl
fields. These two fields later will be used by the client to lazily initialize thewsClient
when there is a subscription operation executed by the client.Client init method
The init method of the graphql:Client sets the
wsConfig
andwsServiceUrl
fields. Values for these fields are obtained from init method parameters. For example, theserviceUrl
passed in the init method will be programmatically changed to have ws(s) protocol and set as the value of thewsServiceUrl
field.Client execute method
No changes are made to the execute method signature. But the data binding type of this method now includes a new type called
Subscriber
. Following is the execute method definition after the new type introduction.If the data binding type is
Subscriber
the client knows that it’s going to execute a subscription operation. The client generates a new UUID and stores the subscriber instance in thesubscribers
map whenever it executes a subscription operation. Further, the client checks the wsClient field value, if it is set to nil the wsClient will be initialized to a websocket:Client. ThewebsocketConfig
andwsServiceUrl
will be used during thiswsClient
initialization.Subscriber class
Subscriber
is a client class which holds the stream and id of a particular subscription operation. The client class definition is given belowAfter executing the subscription operation the resulting stream can be obtained by executing the
getStream
method on theSubscriber
instance. ThisgetStream
method allows data binding on the steam type. Further, the subscription can be cancelled by calling the unsubscribe method on theSubscriber
instance.Client close method
A new close method could be introduced in the
graphql:Client
. Executing this method sends a “complete/stop” message to all subscription operations and closes the websocket connection. executing subscription operation after callingclose()
method will return agraphql:ClientError
(Websocket connection already closed)Following is an example showing a client executing a subscription operation.
Handling multiple subscriptions with graphql:Client
Following pseudocode explains the client multiplexing logic
type
isnext
orerror
:type
iscomplete
: