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.
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:
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):
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
.
This approach can provide a robust and very scalable foundation for subscriptions in GraphQL.
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.
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.
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.
I just made a PoC for Isomorphic Projections work. Projections are used by the server to power queries from events, and on the client to power optimistic responses within Apollo client. The same code on the server and client (assuming Node.js on the backend).
I'll post a repo soon if there's interest