diff --git a/spec/Appendix B -- Grammar Summary.md b/spec/Appendix B -- Grammar Summary.md index b1a79f703..d3b59f131 100644 --- a/spec/Appendix B -- Grammar Summary.md +++ b/spec/Appendix B -- Grammar Summary.md @@ -94,7 +94,7 @@ OperationDefinition : - SelectionSet - OperationType Name? VariableDefinitions? Directives? SelectionSet -OperationType : one of query mutation +OperationType : one of query mutation subscription SelectionSet : { Selection+ } diff --git a/spec/Section 2 -- Language.md b/spec/Section 2 -- Language.md index 6e24c7950..15b95849a 100644 --- a/spec/Section 2 -- Language.md +++ b/spec/Section 2 -- Language.md @@ -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 @@ -193,12 +193,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. diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 49d761bd7..6941e20d8 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -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 @@ -1013,12 +1013,14 @@ must *not* be queried if either the `@skip` condition is true *or* the ## 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 @@ -1043,3 +1045,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. diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index e6a453eaa..52d73a9b8 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -124,6 +124,7 @@ type __Schema { types: [__Type!]! queryType: __Type! mutationType: __Type + subscriptionType: __Type directives: [__Directive!]! } @@ -195,6 +196,7 @@ type __Directive { enum __DirectiveLocation { QUERY MUTATION + SUBSCRIPTION FIELD FRAGMENT_DEFINITION FRAGMENT_SPREAD diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index df923cbab..5077d35b4 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -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 diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index ce5cd4121..6a34046a7 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -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): @@ -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. @@ -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. @@ -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