Skip to content

Commit

Permalink
Spec: Formal Subscriptions Definition (graphql#305)
Browse files Browse the repository at this point in the history
* Subscription validation rule: single root field

* typo in explanatory text

* Updated wording to match Execution section

* Execution section for subscriptions

* Remove leftover section on Subscription functions

* Incorporate feedback from first review

* Line feed after subscription sections

* Fix spec syntax

* Address wincent's feedback

* Modify wording from Laney's feedback

* Address Sashko's feedback

* Fix inline code snippets

* Address Lee's feedback

* Address second round of feedback w/ Lee

* Address third round of feedback from Lee

* Add subscriptions to other sections

* Address more feedback on Execution section

* Remove extra single root field check and reword subscriptions description

* and/or

* Minor editing updates

* Mini-header for event streams
  • Loading branch information
robzhu authored and IvanGoncharov committed Jun 17, 2017
1 parent 6d0b83e commit 3c5a84a
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 11 deletions.
2 changes: 1 addition & 1 deletion spec/Appendix B -- Grammar Summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ OperationDefinition :
- SelectionSet
- OperationType Name? VariableDefinitions? Directives? SelectionSet

OperationType : one of query mutation
OperationType : one of query mutation subscription

SelectionSet : { Selection+ }

Expand Down
8 changes: 5 additions & 3 deletions spec/Section 2 -- Language.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Clients use the GraphQL query language to make requests to a GraphQL service.
We refer to these request sources as documents. A document may contain
operations (queries and mutations are both operations) as well as fragments, a
operations (queries, mutations, and subscriptions) as well as fragments, a
common unit of composition allowing for query reuse.

A GraphQL document is defined as a syntactic grammar where terminal symbols are
Expand Down Expand Up @@ -198,12 +198,14 @@ OperationDefinition :
- OperationType Name? VariableDefinitions? Directives? SelectionSet
- SelectionSet

OperationType : one of `query` `mutation`
OperationType : one of `query` `mutation` `subscription`

There are two types of operations that GraphQL models:
There are three types of operations that GraphQL models:

* query - a read-only fetch.
* mutation - a write followed by a fetch.
* subscription - a long-lived request that fetches data in response to source
events.

Each operation is represented by an optional operation name and a selection set.

Expand Down
23 changes: 18 additions & 5 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ A given GraphQL schema must itself be internally valid. This section describes
the rules for this validation process where relevant.

A GraphQL schema is represented by a root type for each kind of operation:
query and mutation; this determines the place in the type system where those
operations begin.
query, mutation, and subscription; this determines the place in the type system
where those operations begin.

All types within a GraphQL schema must have unique names. No two provided types
may have the same name. No provided type may have a name which conflicts with
Expand Down Expand Up @@ -1048,12 +1048,14 @@ type ExampleType {

## Initial types

A GraphQL schema includes types, indicating where query and mutation
operations start. This provides the initial entry points into the
A GraphQL schema includes types, indicating where query, mutation, and
subscription operations start. This provides the initial entry points into the
type system. The query type must always be provided, and is an Object
base type. The mutation type is optional; if it is null, that means
the system does not support mutations. If it is provided, it must
be an object base type.
be an object base type. Similarly, the subscription type is optional; if it is
null, the system does not support subscriptions. If it is provided, it must be
an object base type.

The fields on the query type indicate what fields are available at
the top level of a GraphQL query. For example, a basic GraphQL query
Expand All @@ -1078,3 +1080,14 @@ mutation setName {

Is valid when the type provided for the mutation starting type is not null,
and has a field named "setName" with a string argument named "name".

```graphql
subscription {
newMessage {
text
}
}
```

Is valid when the type provided for the subscription starting type is not null,
and has a field named "newMessage" and only contains a single root field.
2 changes: 2 additions & 0 deletions spec/Section 4 -- Introspection.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ type __Schema {
types: [__Type!]!
queryType: __Type!
mutationType: __Type
subscriptionType: __Type
directives: [__Directive!]!
}
Expand Down Expand Up @@ -195,6 +196,7 @@ type __Directive {
enum __DirectiveLocation {
QUERY
MUTATION
SUBSCRIPTION
FIELD
FRAGMENT_DEFINITION
FRAGMENT_SPREAD
Expand Down
61 changes: 61 additions & 0 deletions spec/Section 5 -- Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,67 @@ query getName {
}
```

### Subscription Operation Definitions

#### Single root field

**Formal Specification**

* For each subscription operation definition {subscription} in the document
* Let {rootFields} be the top level selection set on {subscription}.
* {rootFields} must be a set of one.

**Explanatory Text**

Subscription operations must have exactly one root field.

Valid examples:

```graphql
subscription sub {
newMessage {
body
sender
}
}
```

```graphql
fragment newMessageFields on Message {
body
sender
}

subscription sub {
newMessage {
... newMessageFields
}
}
```

Invalid:

```!graphql
subscription sub {
newMessage {
body
sender
}
disallowedSecondRootField
}
```

Introspection fields are counted. The following example is also invalid:

```!graphql
subscription sub {
newMessage {
body
sender
}
__typename
}
```

## Fields

Expand Down
147 changes: 145 additions & 2 deletions spec/Section 6 -- Execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ ExecuteRequest(schema, document, operationName, variableValues, initialValue):
* Return {ExecuteQuery(operation, schema, coercedVariableValues, initialValue)}.
* Otherwise if {operation} is a mutation operation:
* Return {ExecuteMutation(operation, schema, coercedVariableValues, initialValue)}.
* Otherwise if {operation} is a subscription operation:
* Return {Subscribe(operation, schema, coercedVariableValues, initialValue)}.

GetOperation(document, operationName):

Expand Down Expand Up @@ -103,8 +105,10 @@ Note: This algorithm is very similar to {CoerceArgumentValues()}.
## Executing Operations

The type system, as described in the “Type System” section of the spec, must
provide a query root object type. If mutations are supported, it must also
provide a mutation root object type.
provide a query root object type. If mutations or subscriptions are supported,
it must also provide a mutation or subscription root object type, respectively.

### Query

If the operation is a query, the result of the operation is the result of
executing the query’s top level selection set with the query root object type.
Expand All @@ -123,6 +127,8 @@ ExecuteQuery(query, schema, variableValues, initialValue):
selection set.
* Return an unordered map containing {data} and {errors}.

### Mutation

If the operation is a mutation, the result of the operation is the result of
executing the mutation’s top level selection set on the mutation root
object type. This selection set should be executed serially.
Expand All @@ -143,6 +149,143 @@ ExecuteMutation(mutation, schema, variableValues, initialValue):
selection set.
* Return an unordered map containing {data} and {errors}.

### Subscription

If the operation is a subscription, the result is an event stream called the
"Response Stream" where each event in the event stream is the result of
executing the operation for each new event on an underlying "Source Stream".

Executing a subscription creates a persistent function on the server that
maps an underlying Source Stream to a returned Response Stream.

Subscribe(subscription, schema, variableValues, initialValue):

* Let {sourceStream} be the result of running {CreateSourceEventStream(subscription, schema, variableValues, initialValue)}.
* Let {responseStream} be the result of running {MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues)}
* Return {responseStream}.

Note: In large scale subscription systems, the {Subscribe} and {ExecuteSubscriptionEvent}
algorithms may be run on separate services to maintain predictable scaling
properties. See the section below on Supporting Subscriptions at Scale.

**Event Streams**

An event stream represents a sequence of discrete events over time which can be
observed. As an example, a "Pub-Sub" system may produce an event stream when
"subscribing to a topic", with an event occurring on that event stream for each
"publish" to that topic. Event streams may produce an infinite sequence of
events or may complete at any point. Event streams may complete in response to
an error or simply because no more events will occur. An observer may at any
point decide to stop observing an event stream by cancelling it, after which it
must receive no more events from that event stream.

As an example, consider a chat application. To subscribe to new messages posted
to the chat room, the client sends a request like so:

```graphql
subscription NewMessages {
newMessage(roomId: 123) {
sender
text
}
}
```

While the client is subscribed, whenever new messages are posted to chat room
with ID "123", the selection for "sender" and "text" will be evaluated and
published to the client, for example:

```js
{
"data": {
"newMessage": {
"sender": "Hagrid",
"text": "You're a wizard!"
}
}
}
```

The "new message posted to chat room" could use a "Pub-Sub" system where the
chat room ID is the "topic" and each "publish" contains the sender and text.

**Supporting Subscriptions at Scale**

Supporting subscriptions is a significant change for any GraphQL server. Query
and mutation operations are stateless, allowing scaling via cloning of GraphQL
server instances. Subscriptions, by contrast, are stateful and require
maintaining the GraphQL document, variables, and other context over the lifetime
of the subscription.

Consider the behavior of your system when state is lost due to the failure of a
single machine in a service. Durability and availability may be improved by
having separate dedicated services for managing subscription state and client
connectivity.

#### Source Stream

A Source Stream represents the sequence of events, each of which will
trigger a GraphQL execution corresponding to that event. Like field value
resolution, the logic to create a Source Stream is application-specific.

CreateSourceEventStream(subscription, schema, variableValues, initialValue):

* Let {subscriptionType} be the root Subscription type in {schema}.
* Assert: {subscriptionType} is an Object type.
* Let {selectionSet} be the top level Selection Set in {subscription}.
* Let {rootField} be the first top level field in {selectionSet}.
* Let {argumentValues} be the result of {CoerceArgumentValues(subscriptionType, rootField, variableValues)}.
* Let {fieldStream} be the result of running {ResolveFieldEventStream(subscriptionType, initialValue, rootField, argumentValues)}.
* Return {fieldStream}.

ResolveFieldEventStream(subscriptionType, rootValue, fieldName, argumentValues):
* Let {resolver} be the internal function provided by {subscriptionType} for
determining the resolved event stream of a subscription field named {fieldName}.
* Return the result of calling {resolver}, providing {rootValue} and {argumentValues}.

Note: This {ResolveFieldEventStream} algorithm is intentionally similar
to {ResolveFieldValue} to enable consistency when defining resolvers
on any operation type.

#### Response Stream

Each event in the underlying Source Stream triggers execution of the subscription
selection set using that event as a root value.

MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues):

* Return a new event stream {responseStream} which yields events as follows:
* For each {event} on {sourceStream}:
* Let {response} be the result of running
{ExecuteSubscriptionEvent(subscription, schema, variableValues, event)}.
* Yield an event containing {response}.
* When {responseStream} completes: complete this event stream.

ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue):

* Let {subscriptionType} be the root Subscription type in {schema}.
* Assert: {subscriptionType} is an Object type.
* Let {selectionSet} be the top level Selection Set in {subscription}.
* Let {data} be the result of running
{ExecuteSelectionSet(selectionSet, subscriptionType, initialValue, variableValues)}
*normally* (allowing parallelization).
* Let {errors} be any *field errors* produced while executing the
selection set.
* Return an unordered map containing {data} and {errors}.

Note: The {ExecuteSubscriptionEvent} algorithm is intentionally similar to
{ExecuteQuery} since this is how the each event result is produced.

#### Unsubscribe

Unsubscribe cancels the Response Stream when a client no longer wishes to receive
payloads for a subscription. This may in turn also cancel the Source Stream.
This is also a good opportunity to clean up any other resources used by
the subscription.

Unsubscribe(responseStream)

* Cancel {responseStream}

## Executing Selection Sets

Expand Down

0 comments on commit 3c5a84a

Please sign in to comment.