by Jeff Lowery
How to Wrap a Streaming I/O Interface in GraphQL
This post will be about using GraphQL to handle a service that uses an I/O stream for interaction between client and server. In a previous post, I mocked up a GraphQL API to the Universal Chess Interface (UCI). The UCI uses stdio to communicate, accepting commands from an input stream and sending responses via an output stream. I’ll be using UCI as an illustration, but I won’t be describing UCI in great detail.
- create and cd into a folder
npm install stockfish
And from there you can type in UCI commands in terminal window and see the results.
A review of Query vs Mutation
Queries are executed in parallel. That is not a problem for a stateless API where each query will return the same result regardless of the order in which results are returned. UCI is not stateless, so commands and results have to operate in sequence. Here’s an example of interaction between the command line ‘client’ and chess engine:
Engine responses to client commands are indented. The first state transition is to initiate the UCI protocol, where the engine responds with default option settings and a uciok signal indicating it is finished. At this point, the client can configure options. These will only take effect when the command isready is issued. The engine responds with readyok when all options are set. Later state transitions will occur during game set up and analysis (not shown).
Running several queries in parallel may issue commands prematurely, since no query waits for the response of another query. The problem can be illustrated with a simple GraphQL API to an mock asynchronous service:
The results are:
In the console windows (bottom half), you can see when responses were returned. Now execute the same requests via Mutation:
Getting a response takes longer because each operation must finish before the next is invoked.
What this means for a GraphQL UCI wrapper
In a previous post, I gave arguments for why GraphQL might be used to wrap UCI. Perhaps the easiest way to do this is to use GraphQL’s subscription service. This will send events back to the client via a web socket. Commands are sent via Queries or Mutations, and the responses come back as subscribed-to events.
In the case of UCI interaction, mutations would be used to ensure that commands are executed in the expected sequence. Before executing a command, you would first set up a subscription to receive the response. By using GraphQL, subscription responses are type-safe, much like return values of a Query or Mutation request.
The client calls GraphQL Mutations to send requests via HTTP, then receives responses (if any) via web socket. Though simple to implement on the server, a socket-based interface is awkward for the client because it is multi-stepped:
- subscribe to the expected response event
- send a command via HTTP
- receive an HTTP response (an acknowledgment that the request was received, not the actual result)
- await the real response to arrive via the web socket.
- act on the response
Simplifying the client-server interaction
Let’s categorize the types of responses UCI sends:
- single line response
- no response
- multi-line, multi-value response, with terminator
(Aside: It is possible to start analysis without a definite time limit (“infinite go”). This would fall under category 2 because analysis will arrive at a best move termination point, either by exhaustion or by the stop command.)
Category 1 is simple call and response, and these can be handled as plain old GraphQL HTTP requests. No need to subscribe for a response: the resolver can just return it when it arrives.
Category 2 receives no response from the engine, but a response is required by HTTP. All that is needed in this case is to acknowledge the request.
Category 3 has two subtypes: requests with multi-line but fixed responses (e.g. option), and requests with streaming, intermediate responses (go). The former can again be handled through HTTP because the response will be predictable and timely. The latter has a varying (possibly long) completion time, and may be sending a series of intermediate responses of interest to the client, which it would like to receive in real time. Since we can’t send back multiple responses to an HTTP request, this latter case cannot be handled by HTTP alone, so the subscription interface as described above is still appropriate.
Despite UCI being a streaming interface, it turns out that for most cases, an HTTP response/request can be used for interaction via GraphQL.
- The GraphQL schema should consist of Mutations because UCI is stateful and commands must execute in sequence
- For Category 1 & 2 commands, HTTP request/response is simplest. There is still streaming going on in the back end, but GraphQL resolvers will instantiate a UCI stream listener specific to the expected UCI command response before sending the command to the engine. That listener will resolve the GraphQL request via HTTP when the response arrives from the engine. This makes lighter work for the client.
- The server will also track UCI state to ensure that commands are executed in the proper context. If the client tries to execute a command before the engine can handle it, an HTTP status error will be returned
- For those cases where there is no expected response from UCI, the GraphQL resolver will just acknowledge the command was received.
- The determinate case for Category 3 (where there’s a sure and quick response) can be handled by HTTP.
- The indeterminate case, where there are intermediate responses before termination, can be handled via web socket. This, in turn, can be wrapped in a GraphpQL subscription service.
The mock implementation pretty much covered the essentials, but this short analysis provides a blueprint for going forward with an implementation.
Code for this article can be found here.