From 0701e5bf029344358620a846a4472420f1d8e598 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 7 Jan 2025 13:53:10 -0500 Subject: [PATCH 1/3] Spec edits for incremental delivery, Examples --- spec/Appendix E -- Examples.md | 191 +++++++++++++++++++++++++++++++++ spec/GraphQL.md | 2 + 2 files changed, 193 insertions(+) create mode 100644 spec/Appendix E -- Examples.md diff --git a/spec/Appendix E -- Examples.md b/spec/Appendix E -- Examples.md new file mode 100644 index 000000000..a63da30ad --- /dev/null +++ b/spec/Appendix E -- Examples.md @@ -0,0 +1,191 @@ +# 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 incremental stream might look like: + +The initial response does not contain any deferred or streamed results in the +`data` entry. The initial response contains a `hasNext` entry, indicating that +subsequent responses will be delivered. There are two Pending Results indicating +that results for both the `@defer` and `@stream` in the query will be delivered +in the subsequent responses. + +```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 +} +``` + +Subsequent response 1, contains the deferred data and the first streamed list +item. There is one Completed Result, 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 +} +``` + +Subsequent response 2, 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} would be set to {false} and +this would be the final response. + +```json example +{ + "incremental": [ + { + "id": "1", + "items": [{ "title": "Return of the Jedi" }] + } + ], + "hasNext": true +} +``` + +Subsequent response 3, contains no incremental data. {hasNext} set to {false} +indicates the end of the incremental stream. This response is sent when the +underlying iterator of the `films` field closes. + +```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 + } +} +``` + +The incremental stream might look like: + +The initial response contains the results of the `firstName` field. Even though +it is also present in the `HomeWorldFragment`, it must be returned in the +initial response because it is also defined outside of any fragments with the +`@defer` directive. Additionally, There are two Pending Results indicating that +results for both `@defer`s in the query will be delivered in the subsequent +responses. + +```json example +{ + "data": { + "person": { + "firstName": "Luke" + } + }, + "pending": [ + { "id": "0", "path": ["person"], "label": "homeWorldDefer" }, + { "id": "1", "path": ["person"], "label": "nameAndWorld" } + ], + "hasNext": true +} +``` + +Subsequent response 1, contains the deferred data from `HomeWorldFragment`. +There is one Completed Result, 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. + +The second Incremental Result contains the data for the `terrain` field. This +incremental result contains a `subPath` property to indicate to clients that the +path of this result can be determined by concatenating the path from the Pending +Result with id `"0"` and this `subPath` entry. + +```json example +{ + "incremental": [ + { + "id": "0", + "data": { "homeWorld": { "name": "Tatooine" } } + }, + { + "id": "0", + "subPath": ["homeWorld"], + "data": { "terrain": "desert" } + } + ], + "completed": [{ "id": "0" }], + "hasNext": true +} +``` + +Subsequent response 2, contains the remaining data from the +`NameAndHomeWorldFragment`. `lastName` is the only remaining field that has not +been delivered in a previous response. + +```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) From 30bc97f8062d63df10ef4525b84a166479815366 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 31 Oct 2025 13:59:46 -0400 Subject: [PATCH 2/3] Spec edits for incremental delivery, Section 7 only --- spec/Appendix E -- Examples.md | 60 ++++----- spec/Section 7 -- Response.md | 233 ++++++++++++++++++++++++++++++++- 2 files changed, 257 insertions(+), 36 deletions(-) diff --git a/spec/Appendix E -- Examples.md b/spec/Appendix E -- Examples.md index a63da30ad..b9fe0161c 100644 --- a/spec/Appendix E -- Examples.md +++ b/spec/Appendix E -- Examples.md @@ -21,13 +21,13 @@ fragment HomeWorldFragment on Person { } ``` -The incremental stream might look like: +The _incremental stream_ might look like: -The initial response does not contain any deferred or streamed results in the -`data` entry. The initial response contains a `hasNext` entry, indicating that -subsequent responses will be delivered. There are two Pending Results indicating -that results for both the `@defer` and `@stream` in the query will be delivered -in the subsequent responses. +The _initial execution result_ does not contain any deferred or streamed results +in the {"data"} entry. The initial execution result contains a {"hasNext"} +entry, indicating that _execution update result_ will be delivered. There are +two _pending result_ indicating that results for both the `@defer` and `@stream` +in the query will be delivered in the execution update results. ```json example { @@ -45,9 +45,9 @@ in the subsequent responses. } ``` -Subsequent response 1, contains the deferred data and the first streamed list -item. There is one Completed Result, indicating that the deferred data has been -completely delivered. +_Execution update result_ 1 contains the deferred data and the first streamed +list item. There is one _completed result_, indicating that the deferred data +has been completely delivered. ```json example { @@ -68,10 +68,10 @@ completely delivered. } ``` -Subsequent response 2, 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} would be set to {false} and -this would be the final response. +_Execution update result_ 2 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"} would be set to +{false} and this would be the final execution update result. ```json example { @@ -85,9 +85,9 @@ this would be the final response. } ``` -Subsequent response 3, contains no incremental data. {hasNext} set to {false} -indicates the end of the incremental stream. This response is sent when the -underlying iterator of the `films` field closes. +_Execution update result_ 3 contains no incremental data. {"hasNext"} set to +{false} indicates the end of the _incremental stream_. This response is sent +when the underlying iterator of the `films` field closes. ```json example { @@ -121,14 +121,14 @@ fragment NameAndHomeWorldFragment on Person { } ``` -The incremental stream might look like: +The _incremental stream_ might look like: -The initial response contains the results of the `firstName` field. Even though -it is also present in the `HomeWorldFragment`, it must be returned in the -initial response because it is also defined outside of any fragments with the -`@defer` directive. Additionally, There are two Pending Results indicating that -results for both `@defer`s in the query will be delivered in the subsequent -responses. +The _initial execution result_ contains the results of the `firstName` field. +Even though it is also present in the `HomeWorldFragment`, it must be returned +in the initial response because it is also defined outside of any fragments with +the `@defer` directive. Additionally, There are two _pending result_ indicating +that results for both `@defer`s in the query will be delivered in the execution +update results. ```json example { @@ -145,15 +145,15 @@ responses. } ``` -Subsequent response 1, contains the deferred data from `HomeWorldFragment`. +_Execution update result_ 1 contains the deferred data from `HomeWorldFragment`. There is one Completed Result, 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. +`@defer`s, it is separated into its own _incremental result_. -The second Incremental Result contains the data for the `terrain` field. This -incremental result contains a `subPath` property to indicate to clients that the -path of this result can be determined by concatenating the path from the Pending -Result with id `"0"` and this `subPath` entry. +The second _incremental result_ contains the data for the `terrain` field. This +_incremental result_ contains a {"subPath"} entry to indicate to clients that +the path of this result can be determined by concatenating the path from the +_pending result_ with id `"0"` and this {"subPath"} entry. ```json example { @@ -173,7 +173,7 @@ Result with id `"0"` and this `subPath` entry. } ``` -Subsequent response 2, contains the remaining data from the +_Execution update result_ 2 contains the remaining data from the `NameAndHomeWorldFragment`. `lastName` is the only remaining field that has not been delivered in a previous response. diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 4ece8639d..dddad1209 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 value will be an _initial execution result_, followed by one or more +_execution update result_. + ### Request Error Result :: A GraphQL request returns a _request error result_ when one or more _request @@ -70,6 +79,59 @@ 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 Execution Result + +:: An _initial execution result_ is the first value yielded by an _incremental +stream_. + +An _initial execution result_ must be a map. + +The _initial execution result_ must contain an entry with key {"data"}, and may +contain entries with keys {"errors"} and {"extensions"}. The value of these +entries are defined in the same way as an _execution result_ as described in the +"Data", "Errors", and "Extensions" sections below. + +The _initial execution result_ must contain an entry with the key {"hasNext"}. +The value of this entry must be {true}. + +The _initial execution result_ may contain an entry with the key {"pending"}. +The value of this entry must be a non-empty list of _pending result_. Each +_pending result_ must be a map as described in the "Pending Result" section +below. + +The _initial execution result_ may contain an entry with they key +{"incremental"}. The value of this entry 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 _initial execution result_ may contain an entry with they key {"completed"}. +The value of this entry must be a non-empty list of _completed result_. Each +_completed result_ must be a map as described in the "Completed Result" section +below. + +### Execution Update Result + +:: An _execution update result_ is the value yielded by an _incremental stream_ +for all values except the first. + +An _execution update result_ must be a map. + +Unlike the _initial execution result_, an _execution update result_ must not +contain entries with keys {"data"} or {"errors"}. + +An _execution update result_ may contain an entry with the key {"extensions"}. +The value of this entry is described in the "Extensions" section. + +An _execution update result_ must contain an entry with the key {"hasNext"}. The +value of this entry must be {true} for all but the last response in the +_incremental stream_. The value of this entry must be {false} for the last +response of the incremental stream. + +The _initial execution result_ may contain entries with keys {"pending"}, +{"incremental"}, and/or {"completed"}. The value of these entries are defined in +the same way as an _initial execution result_ as described in the "Pending +Result", "Incremental Result", and "Completed Result" sections below. + ### Response Position @@ -94,6 +156,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 result_, 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,15 +388,171 @@ 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 -extend the protocol however they see fit, and hence there are no additional -restrictions on its contents. +The {"extensions"} entry in an _execution result_, _request error result_, +_initial execution result_, or a _execution 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. + +### Pending Result + +:: A _pending result_ is used to communicate to clients that the GraphQL service +has chosen to incrementally deliver data associated with a `@defer` or `@stream` +directive. Each pending result corresponds to a specific `@defer` or `@stream` +directive located at a _response position_ in the response data. The presence of +a pending result indicates that clients should expect the associated data in +either the current response, or one of the following responses. + +**Pending Result Format** + +A _pending result_ must be a map. + +Every _pending result_ must contain an entry with the key {"id"} with a string +value. This {"id"} should be used by clients to correlate pending results with +_incremental result_ and _completed result_. The {"id"} value must be unique for +the entire _incremental stream_ response. There must not be any other pending +result in the _incremental stream_ that contains the same {"id"}. + +Every _pending result_ must contain an entry with the key {"path"}. When the +pending result 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 pending result 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 pending result must contain an entry {"label"} with the value of this +argument. Clients should use this entry to differentiate the _pending results_ +for different deferred fragments at the same _response position_. + +If a pending result 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 execution result_, or one of the prior _execution update result_ in the +_incremental stream_. + +:: The _associated pending result_ is a specific _pending result_ associated +with any given _incremental result_ or _completed result_. The associated +pending result can be determined by finding the pending result where the value +of its {"id"} entry is the same value of the {"id"} entry of the given +incremental result or completed result. The associated pending result must +appear in the _incremental stream_, in the same or prior _initial execution +result_ or _execution update result_ as the given incremental result or +completed result. + +### Incremental Result + +:: The _incremental result_ is used to deliver data that the GraphQL service has +chosen to incrementally deliver. An incremental result may be ether 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"} with a +string value. The definition of _associated pending result_ describes how this +value is used to determine the associated pending result for a given +_incremental result_. + +#### Incremental List Result + +:: An _incremental list result_ is a _incremental result_ used to deliver +additional list items for a list field with a `@stream` directive. The +_associated pending result_ 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 pending result_. + +**Incremental List Result Format** + +Every _incremental list result_ must contain an entry with the key {"id"}, used +to determine the _associated pending result_ for this _incremental result_. + +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 a _response position_ higher than the +_incremental list result_'s response position, 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 a _completed result_. + +If any _execution error_ were raised during the execution of the results in +{"items"} and these errors did not propagate to a path higher than the +_incremental list result_'s path, 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 a _incremental result_ used to deliver +additional response fields that were contained in one or more fragments with a +`@defer` directive. The _associated pending result_ 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 this entry +is present, The incremental object result's _response position_ can be +determined by concatenating the value of the _associated pending result_'s +{"path"} entry with the value of this {"subPath"} entry. If no {"subPath"} entry +is present, the _response position_ is the value of the associated pending +result's {"path"} entry. + +An _incremental object result_ may be used to deliver data for response fields +that were contained in more than one deferred fragments. In that case, the +_associated pending result_ of the incremental object result must be a _pending +result_ with the longest {"path"}. + +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 _response position_ higher than the +_incremental object result_'s response position, The incremental object result +is considered failed and should not be included in the incremental stream. The +errors that caused this failure will be included in a _completed result_. + +If any _execution error_ were raised during the execution of the results in +{"data"} and these errors did not propagate to a _response position_ higher than +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. + +### Completed Result + +:: A _completed result_ is used to communicate that the GraphQL service has +completed the incremental delivery of the data associated with the _associated +pending result_. The corresponding data must have been completed in the same +_initial execution result_ or _execution update result_ in which this completed +result appears. + +**Completed Result Format** + +A _completed result_ must be a map. + +Every _completed result_ must contain an entry with the key {"id"} with a string +value. The definition of _associated pending result_ describes how this value is +used to determine the associated pending result for a given _completed result_. + +A _completed result_ may contain an {"errors"} entry. When the {"errors"} entry +is present, it informs clients that the delivery of the data from the +_associated pending result_ has failed, due to an execution error propagating to +a _response position_ higher than 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 +clients, any of the maps described in the "Response" section must not contain any entries other than those described above. Clients must ignore any entries other than those described above. From 153b96a04f287205c79916043bb215ae490280d3 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 6 Nov 2025 15:05:43 -0500 Subject: [PATCH 3/3] fix typos --- spec/Appendix E -- Examples.md | 2 +- spec/Section 7 -- Response.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/Appendix E -- Examples.md b/spec/Appendix E -- Examples.md index b9fe0161c..618c95109 100644 --- a/spec/Appendix E -- Examples.md +++ b/spec/Appendix E -- Examples.md @@ -126,7 +126,7 @@ The _incremental stream_ might look like: The _initial execution result_ contains the results of the `firstName` field. Even though it is also present in the `HomeWorldFragment`, it must be returned in the initial response because it is also defined outside of any fragments with -the `@defer` directive. Additionally, There are two _pending result_ indicating +the `@defer` directive. Additionally, there are two _pending result_ indicating that results for both `@defer`s in the query will be delivered in the execution update results. diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index dddad1209..6668f932b 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -389,7 +389,7 @@ discouraged. ### Extensions The {"extensions"} entry in an _execution result_, _request error result_, -_initial execution result_, or a _execution update result_, if set, must have a +_initial execution result_, or an _execution 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. @@ -446,7 +446,7 @@ completed result. ### Incremental Result :: The _incremental result_ is used to deliver data that the GraphQL service has -chosen to incrementally deliver. An incremental result may be ether an +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. @@ -458,7 +458,7 @@ _incremental result_. #### Incremental List Result -:: An _incremental list result_ is a _incremental result_ used to deliver +:: An _incremental list result_ is an _incremental result_ used to deliver additional list items for a list field with a `@stream` directive. The _associated pending result_ for this _incremental list result_ must be associated with a `@stream` directive. @@ -478,7 +478,7 @@ 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 a _response position_ higher than the -_incremental list result_'s response position, The incremental list result is +_incremental list result_'s response position, 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 a _completed result_. @@ -490,7 +490,7 @@ entry is described in the "Errors" section. #### Incremental Object Result -:: An _incremental object result_ is a _incremental result_ used to deliver +:: 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 pending result_ for this _incremental object result_ must be associated with a `@defer` directive. @@ -498,7 +498,7 @@ result_ must be associated with a `@defer` directive. **Incremental Object Result Format** The _incremental object result_ may contain a {"subPath"} entry. If this entry -is present, The incremental object result's _response position_ can be +is present, the incremental object result's _response position_ can be determined by concatenating the value of the _associated pending result_'s {"path"} entry with the value of this {"subPath"} entry. If no {"subPath"} entry is present, the _response position_ is the value of the associated pending @@ -516,7 +516,7 @@ 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 _response position_ higher than the -_incremental object result_'s response position, The incremental object result +_incremental object result_'s response position, the incremental object result is considered failed and should not be included in the incremental stream. The errors that caused this failure will be included in a _completed result_.