Skip to content

RFC: deduplicated incremental delivery #1052

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

Closed
wants to merge 46 commits into from
Closed
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
d12884e
Extract common logic from ExecuteQuery, ExecuteMutation and ExecuteSu…
benjie Apr 28, 2023
72d5447
Change ExecuteSelectionSet to ExecuteGroupedFieldSet
benjie Apr 28, 2023
4d62b8b
Correct reference to MergeSelectionSets
benjie Aug 21, 2023
8fd0df3
move Field Collection section earlier
yaacovCR Dec 6, 2023
f5e26e3
enhance(ResolveFieldValue): add async collection language
yaacovCR Dec 7, 2023
648bf34
Introduce @stream.
robrichard Aug 18, 2022
3677a09
Introduce @defer.
robrichard Dec 6, 2023
0b25562
Add GetPending algorithm
yaacovCR Jan 9, 2024
dc443e8
fix nomenclature for GetParentAndParentDeferState
yaacovCR Jan 9, 2024
8cc2507
doc: add more prose for ProcessIncrementalDigests
yaacovCR Jan 9, 2024
8c138b0
fix: add missing incremental digest processing for streams
yaacovCR Jan 9, 2024
ea03b3c
nit: add caps for Deferred Fragment
yaacovCR Jan 9, 2024
1be7a58
fix: remove unnecessary variable
yaacovCR Jan 9, 2024
d998aca
fix: change variable name to be consistent
yaacovCR Jan 9, 2024
b97a920
fix typos in GetUpdatesForDeferredResult
yaacovCR Jan 9, 2024
a694666
only save defer parent rather than all ancestors
yaacovCR Jan 10, 2024
6941ddd
remove Incremental Digests concept
yaacovCR Jan 10, 2024
aa6e3df
if nested defers are completed, keep processing them
yaacovCR Jan 10, 2024
21ef532
fix how new defer usages are collected
yaacovCR Jan 15, 2024
74f2f57
rewrite
yaacovCR Jan 18, 2024
ae882e8
change to have an incremental update stream mapped to an incremental …
yaacovCR Jan 19, 2024
19a4757
small fix with regard to update packaging
yaacovCR Jan 19, 2024
a9e2d87
editorial change
yaacovCR Jan 19, 2024
f43b57f
Fix typo
yaacovCR Jan 19, 2024
986853f
use some initialization magic
yaacovCR Jan 26, 2024
77c7846
Remove GetNonEmptyParent
yaacovCR Jan 26, 2024
69c9f5f
this should never be previously defined, as each future is returned e…
yaacovCR Jan 26, 2024
0cf445e
fix mistake when setting count
yaacovCR Jan 26, 2024
fa67030
rename {defers} to {deferredFragments}
yaacovCR Jan 26, 2024
7c0fccf
a child defer state is always defined unless removed here
yaacovCR Jan 26, 2024
19fbef6
get rid of count and sent
yaacovCR Jan 26, 2024
3a14539
Rename GetSinglyDeferredFutures to FilterNestedFutures
yaacovCR Jan 26, 2024
0d91637
add some more magic
yaacovCR Jan 26, 2024
d7906cf
typo
yaacovCR Jan 27, 2024
4511190
refine ReleaseFragment
yaacovCR Jan 28, 2024
a289616
complete renaming
yaacovCR Jan 28, 2024
63cf6c2
fix typo, changing ancestors to parent
yaacovCR Jan 29, 2024
5e62f59
move ExecuteInitialResult to later
yaacovCR Jan 29, 2024
c00963d
simplify
yaacovCR Jan 30, 2024
19216eb
minor fixes to the major rewrite
yaacovCR Jan 30, 2024
3ffa48a
switch line order
yaacovCR Jan 30, 2024
6c289c4
add return line when finished
yaacovCR Jan 30, 2024
894b994
finish renaming update
yaacovCR Jan 30, 2024
3768025
initialize only after yielding
yaacovCR Jan 30, 2024
4fc4dc1
fix reset of variable
yaacovCR Jan 30, 2024
a966583
fix variable name typo
yaacovCR Jan 30, 2024
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
@@ -4,6 +4,7 @@ ignoreRegExpList:
- /[a-z]{2,}'s/
words:
# Terms of art
- deprioritization
- endianness
- interoperation
- monospace
106 changes: 104 additions & 2 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
@@ -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
@@ -1941,6 +1942,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.
@@ -2161,3 +2167,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.
- `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.
181 changes: 180 additions & 1 deletion spec/Section 5 -- Validation.md
Original file line number Diff line number Diff line change
@@ -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.
@@ -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.
- Return false.

**Explanatory Text**

If multiple field selections with the same response names are encountered during
@@ -463,7 +474,7 @@ unambiguous. Therefore any two field selections which might both be encountered
for the same object are only valid if they are equivalent.

During execution, the simultaneous execution of fields with the same response
name is accomplished by {MergeSelectionSets()} and {CollectFields()}.
name is accomplished by {CollectSubfields()}.

For simple hand-written GraphQL, this rule is obviously a clear developer error,
however nested fragments can make this difficult to detect manually.
@@ -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
1,184 changes: 1,016 additions & 168 deletions spec/Section 6 -- Execution.md

Large diffs are not rendered by default.

208 changes: 197 additions & 11 deletions spec/Section 7 -- Response.md
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ the case that any _field error_ was raised on a field and was replaced with

## Response Format

A response to a GraphQL request must be a map.
A response to a GraphQL request must be a map or a response stream of maps.

If the request raised any errors, the response map must contain an entry with
key `errors`. The value of this entry is described in the "Errors" section. If
@@ -22,14 +22,40 @@ key `data`. The value of this entry is described in the "Data" section. If the
request failed before execution, due to a syntax error, missing information, or
validation error, this entry must not be present.

When the response of the GraphQL operation is a response stream, the first value
will be the initial response. All subsequent values may contain an `incremental`
entry, containing a list of Defer or Stream payloads.

The `label` and `path` entries on Defer and Stream payloads are used by clients
to identify the `@defer` or `@stream` directive from the GraphQL operation that
triggered this response to be included in an `incremental` entry on a value
returned by the response stream. When a label is provided, the combination of
these two entries will be unique across all Defer and Stream payloads returned
in the response stream.

If the response of the GraphQL operation is a response stream, each response map
must contain an entry with key `hasNext`. The value of this entry is `true` for
all but the last response in the stream. The value of this entry is `false` for
the last response of the stream. This entry must not be present for GraphQL
operations that return a single response map.

The GraphQL service may determine there are no more values in the response
stream after a previous value with `hasNext` equal to `true` has been emitted.
In this case the last value in the response stream should be a map without
`data` and `incremental` entries, and a `hasNext` entry with a value of `false`.

The response map may also contain an entry with key `extensions`. This entry, if
set, must have a map as its value. This entry is reserved for implementors to
extend the protocol however they see fit, and hence there are no additional
restrictions on its contents.
restrictions on its contents. When the response of the GraphQL operation is a
response stream, implementors may send subsequent response maps containing only
`hasNext` and `extensions` entries. Defer and Stream payloads may also contain
an entry with the key `extensions`, also reserved for implementors to extend the
protocol however they see fit.

To ensure future changes to the protocol do not break existing services and
clients, the top level response map must not contain any entries other than the
three described above.
five described above.

Note: When `errors` is present in the response, it may be helpful for it to
appear first when serialized to make it more clear when errors are present in a
@@ -107,14 +133,8 @@ syntax element.
If an error can be associated to a particular field in the GraphQL result, it
must contain an entry with the key `path` that details the path of the response
field which experienced the error. This allows clients to identify whether a
`null` result is intentional or caused by a runtime error.

This field should be a list of path segments starting at the root of the
response and ending with the field associated with the error. Path segments that
represent fields should be strings, and path segments that represent list
indices should be 0-indexed integers. If the error happens in an aliased field,
the path to the error should use the aliased name, since it represents a path in
the response, not in the request.
`null` result is intentional or caused by a runtime error. The value of this
field is described in the [Path](#sec-Path) section.

For example, if fetching one of the friends' names fails in the following
operation:
@@ -244,6 +264,172 @@ discouraged.
}
```

