Adding @defer and @stream to Absinthe

So I’ve got some use-cases that would be made much simpler if Absinthe supported the (not yet standardised) @defer and @stream directives (see https://dev-blog.apollodata.com/new-features-in-graphql-batch-defer-stream-live-and-subscribe-7585d0c28b07). I’m happy to take a stab at adding these myself, but it would be helpful to have at least some pointers in the right direction so that I know I’m not going off on a wild tangent :slight_smile:
@benwilson512 suggested looking at the following items first:

what I’d suggest is doing some research on the other frameworks and what they’re doing in terms of:

  1. what does the iniial client response look like?
  2. How are updates shaped?
  3. what kind of API exists on the server side for pushing changes?

So after a lot of digging through other public graphql implementations, the answer to what they’re doing is basically “they’re not” :slight_smile: The only implementation I could find anywhere was in graphql-java (GitHub - graphql-java/graphql-java: GraphQL Java implementation) and implemented only @defer, and then only with the word “experimental” plastered in big red letters all over it (though, you know, fair enough). Moreover, graphql-java leaves the transport layer (websockets etc) entirely as an exercise for the reader - so it’s no help with that aspect. In fact, it even leaves the final encoding up to the user - the closest the library gets is outputting a Map that should be easy to encode. Still, let’s work with what we’ve got.

  1. what does the iniial client response look like?

The initial client request returns the standard response, excluding any fields tagged with @defer. These are excluded entirely. For a field whose sub-fields are entirely @defer tagged, this means an empty field set:

query {
  post {
    author @defer
    comments @defer {
      text
    }
  }
}

Initially yields:

{"data": {"post" {}}}

(This, of course, supposes that the JSON encoder selected will encode an empty map as {} and not null or something else, but it seems like a reasonable assumption.)

  1. How are updates shaped?

Again, because the library stops at the “here’s the result as a map” level, the answer is really “they’re not”. In fact as far as I can tell, the function that would deal with adding things like the path section (ExecutionResultImpl.toSpecification()) hasn’t been updated to deal specifically with @defer at all, so there’s no existing answer for this in any of the publicly available libraries.

The presentation linked at the top, though, gives a reasonable example that would make the updates for the example query above look like this:

{
  "path": ["post", "author"],
  "data": "Somebody"
}

and then

{
  "path": ["post", "comments"],
  "data": [{"text": "Hi, thanks for your post"}, {"text": "Good comment, thanks."}]
}

The example in the presentation also shows how to handle a @defer within a list field - I won’t bother repeating that here.

  1. what kind of API exists on the server side for pushing changes?

graphql-java relies on the implementer to explicitly check for any deferred fields and resolve and return them after returning the initial result. See DeferredExamples.java if you’re interested, but I’m really not a fan of that approach - the whole thing is also very verbose (as is Java’s wont) - I think we can probably do better.

I’m probably being wildly optimistic here, but I don’t think it should be too awful fitting this into Absinthe’s structure. The way I imagine it working is something like this:

  • @defer fields are tagged in whichever phase is appropriate (I haven’t dug through the phases in detail yet)
  • When we go to resolve such a tagged field, we spawn a new process and call the resolver in that, remembering it and tagging it with the path
  • Once the full non-deferred object has been resolved and returned, we start waiting for responses from the deferred resolvers. As these arrive, we add the path and send them on to the client.

There’s of course a bunch of details - like what do we do with a @defer field that is part of a larger, non-deferred object which is all handled by a single resolver? (My vote would be, initially at least, just ignore the @defer). And, like, a billion other things I haven’t thought of yet.

Then of course there’s @stream. It’s almost-but-not-quite a subset of @defer, but I think maybe starting with just @defer will be easiest and give us some good pointers for future similar directives.

5 Likes