Skip to content
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

Open
wants to merge 8 commits into
base: incremental-integration
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ignoreRegExpList:
- /[a-z]{2,}'s/
words:
# Terms of art
- deprioritization
- endianness
- interoperation
- monospace
Expand Down
191 changes: 191 additions & 0 deletions spec/Appendix C -- Examples.md
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
}
```
2 changes: 2 additions & 0 deletions spec/GraphQL.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,5 @@ Note: This is an example of a non-normative note.
# [Appendix: Notation Conventions](Appendix%20A%20--%20Notation%20Conventions.md)

# [Appendix: Grammar Summary](Appendix%20B%20--%20Grammar%20Summary.md)

# [Appendix: Examples](Appendix%20C%20--%20Examples.md)
117 changes: 115 additions & 2 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Copy link
Contributor

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?

`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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same note should be there for the @defer section above

`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
Copy link
Member

Choose a reason for hiding this comment

The 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 initialCount: 2 I'd argue that at least 2 results should be supplied, or the entire thing should not be deferred (e.g. if there are fewer than 2 results). Skipping @defer / @include result in the client getting more data up front, but ignoring initialCount allows for the client to get less data up front, and that's a problem to my mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:

  • We are still "highly recommending" servers send the correct number of list items earlier in this paragraph
  • This allows the most flexibility, clients shouldn't make any assumptions about the length of the initial list
  • From a type perspective clients should already not be making assumptions about the length of the list

Loading
Loading