### Incremental Delivery

The `pending` entry in the response is a non-empty list of references to pending
Defer or Stream results. If the response of the GraphQL operation is a response
stream, this field should appear on the initial and possibly subsequent
payloads.

The `incremental` entry in the response is a non-empty list of data fulfilling
Defer or Stream results. If the response of the GraphQL operation is a response
stream, this field may appear on the subsequent payloads.

The `completed` entry in the response is a non-empty list of references to
completed Defer or Stream results.

For example, 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:

Response 1, the initial response does not contain any deferred or streamed
results.

```json example
{
"data": {
"person": {
"name": "Luke Skywalker",
"films": [{ "title": "A New Hope" }]
}
},
"pending": [
{ "path": ["person"], "label": "homeWorldDefer" },
{ "path": ["person", "films"], "label": "filmStream" }
],
"hasNext": true
}
```

Response 2, contains the defer payload and the first stream payload.

```json example
{
"incremental": [
{
"path": ["person"],
"data": { "homeWorld": { "name": "Tatooine" } }
},
{
"path": ["person", "films"],
"items": [{ "title": "The Empire Strikes Back" }]
}
],
"completed": [{ "path": ["person"], "label": "homeWorldDefer" }],
"hasNext": true
}
```

Response 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": [
{
"path": ["person", "films"],
"items": [{ "title": "Return of the Jedi" }]
}
],
"hasNext": true
}
```

Response 4, contains no incremental payloads. {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
{
"completed": [{ "path": ["person", "films"], "label": "filmStream" }],
"hasNext": false
}
```

