Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Kiollpt/6c75fb16f7cedf464850ac4970efd79e to your computer and use it in GitHub Desktop.
Save Kiollpt/6c75fb16f7cedf464850ac4970efd79e to your computer and use it in GitHub Desktop.
Event-stream based GraphQL subscriptions for real-time updates

In this gist I would like to describe an idea for GraphQL subscriptions. It was inspired by conversations about subscriptions in the GraphQL slack channel and different GH issues, like #89 and #411.

Conceptual Model

At the moment GraphQL allows 2 types of queries:

  • query
  • mutation

Reference implementation also adds the third type: subscription. It does not have any semantics yet, so here I would like to propose one possible semantics interpretation and the reasoning behind it.

The fact that query and mutation are two separate GraphQL types gives us a hint, that a read model and a write model are separated and don't necessarily have the same representation. It closely resembles CQRS (Command Query Responsibility Segregation) pattern, where writes and reads are separated and have different model behind them:

Separation between read and wripe model

Even though GraphQL is backend-agnostic, so the database on this diagram is just to make an example more concrete (in general it can be anything, for example an SQL Database, NoSQL Database, REST service, in-memory store, etc.). The "Write Model" in the diagram is a GraphQL MutationType and the "Query Model" is a QueryType both of which we are provided to a schema and serve as a main entry point for queries and mutations.

Mutation field can be seen as some kind of a "command" to perform some side-effect and the return updated data which is relevant for this mutation. That's why it has a special semantics: all mutation fields are executed strictly sequentially. I believe that subscriptions will need a special semantics as well, but it would be different from query and mutation.

There is also another useful pattern that is often used in combination with read-write model separation: event-sourcing. It generally says that a result of "command" is a list of events which can be saved and then used to create a query model(s) (there is a very loose coupling between write and read model, so it can even happen asynchronously):

Event-sourcing

The "Event Store" part is not really relevant for GraphQL, but event-sourcing approach provides us with the "Event Model" which can be defined by user in addition to the query and mutation models. It can be called, for instance, a SubscriptionType and can be provided to a schema together with QueryType and MutationType.

Event-stream based subscriptions

This approach can provide a robust and very scalable foundation for subscriptions in GraphQL.

Execution Semantics

Every field in SybscriptionType represents an event stream. This means, that the result of resolve should be some kind of iterable or observable sequence of events in contrast to a single value or Promise/Future that query and mutation types expect. This can be an Iterable (sync) or an Observable (async). Server implementation may support particular libraries like reactivex, but as far as GraphQL spec is concerned, every subscription field will just emits values of provided GraphQL type to this observable sequence as long as subscription is active and there is some data to emit.

This approach has nice compositional property: all subscription field results can be composed together in one data stream. Synchronous iterable sequences can be just concatenated together. Async observable sequences can be merged with operation like merge (in case or reactivex family of libraries). So the result of GraphQL query will be a merged sequence of all subscription field sequences.

Here is an example that demonstrated this. First let's define a schema:

type Droid {
  id: String!
  name: String
  friends: [Droid]
}

type QueryType {
  droids: [Droid]
  droid(id: String!): Droid
}

interface DroidEvent {
  eventId: Int
}

type NameChanged implements DroidEvent {
  eventId: Int
  droid: Droid!
  oldName: String
  newName: String
}

type FriendAdded implements DroidEvent {
  eventId: Int
  droid: Droid!
  friend: Droid!
}

type SubscriptionType {
  droidEvents(lastSeenEventId: Int): DroidEvent!
}

type MutationType {
  changeName(id: String!, newName: String): Droid
  addFriend(id: String!, friendId: String!): Droid
}

A query can look like this:

subscription MyDroidEvents {
  droidEvents(lastSeenEventId: 5) {
    tpe: __typename
    eventId

    ... on NameChanged {
      oldName
      newName
    }
  }
}

The GraphQL is agnostic to the network protocol. So user has a lot of flexibility in terms of how to expose the event stream produced by the query execution. Given an Observable, user can stream events to a WebSocket, provide a server-sent events endpoint, etc. In case of server-sent events endpoint the response can look like this:

POST /graphql
...
HTTP/1.1 200 OK
Content-Type: text/event-stream
...

id: 6
data: {"data": {"droidEvents": {"tpe": "NameChanged", "eventId": 6, "oldName": "foo", "newName": "bar"}}}

id: 7
data: {"data": {"droidEvents": {"tpe": "FriendAdded", "eventId": 7}}}

id: 8
data: {"data": {"droidEvents": {"tpe": "NameChanged", "eventId": 8, "oldName": "bar", "newName": "baz"}}}

...

As you probably noticed, droidEvents is also part of the response - it was added as a result of merge. This allows client to distinguish between events coming from different streams.

Subscription field can have arguments and aliases just like any other field. This opens up a lot of opportunities. Users can, for instance, filter event stream or provide a lastSeenEventId like I have shown in the example. An event stream can come from different sources and not all of them support features like lastSeenEventId, so it is important that GraphQL spec does not require or even know about these concepts.

Subscriptions without explicit events

Even though this approach is described in terms of patterns like CQRS and event-sourcing, they are only used as a helper to reason about the described model. Event Model is used just to demonstrate this approach, which means that subscription field results as well as their meaning are completely user-defined. It can be some kind of "event", but it also can be the same type used in a query. For instance we can define the SubscriptionType like this:

type SubscriptionType {
  droid: Droid!
}

Given a query:

subscription MyDroidEvents {
  droid {
    name
    friends
  }
}

user can write logic that analyses nested fields in a resolve function of the droid field. It can then emit a Droid object every time name or friends field is changed because of some other mutation query or maybe some external change.

Conclusion

Described approach provides pretty minimal semantics for subscription queries. I hope that it would be helpful (at least some parts of it) for the design of GraphQL subscription mechanism.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment