-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Spec edits for incremental delivery, Section 3 & 7 only #1124
base: incremental-integration
Are you sure you want to change the base?
Changes from all commits
abafb76
0310656
fcf898f
ffb8cad
c7c3ee1
2934a59
5d72416
03adfb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
# C. 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 stream might look like: | ||
|
||
Payload 1, 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 payloads will be delivered. There are two Pending | ||
Responses indicating that results for both the `@defer` and `@stream` in the | ||
query will be delivered in the subsequent payloads. | ||
|
||
```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 | ||
} | ||
``` | ||
|
||
Payload 2, 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 | ||
} | ||
``` | ||
|
||
Payload 3, contains the final stream payload. 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 | ||
} | ||
``` | ||
|
||
Payload 4, contains no incremental data. {hasNext} set to {false} indicates the | ||
end of the response 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 response stream might look like: | ||
|
||
Payload 1, 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 payload because it is also defined outside of any fragments with | ||
the `@defer` directive. Additionally, There are two Pending Responses indicating | ||
that results for both `@defer`s in the query will be delivered in the subsequent | ||
payloads. | ||
|
||
```json example | ||
{ | ||
"data": { | ||
"person": { | ||
"firstName": "Luke" | ||
} | ||
}, | ||
"pending": [ | ||
{ "id": "0", "path": ["person"], "label": "homeWorldDefer" }, | ||
{ "id": "1", "path": ["person"], "label": "nameAndWorld" } | ||
], | ||
"hasNext": true | ||
} | ||
``` | ||
|
||
Payload 2, 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 | ||
} | ||
``` | ||
|
||
Payload 3, contains the remaining data from the `NameAndHomeWorldFragment`. | ||
`lastName` is the only remaining field that has not been delivered in a previous | ||
payload. | ||
|
||
```json example | ||
{ | ||
"incremental": [ | ||
{ | ||
"id": "1", | ||
"data": { "lastName": "Skywalker" } | ||
} | ||
], | ||
"completed": [{ "id": "1" }], | ||
"hasNext": false | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -794,8 +794,8 @@ And will yield the subset of each object type queried: | |
When querying an Object, the resulting mapping of fields are conceptually | ||
ordered in the same order in which they were encountered during execution, | ||
excluding fragments for which the type does not apply and fields or fragments | ||
that are skipped via `@skip` or `@include` directives. This ordering is | ||
correctly produced when using the {CollectFields()} algorithm. | ||
that are skipped via `@skip` or `@include` directives or postponed via `@defer`. | ||
This ordering is correctly produced when using the {CollectFields()} algorithm. | ||
|
||
Response serialization formats capable of representing ordered maps should | ||
maintain this ordering. Serialization formats which can only represent unordered | ||
|
@@ -1946,6 +1946,14 @@ GraphQL implementations that support the type system definition language must | |
provide the `@deprecated` directive if representing deprecated portions of the | ||
schema. | ||
|
||
GraphQL implementations may provide the `@defer` and/or `@stream` directives. If | ||
either or both of these directives are provided, they must conform to the | ||
requirements defined in this specification. | ||
|
||
Note: The [Directives Are Defined](#sec-Directives-Are-Defined) validation rule | ||
ensures that GraphQL Operations containing the `@defer` or `@stream` directives | ||
cannot be executed by a GraphQL service that does not support them. | ||
|
||
GraphQL implementations that support the type system definition language should | ||
provide the `@specifiedBy` directive if representing custom scalar definitions. | ||
|
||
|
@@ -2162,3 +2170,108 @@ to the relevant IETF specification. | |
```graphql example | ||
scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") | ||
``` | ||
|
||
### @defer | ||
|
||
```graphql | ||
directive @defer( | ||
label: String | ||
if: Boolean! = true | ||
) on FRAGMENT_SPREAD | INLINE_FRAGMENT | ||
``` | ||
|
||
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, the response may consist of multiple | ||
payloads: the initial payload containing all non-deferred data, while subsequent | ||
payloads include deferred data. | ||
|
||
The `@include` and `@skip` directives take precedence over `@defer`. | ||
|
||
```graphql example | ||
query myQuery($shouldDefer: Boolean! = true) { | ||
user { | ||
name | ||
...someFragment @defer(label: "someLabel", if: $shouldDefer) | ||
} | ||
} | ||
fragment someFragment on User { | ||
id | ||
profile_picture { | ||
uri | ||
} | ||
} | ||
``` | ||
|
||
#### @defer Arguments | ||
|
||
- `if: Boolean! = true` - When `true`, fragment _should_ be deferred (see | ||
related note below). When `false`, fragment will not be deferred. Defaults to | ||
`true` when omitted. | ||
- `label: String` - An optional string literal (variables are disallowed) used | ||
by GraphQL clients to identify data from payloads and associate it with the | ||
corresponding defer directive. If provided, the GraphQL service must include | ||
this label in the corresponding pending object within the response. The | ||
`label` argument must be unique across all `@defer` and `@stream` directives | ||
in the document. | ||
|
||
### @stream | ||
|
||
```graphql | ||
directive @stream( | ||
label: String | ||
if: Boolean! = true | ||
initialCount: Int = 0 | ||
) on FIELD | ||
``` | ||
|
||
The `@stream` directive may be provided for a field whose type incorporates a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same note should be there for the |
||
`List` type modifier; the directive enables the backend to leverage technology | ||
such as asynchronous iterators to provide a partial list initially, and | ||
additional list items in subsequent payloads. | ||
|
||
The `@include` and `@skip` directives take precedence over `@stream`. | ||
|
||
Note: The [Directives Are Defined](#sec-Directives-Are-Defined) validation rule | ||
ensures that GraphQL Operations containing the `@stream` directive cannot be | ||
executed by a GraphQL service that does not support this directive. | ||
|
||
```graphql example | ||
query myQuery($shouldStream: Boolean! = true) { | ||
user { | ||
friends(first: 10) { | ||
nodes | ||
@stream(label: "friendsStream", initialCount: 5, if: $shouldStream) { | ||
name | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
#### @stream Arguments | ||
|
||
- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related | ||
note below). When `false`, the field will not be streamed and all list items | ||
will be initially included. Defaults to `true` when omitted. | ||
- `label: String` - An optional string literal (variables are disallowed) used | ||
by GraphQL clients to identify data from payloads and associate it with the | ||
corresponding stream directive. If provided, the GraphQL service must include | ||
this label in the corresponding pending object within the response. The | ||
`label` argument must be unique across all `@defer` and `@stream` directives | ||
in the document. | ||
- `initialCount: Int` - The number of list items the service should return | ||
initially. If omitted, defaults to `0`. A field error will be raised if the | ||
value of this argument is less than `0`. | ||
|
||
Note: The ability to defer and/or stream parts of a response can have a | ||
robrichard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
potentially significant impact on application performance. Developers generally | ||
need clear, predictable control over their application's performance. It is | ||
highly recommended that GraphQL services honor the `@defer` and `@stream` | ||
directives on each execution. However, the specification allows advanced use | ||
cases where the service can determine that it is more performant to not defer | ||
and/or stream. Therefore, GraphQL clients _must_ be able to process a response | ||
that ignores the `@defer` and/or `@stream` directives. This also applies to the | ||
`initialCount` argument on the `@stream` directive. Clients _must_ be able to | ||
process a streamed response that contains a different number of initial list | ||
items than what was specified in the `initialCount` argument. | ||
Comment on lines
+2274
to
+2277
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, I didn't realise we had applied this to initialCount - I thought it was either non-deferred (i.e. give you the whole list) or deferred with initialCount supplied. I think we should revisit this discussion, it's quite different to the general "don't defer" and "don't stream" optimizations in my mind - specifically if you specify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Happy to revisit this discussion. If I am remembering correctly, the arguments for this version were:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be
must not
be deferred?