#### Streamed data

Streamed data may appear as an item in the `incremental` entry of a response.
Streamed data is the result of an associated `@stream` directive in the
operation. A stream payload must contain `items` and `path` entries and may
contain `errors`, and `extensions` entries.

##### Items

The `items` entry in a stream payload is a list of results from the execution of
the associated @stream directive. This output will be a list of the same type of
the field with the associated `@stream` directive. If an error has caused a
`null` to bubble up to a field higher than the list field with the associated
`@stream` directive, then the stream will complete with errors.

#### Deferred data

Deferred data is a map that may appear as an item in the `incremental` entry of
a response. Deferred data is the result of an associated `@defer` directive in
the operation. A defer payload must contain `data` and `path` entries and may
contain `errors`, and `extensions` entries.

##### Data

The `data` entry in a Defer payload will be of the type of a particular field in
the GraphQL result. The adjacent `path` field will contain the path segments of
the field this data is associated with. If an error has caused a `null` to
bubble up to a field higher than the field that contains the fragment with the
associated `@defer` directive, then the fragment will complete with errors.

#### Path

A `path` field allows for the association to a particular field in a GraphQL
result. This field should be a list of path segments starting at the root of the
response and ending with the field to be associated with. Path segments that
represent fields should be strings, and path segments that represent list
indices should be 0-indexed integers. If the path is associated to an aliased
field, the path should use the aliased name, since it represents a path in the
response, not in the request.

When the `path` field is present on a Stream payload, it indicates that the
`items` field represents the partial result of the list field containing the
corresponding `@stream` directive. All but the non-final path segments must
refer to the location of the list field containing the corresponding `@stream`
directive. The final segment of the path list must be a 0-indexed integer. This
integer indicates that this result is set at a range, where the beginning of the
range is at the index of this integer, and the length of the range is the length
of the data.

When the `path` field is present on a Defer payload, it indicates that the
`data` field represents the result of the fragment containing the corresponding
`@defer` directive. The path segments must point to the location of the result
of the field containing the associated `@defer` directive.

When the `path` field is present on an "Error result", it indicates the response
field which experienced the error.

#### Label

Stream and Defer payloads may contain a string field `label`. This `label` is
the same label passed to the `@defer` or `@stream` directive associated with the
response. This allows clients to identify which `@defer` or `@stream` directive
is associated with this value. `label` will not be present if the corresponding
`@defer` or `@stream` directive is not passed a `label` argument.

## Serialization Format

GraphQL does not require a specific serialization format. However, clients