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 @defer/@stream #742

Closed
wants to merge 62 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
42bd98a
Introduce @defer and @stream.
robrichard Aug 18, 2022
583acb6
fix typos
robrichard Feb 17, 2021
a4bbad6
clear up that it is legal to support either defer or stream individually
robrichard Feb 17, 2021
b4fb65c
Add sumary of arguments to Type System
robrichard Feb 17, 2021
3257f19
Update Section 3 -- Type System.md
robrichard May 15, 2021
3cce3fa
clarification on defer/stream requirement
robrichard Nov 19, 2021
f81d745
clarify negative values of initialCount
robrichard Nov 20, 2021
b0cae7f
allow extensions only subsequent payloads
robrichard Nov 25, 2021
9449853
fix typo
robrichard Nov 26, 2021
c812890
Raise a field error if initialCount is less than zero
robrichard Aug 18, 2022
c1fefe4
data is not necessarily an object in subsequent payloads
robrichard Dec 6, 2021
6dd9b79
add Defer And Stream Directives Are Used On Valid Root Field rule
robrichard Dec 6, 2021
ebfefb6
wait for parent async record to ensure correct order of payloads
robrichard Aug 18, 2022
30674c8
Simplify execution, payloads should begin execution immediately
robrichard Dec 20, 2021
101516b
Clarify error handling
robrichard Dec 20, 2021
80ff450
add isCompletedIterator to AsyncPayloadRecord to track completed iter…
robrichard Dec 30, 2021
b73af22
fix typo
robrichard Jan 21, 2022
f634192
deferDirective and visitedFragments
robrichard Feb 2, 2022
7eafac8
stream if argument, indexPath -> itemPath
robrichard Feb 7, 2022
83e058d
Clarify stream only applies to outermost list of multi-dimensional ar…
robrichard Feb 7, 2022
5aa915b
add validation “Defer And Stream Directive Labels Are Unique”
robrichard Mar 7, 2022
d7fe43a
Clarification on labels
robrichard Mar 8, 2022
379f10c
fix wrong quotes
robrichard Mar 23, 2022
1ffad84
remove label/path requirement
robrichard Mar 23, 2022
21dd0bc
add missing line
robrichard Jun 9, 2022
3a569b6
fix ExecuteRequest
robrichard Jun 9, 2022
a596ead
fix response
robrichard Jun 9, 2022
4543a47
Align deferred fragment field collection with reference implementation
robrichard Aug 3, 2022
5a093cd
spec updates to reflect latest discussions
robrichard Aug 18, 2022
8fa426e
Note about mutation execution order
robrichard Aug 18, 2022
fcc6393
minor change for uniqueness
robrichard Aug 18, 2022
aa22c5f
fix typos
robrichard Aug 18, 2022
089d06c
if: Boolean! = true
robrichard Aug 23, 2022
a154bda
address pr feedback
robrichard Aug 23, 2022
67689c5
clarify null behavior of if
robrichard Aug 24, 2022
9230085
Add error boundary behavior
robrichard Sep 8, 2022
4bf09d0
defer/stream response => payload
robrichard Sep 8, 2022
1499e64
event stream => response stream
robrichard Sep 8, 2022
722de48
link to path section
robrichard Sep 8, 2022
c4bdfaf
use case no dash
robrichard Sep 8, 2022
d84dd0b
remove "or null"
robrichard Sep 8, 2022
2248f35
add detailed incremental example
robrichard Sep 8, 2022
2fb409c
update label validation rule
robrichard Sep 8, 2022
66c3f7b
clarify hasNext on incremental example
robrichard Sep 8, 2022
9287b62
clarify canceling of subsequent payloads
robrichard Sep 8, 2022
e0c0ad9
Add examples for non-null cases
robrichard Sep 8, 2022
99baf54
typo
robrichard Sep 9, 2022
4931f7a
improve non-null example
robrichard Sep 9, 2022
da96e98
Add FilterSubsequentPayloads algorithm
robrichard Sep 9, 2022
aff1113
link to note on should
robrichard Oct 12, 2022
fb3e05c
update on hasNext
robrichard Nov 1, 2022
039e4eb
small fixes (#3)
yaacovCR Nov 7, 2022
777c21b
remove ResolveFIeldGenerator (#4)
yaacovCR Nov 16, 2022
7626800
fix typos (#6)
yaacovCR Nov 18, 2022
d16a432
Add error handling for stream iterators (#5)
yaacovCR Nov 21, 2022
979e581
Raise a field error if defer/stream encountered during subscription e…
robrichard Nov 22, 2022
1207121
Add validation rule for defer/stream on subscriptions
robrichard Nov 22, 2022
6545a55
clarify label is not required
robrichard Nov 23, 2022
4c2e3f9
fix parentRecord argument in ExecuteStreamField (#7)
yaacovCR Nov 29, 2022
e4ee3eb
fix typo
robrichard Dec 5, 2022
22de5de
replace server with service
robrichard Jan 15, 2023
c630301
CollectFields does not require path or asyncRecord (#11)
yaacovCR Jan 16, 2023
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
106 changes: 104 additions & 2 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -794,8 +794,9 @@ 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 temporarily skipped 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 @@ -1942,6 +1943,11 @@ by a validator, executor, or client tool such as a code generator.

GraphQL implementations should provide the `@skip` and `@include` directives.

GraphQL implementations are not required to implement the `@defer` and `@stream`
directives. If either or both of these directives are implemented, they must be
implemented according to this specification. GraphQL implementations that do not
support these directives must not make them available via introspection.

GraphQL implementations that support the type system definition language must
provide the `@deprecated` directive if representing deprecated portions of the
schema.
Expand Down Expand Up @@ -2162,3 +2168,99 @@ 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 for fragment spreads and inline fragments
to inform the executor to delay the execution of the current fragment to
indicate deprioritization of the current fragment. A query with `@defer`
directive will cause the request to potentially return multiple responses, where
non-deferred data is delivered in the initial response and data deferred is
delivered in a subsequent response. `@include` and `@skip` take precedence over
`@defer`.

```graphql example
query myQuery($shouldDefer: Boolean) {
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](#note-088b7)). When `false`, fragment will not be deferred and
data will be included in the initial response. Defaults to `true` when
omitted.
- `label: String` - May be used by GraphQL clients to identify the data from
responses and associate it with the corresponding defer directive. If
provided, the GraphQL service must add it to the corresponding payload.
`label` must be unique label across all `@defer` and `@stream` directives in a
document. `label` must not be provided as a variable.

### @stream

```graphql
directive @stream(
label: String
if: Boolean! = true
initialCount: Int = 0
) on FIELD
```

The `@stream` directive may be provided for a field of `List` type so that the
backend can leverage technology such as asynchronous iterators to provide a
partial list in the initial response, and additional list items in subsequent
responses. `@include` and `@skip` take precedence over `@stream`.

```graphql example
query myQuery($shouldStream: Boolean) {
user {
friends(first: 10) {
nodes @stream(label: "friendsStream", initialCount: 5, if: $shouldStream)
}
}
}
```

#### @stream Arguments

- `if: Boolean! = true` - When `true`, field _should_ be streamed (See
[related note](#note-088b7)). When `false`, the field will not be streamed and
all list items will be included in the initial response. Defaults to `true`
when omitted.
- `label: String` - May be used by GraphQL clients to identify the data from
responses and associate it with the corresponding stream directive. If
provided, the GraphQL service must add it to the corresponding payload.
`label` must be unique label across all `@defer` and `@stream` directives in a
document. `label` must not be provided as a variable.
robrichard marked this conversation as resolved.
Show resolved Hide resolved
- `initialCount: Int` - The number of list items the service should return as
part of the initial response. 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
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.
179 changes: 179 additions & 0 deletions spec/Section 5 -- Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ FieldsInSetCanMerge(set):
{set} including visiting fragments and inline fragments.
- Given each pair of members {fieldA} and {fieldB} in {fieldsForName}:
- {SameResponseShape(fieldA, fieldB)} must be true.
- {SameStreamDirective(fieldA, fieldB)} must be true.
- If the parent types of {fieldA} and {fieldB} are equal or if either is not
an Object Type:
- {fieldA} and {fieldB} must have identical field names.
Expand Down Expand Up @@ -455,6 +456,16 @@ SameResponseShape(fieldA, fieldB):
- If {SameResponseShape(subfieldA, subfieldB)} is false, return false.
- Return true.

SameStreamDirective(fieldA, fieldB):

- If neither {fieldA} nor {fieldB} has a directive named `stream`.
- Return true.
- If both {fieldA} and {fieldB} have a directive named `stream`.
- Let {streamA} be the directive named `stream` on {fieldA}.
- Let {streamB} be the directive named `stream` on {fieldB}.
- If {streamA} and {streamB} have identical sets of arguments, return true.
Copy link
Contributor

Choose a reason for hiding this comment

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

if they have just different initialCount, shouldn't it be considered 'the same' case?

Copy link
Contributor Author

@robrichard robrichard Sep 8, 2022

Choose a reason for hiding this comment

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

This was what was decided based on feedback from Facebook's implementation: graphql/graphql-js#2319 (comment)

If multiple streams on the same field are needed, they can be aliased. Again, if this blocks certain use cases I think it can be explored in a follow up proposal.

- Return false.

**Explanatory Text**

If multiple field selections with the same response names are encountered during
Expand Down Expand Up @@ -1517,6 +1528,174 @@ query ($foo: Boolean = true, $bar: Boolean = false) {
}
```

### Defer And Stream Directives Are Used On Valid Root Field

** Formal Specification **

- For every {directive} in a document.
- Let {directiveName} be the name of {directive}.
- Let {mutationType} be the root Mutation type in {schema}.
- Let {subscriptionType} be the root Subscription type in {schema}.
- If {directiveName} is "defer" or "stream":
- The parent type of {directive} must not be {mutationType} or
{subscriptionType}.

**Explanatory Text**

The defer and stream directives are not allowed to be used on root fields of the
mutation or subscription type.

For example, the following document will not pass validation because `@defer`
has been used on a root mutation field:

```raw graphql counter-example
mutation {
... @defer {
mutationField
}
}
```

### Defer And Stream Directives Are Used On Valid Operations

** Formal Specification **

- Let {subscriptionFragments} be the empty set.
- For each {operation} in a document:
- If {operation} is a subscription operation:
- Let {fragments} be every fragment referenced by that {operation}
transitively.
- For each {fragment} in {fragments}:
- Let {fragmentName} be the name of {fragment}.
- Add {fragmentName} to {subscriptionFragments}.
- For every {directive} in a document:
- If {directiveName} is not "defer" or "stream":
- Continue to the next {directive}.
- Let {ancestor} be the ancestor operation or fragment definition of
{directive}.
- If {ancestor} is a fragment definition:
- If the fragment name of {ancestor} is not present in
{subscriptionFragments}:
- Continue to the next {directive}.
- If {ancestor} is not a subscription operation:
- Continue to the next {directive}.
- Let {if} be the argument named "if" on {directive}.
- {if} must be defined.
- Let {argumentValue} be the value passed to {if}.
- {argumentValue} must be a variable, or the boolean value "false".

**Explanatory Text**

The defer and stream directives can not be used to defer or stream data in
subscription operations. If these directives appear in a subscription operation
they must be disabled using the "if" argument. This rule will not permit any
defer or stream directives on a subscription operation that cannot be disabled
using the "if" argument.

For example, the following document will not pass validation because `@defer`
has been used in a subscription operation with no "if" argument defined:

```raw graphql counter-example
subscription sub {
newMessage {
... @defer {
body
}
}
}
```

### Defer And Stream Directive Labels Are Unique

** Formal Specification **

- Let {labelValues} be an empty set.
- For every {directive} in the document:
- Let {directiveName} be the name of {directive}.
- If {directiveName} is "defer" or "stream":
- For every {argument} in {directive}:
- Let {argumentName} be the name of {argument}.
- Let {argumentValue} be the value passed to {argument}.
- If {argumentName} is "label":
- {argumentValue} must not be a variable.
- {argumentValue} must not be present in {labelValues}.
- Append {argumentValue} to {labelValues}.

**Explanatory Text**

The `@defer` and `@stream` directives each accept an argument "label". This
label may be used by GraphQL clients to uniquely identify response payloads. If
a label is passed, it must not be a variable and it must be unique within all
other `@defer` and `@stream` directives in the document.

For example the following document is valid:

```graphql example
{
dog {
...fragmentOne
...fragmentTwo @defer(label: "dogDefer")
}
pets @stream(label: "petStream") {
name
}
}

fragment fragmentOne on Dog {
name
}

fragment fragmentTwo on Dog {
owner {
name
}
}
```

For example, the following document will not pass validation because the same
label is used in different `@defer` and `@stream` directives.:

```raw graphql counter-example
{
dog {
...fragmentOne @defer(label: "MyLabel")
}
pets @stream(label: "MyLabel") {
name
}
}

fragment fragmentOne on Dog {
name
}
```

### Stream Directives Are Used On List Fields

**Formal Specification**

- For every {directive} in a document.
- Let {directiveName} be the name of {directive}.
- If {directiveName} is "stream":
- Let {adjacent} be the AST node the directive affects.
- {adjacent} must be a List type.

**Explanatory Text**

GraphQL directive locations do not provide enough granularity to distinguish the
type of fields used in a GraphQL document. Since the stream directive is only
valid on list fields, an additional validation rule must be used to ensure it is
used correctly.

For example, the following document will only pass validation if `field` is
defined as a List type in the associated schema.

```graphql counter-example
query {
field @stream(initialCount: 0)
}
```

## Variables

### Variable Uniqueness
Expand Down
Loading