Skip to content

Commit d7e3f06

Browse files
robzhuleebyron
authored andcommitted
Spec: Formal Subscriptions Definition (#305)
* 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
1 parent 7dc6ee7 commit d7e3f06

6 files changed

+232
-11
lines changed

spec/Appendix B -- Grammar Summary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ OperationDefinition :
9494
- SelectionSet
9595
- OperationType Name? VariableDefinitions? Directives? SelectionSet
9696

97-
OperationType : one of query mutation
97+
OperationType : one of query mutation subscription
9898

9999
SelectionSet : { Selection+ }
100100

spec/Section 2 -- Language.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

88
A GraphQL document is defined as a syntactic grammar where terminal symbols are
@@ -193,12 +193,14 @@ OperationDefinition :
193193
- OperationType Name? VariableDefinitions? Directives? SelectionSet
194194
- SelectionSet
195195

196-
OperationType : one of `query` `mutation`
196+
OperationType : one of `query` `mutation` `subscription`
197197

198-
There are two types of operations that GraphQL models:
198+
There are three types of operations that GraphQL models:
199199

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

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

spec/Section 3 -- Type System.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ A given GraphQL schema must itself be internally valid. This section describes
1212
the rules for this validation process where relevant.
1313

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

1818
All types within a GraphQL schema must have unique names. No two provided types
1919
may have the same name. No provided type may have a name which conflicts with
@@ -1013,12 +1013,14 @@ must *not* be queried if either the `@skip` condition is true *or* the
10131013

10141014
## Initial types
10151015

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

10231025
The fields on the query type indicate what fields are available at
10241026
the top level of a GraphQL query. For example, a basic GraphQL query
@@ -1043,3 +1045,14 @@ mutation setName {
10431045

10441046
Is valid when the type provided for the mutation starting type is not null,
10451047
and has a field named "setName" with a string argument named "name".
1048+
1049+
```graphql
1050+
subscription {
1051+
newMessage {
1052+
text
1053+
}
1054+
}
1055+
```
1056+
1057+
Is valid when the type provided for the subscription starting type is not null,
1058+
and has a field named "newMessage" and only contains a single root field.

spec/Section 4 -- Introspection.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ type __Schema {
124124
types: [__Type!]!
125125
queryType: __Type!
126126
mutationType: __Type
127+
subscriptionType: __Type
127128
directives: [__Directive!]!
128129
}
129130
@@ -195,6 +196,7 @@ type __Directive {
195196
enum __DirectiveLocation {
196197
QUERY
197198
MUTATION
199+
SUBSCRIPTION
198200
FIELD
199201
FRAGMENT_DEFINITION
200202
FRAGMENT_SPREAD

spec/Section 5 -- Validation.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,67 @@ query getName {
198198
}
199199
```
200200

201+
### Subscription Operation Definitions
202+
203+
#### Single root field
204+
205+
**Formal Specification**
206+
207+
* For each subscription operation definition {subscription} in the document
208+
* Let {rootFields} be the top level selection set on {subscription}.
209+
* {rootFields} must be a set of one.
210+
211+
**Explanatory Text**
212+
213+
Subscription operations must have exactly one root field.
214+
215+
Valid examples:
216+
217+
```graphql
218+
subscription sub {
219+
newMessage {
220+
body
221+
sender
222+
}
223+
}
224+
```
225+
226+
```graphql
227+
fragment newMessageFields on Message {
228+
body
229+
sender
230+
}
231+
232+
subscription sub {
233+
newMessage {
234+
... newMessageFields
235+
}
236+
}
237+
```
238+
239+
Invalid:
240+
241+
```!graphql
242+
subscription sub {
243+
newMessage {
244+
body
245+
sender
246+
}
247+
disallowedSecondRootField
248+
}
249+
```
250+
251+
Introspection fields are counted. The following example is also invalid:
252+
253+
```!graphql
254+
subscription sub {
255+
newMessage {
256+
body
257+
sender
258+
}
259+
__typename
260+
}
261+
```
201262

202263
## Fields
203264

spec/Section 6 -- Execution.md

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ ExecuteRequest(schema, document, operationName, variableValues, initialValue):
3434
* Return {ExecuteQuery(operation, schema, coercedVariableValues, initialValue)}.
3535
* Otherwise if {operation} is a mutation operation:
3636
* Return {ExecuteMutation(operation, schema, coercedVariableValues, initialValue)}.
37+
* Otherwise if {operation} is a subscription operation:
38+
* Return {Subscribe(operation, schema, coercedVariableValues, initialValue)}.
3739

3840
GetOperation(document, operationName):
3941

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

105107
The type system, as described in the “Type System” section of the spec, must
106-
provide a query root object type. If mutations are supported, it must also
107-
provide a mutation root object type.
108+
provide a query root object type. If mutations or subscriptions are supported,
109+
it must also provide a mutation or subscription root object type, respectively.
110+
111+
### Query
108112

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

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

152+
### Subscription
153+
154+
If the operation is a subscription, the result is an event stream called the
155+
"Response Stream" where each event in the event stream is the result of
156+
executing the operation for each new event on an underlying "Source Stream".
157+
158+
Executing a subscription creates a persistent function on the server that
159+
maps an underlying Source Stream to a returned Response Stream.
160+
161+
Subscribe(subscription, schema, variableValues, initialValue):
162+
163+
* Let {sourceStream} be the result of running {CreateSourceEventStream(subscription, schema, variableValues, initialValue)}.
164+
* Let {responseStream} be the result of running {MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues)}
165+
* Return {responseStream}.
166+
167+
Note: In large scale subscription systems, the {Subscribe} and {ExecuteSubscriptionEvent}
168+
algorithms may be run on separate services to maintain predictable scaling
169+
properties. See the section below on Supporting Subscriptions at Scale.
170+
171+
**Event Streams**
172+
173+
An event stream represents a sequence of discrete events over time which can be
174+
observed. As an example, a "Pub-Sub" system may produce an event stream when
175+
"subscribing to a topic", with an event occurring on that event stream for each
176+
"publish" to that topic. Event streams may produce an infinite sequence of
177+
events or may complete at any point. Event streams may complete in response to
178+
an error or simply because no more events will occur. An observer may at any
179+
point decide to stop observing an event stream by cancelling it, after which it
180+
must receive no more events from that event stream.
181+
182+
As an example, consider a chat application. To subscribe to new messages posted
183+
to the chat room, the client sends a request like so:
184+
185+
```graphql
186+
subscription NewMessages {
187+
newMessage(roomId: 123) {
188+
sender
189+
text
190+
}
191+
}
192+
```
193+
194+
While the client is subscribed, whenever new messages are posted to chat room
195+
with ID "123", the selection for "sender" and "text" will be evaluated and
196+
published to the client, for example:
197+
198+
```js
199+
{
200+
"data": {
201+
"newMessage": {
202+
"sender": "Hagrid",
203+
"text": "You're a wizard!"
204+
}
205+
}
206+
}
207+
```
208+
209+
The "new message posted to chat room" could use a "Pub-Sub" system where the
210+
chat room ID is the "topic" and each "publish" contains the sender and text.
211+
212+
**Supporting Subscriptions at Scale**
213+
214+
Supporting subscriptions is a significant change for any GraphQL server. Query
215+
and mutation operations are stateless, allowing scaling via cloning of GraphQL
216+
server instances. Subscriptions, by contrast, are stateful and require
217+
maintaining the GraphQL document, variables, and other context over the lifetime
218+
of the subscription.
219+
220+
Consider the behavior of your system when state is lost due to the failure of a
221+
single machine in a service. Durability and availability may be improved by
222+
having separate dedicated services for managing subscription state and client
223+
connectivity.
224+
225+
#### Source Stream
226+
227+
A Source Stream represents the sequence of events, each of which will
228+
trigger a GraphQL execution corresponding to that event. Like field value
229+
resolution, the logic to create a Source Stream is application-specific.
230+
231+
CreateSourceEventStream(subscription, schema, variableValues, initialValue):
232+
233+
* Let {subscriptionType} be the root Subscription type in {schema}.
234+
* Assert: {subscriptionType} is an Object type.
235+
* Let {selectionSet} be the top level Selection Set in {subscription}.
236+
* Let {rootField} be the first top level field in {selectionSet}.
237+
* Let {argumentValues} be the result of {CoerceArgumentValues(subscriptionType, rootField, variableValues)}.
238+
* Let {fieldStream} be the result of running {ResolveFieldEventStream(subscriptionType, initialValue, rootField, argumentValues)}.
239+
* Return {fieldStream}.
240+
241+
ResolveFieldEventStream(subscriptionType, rootValue, fieldName, argumentValues):
242+
* Let {resolver} be the internal function provided by {subscriptionType} for
243+
determining the resolved event stream of a subscription field named {fieldName}.
244+
* Return the result of calling {resolver}, providing {rootValue} and {argumentValues}.
245+
246+
Note: This {ResolveFieldEventStream} algorithm is intentionally similar
247+
to {ResolveFieldValue} to enable consistency when defining resolvers
248+
on any operation type.
249+
250+
#### Response Stream
251+
252+
Each event in the underlying Source Stream triggers execution of the subscription
253+
selection set using that event as a root value.
254+
255+
MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues):
256+
257+
* Return a new event stream {responseStream} which yields events as follows:
258+
* For each {event} on {sourceStream}:
259+
* Let {response} be the result of running
260+
{ExecuteSubscriptionEvent(subscription, schema, variableValues, event)}.
261+
* Yield an event containing {response}.
262+
* When {responseStream} completes: complete this event stream.
263+
264+
ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue):
265+
266+
* Let {subscriptionType} be the root Subscription type in {schema}.
267+
* Assert: {subscriptionType} is an Object type.
268+
* Let {selectionSet} be the top level Selection Set in {subscription}.
269+
* Let {data} be the result of running
270+
{ExecuteSelectionSet(selectionSet, subscriptionType, initialValue, variableValues)}
271+
*normally* (allowing parallelization).
272+
* Let {errors} be any *field errors* produced while executing the
273+
selection set.
274+
* Return an unordered map containing {data} and {errors}.
275+
276+
Note: The {ExecuteSubscriptionEvent} algorithm is intentionally similar to
277+
{ExecuteQuery} since this is how the each event result is produced.
278+
279+
#### Unsubscribe
280+
281+
Unsubscribe cancels the Response Stream when a client no longer wishes to receive
282+
payloads for a subscription. This may in turn also cancel the Source Stream.
283+
This is also a good opportunity to clean up any other resources used by
284+
the subscription.
285+
286+
Unsubscribe(responseStream)
287+
288+
* Cancel {responseStream}
146289

147290
## Executing Selection Sets
148291

0 commit comments

Comments
 (0)