Skip to content

Commit 3c5a84a

Browse files
robzhuIvanGoncharov
authored andcommitted
Spec: Formal Subscriptions Definition (graphql#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 6d0b83e commit 3c5a84a

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
@@ -95,7 +95,7 @@ OperationDefinition :
9595
- SelectionSet
9696
- OperationType Name? VariableDefinitions? Directives? SelectionSet
9797

98-
OperationType : one of query mutation
98+
OperationType : one of query mutation subscription
9999

100100
SelectionSet : { Selection+ }
101101

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
@@ -198,12 +198,14 @@ OperationDefinition :
198198
- OperationType Name? VariableDefinitions? Directives? SelectionSet
199199
- SelectionSet
200200

201-
OperationType : one of `query` `mutation`
201+
OperationType : one of `query` `mutation` `subscription`
202202

203-
There are two types of operations that GraphQL models:
203+
There are three types of operations that GraphQL models:
204204

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

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

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
@@ -1048,12 +1048,14 @@ type ExampleType {
10481048

10491049
## Initial types
10501050

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

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

10791081
Is valid when the type provided for the mutation starting type is not null,
10801082
and has a field named "setName" with a string argument named "name".
1083+
1084+
```graphql
1085+
subscription {
1086+
newMessage {
1087+
text
1088+
}
1089+
}
1090+
```
1091+
1092+
Is valid when the type provided for the subscription starting type is not null,
1093+
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)