haskell-graphql / graphql-api

Write type-safe GraphQL services in Haskell
BSD 3-Clause "New" or "Revised" License
406 stars 35 forks source link

Thinking about Subscriptions #203

Open RichardWarfield opened 6 years ago

RichardWarfield commented 6 years ago

I'd like to help with adding GraphQL subscriptions support to this package. I have started porting the subscriptions-transport-ws package for subscriptions over WebSockets to Haskell here. There's a sample application and GraphiQL frontend for testing.

The below discussion is about the issues encountered in creating an implementation of subscriptions with graphql-api. TL;DR: I can't quite follow the spec on subscription execution due to the lack of an initialValue concept in graphql-api resolvers/handlers. I've implemented a workaround and would like to get feedback from the graphql-api authors/community.


For graphql-api to support subscriptions in terms of syntax and validation, the changes are pretty trivial and obvious (see my fork).

Execution is tricker. Unlike for queries or mutations, the section in the spec on executing subscriptions is pretty long and detailed. For reasons explained below, I don't believe it is possible to follow the spec's execution methodology closely in graphql-api (at least without significant and annoying changes).

To summarize my understanding of the spec (and simplifying greatly), a subscription should be executed as follows:

  1. A stream of "source events" is created using the resolver for the subscription type in the schema
  2. Each source event triggers an execution of the subscription selection set using the source event as a root value, using the function called (in the spec) [ExecuteSelectionSet](http://facebook.github.io/graphql/June2018/#ExecuteSelectionSet())

In my example app, the source stream is just a clock that pushes the current time to the stream each second. Under the spec's methodolgy, each timestamp would get passed to ExecuteSelectionSet as the initial value, which in turn calls the field resolvers which also receive the initial value.

In graphql-api there isn't the concept of an initial value passed to resolvers/handlers. I suppose this could be added but it would break a lot of stuff and be ugly.

The workaround I've used in the sample app is, instead of passing the source event as an initialValue (which doesn't exist in graphql-api), I instead pass it as an Argument. So my root schema looks like:

type GQLRoot = Object "GQLRoot" '[]
  '[
  Argument "sourceEvent" Int32 :> Field "now" Now
   ]

Then when a user issues a subscription subscription sub { now { ... } }, it is converted into a query like `{ now(sourceEvent: ) {...} }, which gets executed once for each source event (i.e. the time stamp).

I'm not sure whether this is the best approach to take for executing subscriptions. I can think of several issues off the top of my head The extra argument is ugly, and end users might see the "wrong" schema -- though some type synonyms / newtypes and simple schema transformations could fix these problems.

theobat commented 5 years ago

I'm quite interested in that too, I would really like to add initial values in graphql-api because when linking efficiently grahpql objects/fields with sql couterparts, we need that (c.f. #182 for more precision).

In my opnion initialValueis part of the spec of resolvers, so if we want to follow the spec, we should add that, or at least some sort of context object which is injected by a father node to its children.

As much as I like this argument workaround, I'm not sure how it extends to all the use cases of initial values in the resolvers. Besides, this workaround push the implementation farther from the spec, which I don't think is the right approach...