diff --git a/spec/Appendix E -- Examples.md b/spec/Appendix E -- Examples.md new file mode 100644 index 000000000..a2a1b4d2d --- /dev/null +++ b/spec/Appendix E -- Examples.md @@ -0,0 +1,219 @@ +# E. Appendix: Examples + +## Incremental Delivery Examples + +### Example 1 - A query containing both defer and stream + +```graphql example +query { + person(id: "cGVvcGxlOjE=") { + ...HomeWorldFragment @defer(label: "homeWorldDefer") + name + films @stream(initialCount: 1, label: "filmsStream") { + title + } + } +} +fragment HomeWorldFragment on Person { + homeWorld { + name + } +} +``` + +The response to this request will be an _incremental stream_ consisting of an +_initial incremental stream result_ followed by one or more _incremental stream +update result_. + +The _initial incremental stream result_ has: + +- a {"data"} entry containing the results of the GraphQL operation except for + the `@defer` and `@stream` selections; +- a {"pending"} entry containing two _incremental pending notices_, one for the + `@defer` selection and for the the `@stream` selection, indicating that these + results will be delivered in a later _incremental stream update result_; +- a {"hasNext"} entry with the value {true}, indicating that the response is not + yet complete. + +If an error were to occur, it would also have an {"errors"} entry; but not in +this example. + +```json example +{ + "data": { + "person": { + "name": "Luke Skywalker", + "films": [{ "title": "A New Hope" }] + } + }, + "pending": [ + { "id": "0", "path": ["person"], "label": "homeWorldDefer" }, + { "id": "1", "path": ["person", "films"], "label": "filmsStream" } + ], + "hasNext": true +} +``` + +Depending on the behavior of the backend and the time at which the deferred and +streamed resources resolve, the stream may produce results in different orders. +In this example, our first _incremental stream update result_ contains the +deferred data and the first streamed list item. There is one _incremental +completion notice_, indicating that the deferred data has been completely +delivered. + +```json example +{ + "incremental": [ + { + "id": "0", + "data": { "homeWorld": { "name": "Tatooine" } } + }, + { + "id": "1", + "items": [{ "title": "The Empire Strikes Back" }] + } + ], + "completed": [ + {"id": "0"} + ] + "hasNext": true +} +``` + +The second _incremental stream update result_ contains the final stream results. +In this example, the underlying iterator does not close synchronously so +{"hasNext"} is set to {true}. If this iterator did close synchronously, +{"hasNext"} could be set to {false} and make this the final incremental stream +update result. + +```json example +{ + "incremental": [ + { + "id": "1", + "items": [{ "title": "Return of the Jedi" }] + } + ], + "hasNext": true +} +``` + +When the underlying iterator of the `films` field closes there is no more data +to deliver, so the third and final _incremental stream update result_ sets +{"hasNext"} to {false} to indicate the end of the _incremental stream_. + +```json example +{ + "hasNext": false +} +``` + +### Example 2 - A query containing overlapping defers + +```graphql example +query { + person(id: "cGVvcGxlOjE=") { + ...HomeWorldFragment @defer(label: "homeWorldDefer") + ...NameAndHomeWorldFragment @defer(label: "nameAndWorld") + firstName + } +} +fragment HomeWorldFragment on Person { + homeWorld { + name + terrain + } +} + +fragment NameAndHomeWorldFragment on Person { + firstName + lastName + homeWorld { + name + } +} +``` + +In this example the response is an _incremental stream_ of the following +results. + +The _initial incremental stream result_ contains the results of the `firstName` +field. Even though it is also present in the `HomeWorldFragment`, it must be +returned in the initial incremental stream result because it is also defined +outside of any fragments with the `@defer` directive. Additionally, there are +two _incremental pending notices_ indicating that results for both `@defer`s in +the query will be delivered in later _incremental stream update result_. + +```json example +{ + "data": { + "person": { + "firstName": "Luke" + } + }, + "pending": [ + { "id": "0", "path": ["person"], "label": "homeWorldDefer" }, + { "id": "1", "path": ["person"], "label": "nameAndWorld" } + ], + "hasNext": true +} +``` + +In this example, the first _incremental stream update result_ contains the +deferred data from `HomeWorldFragment`. There is one _incremental completion +notice_, indicating that `HomeWorldFragment` has been completely delivered. +Because the `homeWorld` field is present in two separate `@defer`s, it is +separated into its own _incremental result_. In this example, this incremental +result contains the id `"0"`, but since the `name` field was included in both +`HomeWorldFragment` and `NameAndHomeWorldFragment`, an id of `"1"` would also be +a valid response. + +The second _incremental result_ in this _incremental stream update result_ +contains the data for the `terrain` field. This _incremental result_ contains a +{"subPath"} entry to indicate to clients that the _response position_ of this +result can be determined by concatenating: the path from the _incremental +pending notice_ for id `"0"`, and the value of this {"subPath"} entry. + +```json example +{ + "incremental": [ + { + "id": "0", + "data": { "homeWorld": { "name": "Tatooine" } } + }, + { + "id": "0", + "subPath": ["homeWorld"], + "data": { "terrain": "desert" } + } + ], + "completed": [{ "id": "0" }], + "hasNext": true +} +``` + +The second _incremental stream update result_ contains the remaining data from +the `NameAndHomeWorldFragment`. `lastName` is the only remaining field from this +selection that has not been delivered in a previous result. With this field now +delivered, clients are informed that the `NameAndHomeWorldFragment` has been +completed by the presence of the associated _incremental completion notice_. +Additionally, {"hasNext"} is set to {false} indicating the end of the +_incremental stream_. + +This example demonstrates that it is necessary for clients to process the entire +incremental stream, as both the initial data and previous incremental results +(with a potentially different value for {"id"}) may be required to complete a +deferred fragment. + +```json example +{ + "incremental": [ + { + "id": "1", + "data": { "lastName": "Skywalker" } + } + ], + "completed": [{ "id": "1" }], + "hasNext": false +} +``` diff --git a/spec/GraphQL.md b/spec/GraphQL.md index 7ac0717ac..0dbdfcdb0 100644 --- a/spec/GraphQL.md +++ b/spec/GraphQL.md @@ -60,4 +60,6 @@ working draft release can be found at # [Appendix: Specified Definitions](Appendix%20D%20--%20Specified%20Definitions.md) +# [Appendix: Examples](Appendix%20E%20--%20Examples.md) + # [Appendix: Licensing](../LICENSE.md) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index c49105661..15f0e4ad5 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2343,8 +2343,9 @@ directive @defer( The `@defer` directive may be provided on a fragment spread or inline fragment to indicate that execution of the related selection set should be deferred. When a request includes the `@defer` directive, it may return an _incremental stream_ -consisting of an _initial execution result_ containing all non-deferred data, -followed by one or more _execution update result_ including deferred data. +consisting of an _initial incremental stream result_ containing all non-deferred +data, followed by one or more _incremental stream update result_ including +deferred data. The `@include` and `@skip` directives take precedence over `@defer`. @@ -2371,9 +2372,9 @@ fragment someFragment on User { - `label: String` - An optional string literal used by GraphQL clients to identify data in the _incremental stream_ and associate it with the corresponding defer directive. If provided, the GraphQL service must include - this label in the corresponding _pending result_ within the _incremental - stream_. The `label` argument must be unique across all `@defer` and `@stream` - directives in the document. Variables are disallowed (via + this label in the corresponding _incremental pending notice_ within the + _incremental stream_. The `label` argument must be unique across all `@defer` + and `@stream` directives in the document. Variables are disallowed (via [Defer And Stream Directive Labels Are Unique](#sec-Defer-And-Stream-Directive-Labels-Are-Unique)) because their values may not be known during validation. @@ -2389,9 +2390,9 @@ directive @stream( The `@stream` directive may be provided for a field whose type incorporates a `List` type modifier. The directive enables returning a partial list initially, -followed by additional items in one or more _execution update result_. If the -field type incorporates multiple `List` type modifiers, only the outermost list -is streamed. +followed by additional items in one or more _incremental stream update result_. +If the field type incorporates multiple `List` type modifiers, only the +outermost list is streamed. Note: The mechanism through which items are streamed is implementation-defined and may use technologies such as asynchronous iterators. @@ -2419,9 +2420,9 @@ query myQuery($shouldStream: Boolean! = true) { - `label: String` - An optional string literal used by GraphQL clients to identify data in the _incremental stream_ and associate it with the corresponding stream directive. If provided, the GraphQL service must include - this label in the corresponding _pending result_ within the _incremental - stream_. The `label` argument must be unique across all `@defer` and `@stream` - directives in the document. Variables are disallowed (via + this label in the corresponding _incremental pending notice_ within the + _incremental stream_. The `label` argument must be unique across all `@defer` + and `@stream` directives in the document. Variables are disallowed (via [Defer And Stream Directive Labels Are Unique](#sec-Defer-And-Stream-Directive-Labels-Are-Unique)) because their values may not be known during validation. - `initialCount: Int! = 0` - The number of list items to include initially when diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 4ece8639d..3bb90807f 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -10,7 +10,8 @@ the case that any _execution error_ was raised and replaced with {null}. ## Response Format :: A GraphQL request returns a _response_. A _response_ is either an _execution -result_, a _response stream_, or a _request error result_. +result_, a _response stream_, an _incremental stream_, or a _request error +result_. ### Execution Result @@ -43,6 +44,14 @@ value of this entry is described in the "Extensions" section. subscription and the request included execution. A response stream must be a stream of _execution result_. +### Incremental Stream + +:: A GraphQL request returns an _incremental stream_ when the GraphQL service +has deferred or streamed data as a result of the `@defer` or `@stream` +directives. When the result of the GraphQL operation is an incremental stream, +the first payload will be an _initial incremental stream result_, optionally +followed by one or more _incremental stream update result_. + ### Request Error Result :: A GraphQL request returns a _request error result_ when one or more _request @@ -70,6 +79,80 @@ The _request error result_ map must not contain an entry with key {"data"}. The _request error result_ map may also contain an entry with key `extensions`. The value of this entry is described in the "Extensions" section. +### Initial Incremental Stream Result + +:: An _initial incremental stream result_ contains the result of executing any +non-deferred selections, along with any errors that occurred during their +execution, as well as details of any future _incremental stream update result_ +to be expected. An initial incremental stream result must be the first payload +yielded by an _incremental stream_. + +An _initial incremental stream result_ must be a map. + +The _initial incremental stream result_ must contain entries with keys {"data"}, +{"pending"}, and {"hasNext"}, and may contain entries with keys {"errors"}, +{"incremental"}, {"completed"}, and {"extensions"}. + +The value of {"data"}, {"errors"} and {"extensions"} are defined in the same way +as an _execution result_ as described in the "Data", "Errors", and "Extensions" +sections below. + +The value of {"hasNext"} must be {false} if the initial incremental stream +result is the last response of the incremental stream. Otherwise, {"hasNext"} +must be {true}. + +The value of {"pending"} must be a non-empty list of _incremental pending +notice_. Each _incremental pending notice_ must be a map as described in the +"Incremental Pending Notice" section below. + +The value of {"incremental"}, if present, must be a non-empty list of +_incremental result_. Each _incremental result_ must be a map as described in +the "Incremental Result" section below. + +The value of {"completed"}, if present, must be a non-empty list of _incremental +completion notice_. Each _incremental completion notice_ must be a map as +described in the "Incremental Completion Notice" section below. + +Note: A GraphQL service is permitted to include incrementally delivered data in +the _initial incremental stream_. For example, A GraphQL middleware layer, such +as a caching CDN or proxy service, may wish to intercept and rewrite the +_incremental stream_ before delivering it to a client. This service may collect +some or all of the _incremental pending notice_, _incremental result_, and +_incremental completion notice_ from the entire _incremental stream_ of the +upstream service, and construct a new incremental stream containing a single +payload: an _initial incremental stream result_ containing the all of the +intercepted incremental pending notices, incremental results, and incremental +completion notices, and the {"hasNext"} entry set to false. This would allow the +client to efficiently render the entire result without having to process +multiple payloads. + +### Incremental Stream Update Result + +:: An _incremental stream update result_ contains the result of executing any +deferred selections, along with any errors that occurred during their execution, +as well as details of any future _incremental stream update result_ to be +expected. All payloads yielded by an _incremental stream_, except the first, +must be incremental stream update results. + +An _incremental stream update result_ must be a map. + +The _incremental stream update result_ must contain an entry with the key +{"hasNext"}, and may contain entries with the keys {"pending"}, {"incremental"}, +{"completed"}, and {"extensions"}. Unlike the _initial incremental stream +result_, an _incremental stream update result_ must not contain entries with +keys {"data"} or {"errors"}. + +The value of {"hasNext"} must be {true} for all but the last response in the +_incremental stream_. Otherwise, {"hasNext"} must be {true}. + +The value of {"pending"}, {"incremental"}, and/or {"completed"}, if present are +defined in the same way as an _initial incremental stream result_ as described +in the "Incremental Pending Notice", "Incremental Result", and "Incremental +Completion Notice" sections below. + +The value of {"extensions"}, if present, is defined in the same way as an +_execution result_ as described in the "Extensions" section below. + ### Response Position @@ -94,6 +177,9 @@ represents a path in the response, not in the request. When a _response path_ is present on an _error result_, it identifies the _response position_ which raised the error. +When a _response path_ is present on an _incremental pending notice_, it +identifies the _response position_ of the incremental data update. + A single field execution may result in multiple response positions. For example, ```graphql example @@ -323,17 +409,186 @@ discouraged. ### Extensions -The {"extensions"} entry in an _execution result_ or _request error result_, if -set, must have a map as its value. This entry is reserved for implementers to +The {"extensions"} entry in an _execution result_, _request error result_, +_initial incremental stream result_, or an _incremental stream update result_, +if set, must have a map as its value. This entry is reserved for implementers to extend the protocol however they see fit, and hence there are no additional restrictions on its contents. +### Incremental Pending Notice + +:: A _incremental pending notice_ is used to communicate to clients that the +GraphQL service has chosen to incrementally deliver data associated with a +`@defer` or `@stream` directive. Each incremental pending notice corresponds to +a specific `@defer` or `@stream` directive located at a _response position_ in +the response data. The presence of an incremental pending notice indicates that +clients should expect the associated data in either the current response, or one +of the following responses. + +**Incremental Pending Notice Format** + +An _incremental pending notice_ must be a map. + +An _incremental pending notice_ must contain entries with the keys {"id"} and +{"path"}, and may contain an entry with key {"label"}. + +The value of {"id"} must be a string. This {"id"} should be used by clients to +correlate incremental pending notices with _incremental result_ and _completed +result_. The {"id"} value must be unique across the entire _incremental stream_ +response. There must not be any other incremental pending notice in the +_incremental stream_ with the same {"id"}. + +The value of {"path"} must be a _response position_. When the incremental +pending notice is associated with a `@stream` directive, it indicates the list +at this _response position_ is not known to be complete. Clients should expect +the GraphQL Service to incrementally deliver the remainder list items of this +list. When the incremental pending notice is associated with a `@defer` +directive, it indicates that the response fields contained in the deferred +fragment are not known to be complete. Clients should expect the GraphQL Service +to incrementally deliver the remainder of the fields contained in the deferred +fragment at this _response position_. + +If the associated `@defer` or `@stream` directive contains a `label` argument, +the incremental pending notice must contain an entry {"label"} with the value of +this argument. Clients should use this entry to differentiate the _incremental +pending notices_ for different deferred fragments at the same _response +position_. + +If an incremental pending notice is not returned for a `@defer` or `@stream` +directive, clients must assume that the GraphQL service chose not to +incrementally deliver this data, and the data can be found either in the +{"data"} entry in the _initial incremental stream result_, or one of the prior +_incremental stream update result_ in the _incremental stream_. + +:: The _associated incremental pending notice_ of an _incremental result_ or +_incremental completion notice_ is the _incremental pending notice_ whose {"id"} +entry has the same value as the {"id"} entry of the given incremental result or +incremental completion notice. + +### Incremental Result + +:: The _incremental result_ is used to deliver data that the GraphQL service has +chosen to incrementally deliver. An incremental result may be either an +_incremental list result_ or an _incremental object result_. + +An _incremental result_ must be a map. + +Every _incremental result_ must contain an entry with the key {"id"}, the value +of which is a string referencing its _associated incremental pending notice_. +The associated incremental pending notice must appear either in the _initial +incremental stream result_, in a prior _incremental stream update result_, or in +the same _incremental stream update result_ as the _incremental result_ that +references it. + +#### Incremental List Result + +:: An _incremental list result_ is an _incremental result_ used to deliver +additional list items for a list field with a `@stream` directive. The +_associated incremental pending notice_ for this _incremental list result_ must +be associated with a `@stream` directive. + +The _response position_ for an _incremental list result_ is the {"path"} entry +from its _associated incremental pending notice_. + +**Incremental List Result Format** + +Every _incremental list result_ must contain an {"items"} entry. The {"items"} +entry must contain a list of additional list items for the list field in the +incremental list result's _response position_. The value of this entry must be a +list of the same type of the response field at this _response position_. + +If any _execution error_ were raised during the execution of the results in +{"items"} and these errors propagate to the _response position_ of the +_incremental list result_ (i.e. the streamed list), or a parent response +position of the incremental list result's response position (i.e. a parent of +the streamed list), the incremental list result is considered failed and should +not be included in the _incremental stream_. The errors that caused this failure +will be included in an _incremental completion notice_. + +If any _execution error_ were raised during the execution of the results in +{"items"} and no such error propagated to the _response position_ of the +_incremental list result_, or a parent response position of the incremental list +result's response position, the incremental list result must contain an entry +with key {"errors"} containing these execution errors. The value of this entry +is described in the "Errors" section. + +#### Incremental Object Result + +:: An _incremental object result_ is an _incremental result_ used to deliver +additional response fields that were contained in one or more fragments with a +`@defer` directive. The _associated incremental pending notice_ for this +_incremental object result_ must be associated with a `@defer` directive. + +**Incremental Object Result Format** + +The _incremental object result_ may contain a {"subPath"} entry. If such an +entry is present, the _response position_ of the incremental object result is +the result of appending the value of this {"subPath"} to the value of the +{"path"} entry of the _associated incremental pending notice_. If no {"subPath"} +entry is present, the _response position_ is the value of the associated +incremental pending notice's {"path"} entry. + +An _incremental object result_ may be used to deliver data for response fields +that were contained in more than one deferred fragment. + +In that case, the _associated incremental pending notice_ of the incremental +object result must be one of the _incremental pending notice_ that corresponding +to a fragment that contained the delivered responsive fields. If any of these +incremental pending notices have a {"path"} of varying length, one of the +incremental pending notices with the longest {"path"} must be chosen to minimize +the size of the {"subPath"}. + +Every _incremental object result_ must contain a {"data"} entry. The {"data"} +entry must contain a map of additional response fields. The {"data"} entry in an +incremental object result will be of the type of the field at the incremental +object result's _response position_. + +If any _execution error_ were raised during the execution of the results in +{"data"} and these errors propagated to a parent _response position_ of the +_incremental object result_'s response position, the incremental object result +is considered failed and should not be included in the incremental stream. The +error that caused this failure will be included in an _incremental completion +notice_. + +If any _execution error_ were raised during the execution of the results in +{"data"} and no such error propagated to a parent _response position_ of the +_incremental object result_'s response position, the incremental object result +must contain an entry with key {"errors"} containing these execution errors. The +value of this entry is described in the "Errors" section. + +### Incremental Completion Notice + +:: An _incremental completion notice_ is used to communicate that the GraphQL +service has completed the incremental delivery of the data associated with the +_associated incremental pending notice_. The corresponding data must have been +completed in the same _initial incremental stream result_ or _incremental stream +update result_ in which this incremental completion notice appears. + +**Incremental Completion Notice Format** + +An _incremental completion notice_ must be a map. + +An _incremental completion notice_ must contain an entry with the key {"id"}, +and may contain an entry with the key {"errors"}. + +The value of {"id"} must be a string referencing its _associated incremental +pending notice_. The associated incremental pending notice must appear either in +the _initial incremental stream result_, in a prior _incremental stream update +result_, or in the same _incremental stream update result_ as the _completed +result_ that references it. + +The value of {"errors"}, if present, informs clients that the delivery of the +data from the _associated incremental pending notice_ has failed, due to an +execution error propagating to a parent _response position_ of the _incremental +result_'s response position. The {"errors"} entry must contain these execution +errors. The value of this entry is described in the "Errors" section. + ### Additional Entries To ensure future changes to the protocol do not break existing services and -clients, the _execution result_ and _request error result_ maps must not contain -any entries other than those described above. Clients must ignore any entries -other than those described above. +clients, any of the maps described in the "Response" section (with the exception +of {"extensions"}) must not contain any entries other than those described +above. Clients must ignore any entries other than those described above. ## Serialization Format