From 20dfd3b28d1ae2553735280eab8a514fc607dc2b Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 18 Aug 2022 17:21:00 -0400 Subject: [PATCH 01/64] Introduce @defer and @stream. Update Section 3 -- Type System.md Update Section 3 -- Type System.md Update Section 3 -- Type System.md Update Section 6 -- Execution.md Update Section 6 -- Execution.md Update Section 6 -- Execution.md Update Section 6 -- Execution.md Update Section 6 -- Execution.md Update Section 6 -- Execution.md Update Section 6 -- Execution.md Update Section 6 -- Execution.md Update Section 6 -- Execution.md Amend changes change initial_count to initialCount add payload fields to Response section add stream validation for overlapping fields spelling updates add note about re-execution add note about final payloads label is optional fix build Update ExecuteQuery with hasNext logic fix spelling fix spaces Update execution to add defer/stream to mutations and subscriptions clarify stream records Apply suggestions from code review Co-authored-by: Benjie Gillam missing bracket Update spec/Section 7 -- Response.md Co-authored-by: Benjie Gillam clarify line about stream record iterator update visitedFragments with defer Updates to consolidate subsequent payload logic for queries, mutations, and subscriptions Apply suggestions from code review Co-authored-by: Benjie Gillam address review feedback Add handling of termination signal more formatting fix spelling Add assertion for record type add "Stream Directives Are Used On List Fields" validation rule Add defaultValue to @stream initialCount Update spec/Section 5 -- Validation.md Co-authored-by: Benjie Gillam # Conflicts: # spec/Section 3 -- Type System.md # spec/Section 5 -- Validation.md # spec/Section 6 -- Execution.md # spec/Section 7 -- Response.md --- cspell.yml | 2 + spec/Section 3 -- Type System.md | 63 +++++- spec/Section 5 -- Validation.md | 37 ++++ spec/Section 6 -- Execution.md | 334 +++++++++++++++++++++++++++---- spec/Section 7 -- Response.md | 67 +++++-- 5 files changed, 451 insertions(+), 52 deletions(-) diff --git a/cspell.yml b/cspell.yml index e8aa73355..66902e41a 100644 --- a/cspell.yml +++ b/cspell.yml @@ -4,12 +4,14 @@ ignoreRegExpList: - /[a-z]{2,}'s/ words: # Terms of art + - deprioritization - endianness - interoperation - monospace - openwebfoundation - parallelization - structs + - sublist - subselection # Fictional characters / examples - alderaan diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index d32b08566..d1c45e1a5 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -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 @@ -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 they 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. @@ -2162,3 +2168,56 @@ 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 +) 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 + } +} +``` + +### @stream + +```graphql +directive @stream(label: String, initialCount: Int = 0, if: Boolean) 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) + } + } +} +``` diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 2000c324c..1c5729e6b 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -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 @@ -1517,6 +1528,32 @@ query ($foo: Boolean = true, $bar: Boolean = false) { } ``` +### 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 diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f357069f9..c415e4882 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -30,13 +30,21 @@ request is determined by the result of executing this operation according to the "Executing Operations” section below. ExecuteRequest(schema, document, operationName, variableValues, initialValue): +Note: the execution assumes implementing language supports coroutines. +Alternatively, the socket can provide a write buffer pointer to allow +{ExecuteRequest()} to directly write payloads into the buffer. - Let {operation} be the result of {GetOperation(document, operationName)}. - Let {coercedVariableValues} be the result of {CoerceVariableValues(schema, operation, variableValues)}. - If {operation} is a query operation: - - Return {ExecuteQuery(operation, schema, coercedVariableValues, - initialValue)}. + - Let {executionResult} be the result of calling {ExecuteQuery(operation, + schema, coercedVariableValues, initialValue, subsequentPayloads)}. + - If {executionResult} is an iterator: + - For each {payload} in {executionResult}: + - Yield {payload}. + - Otherwise: + - Return {executionResult}. - Otherwise if {operation} is a mutation operation: - Return {ExecuteMutation(operation, schema, coercedVariableValues, initialValue)}. @@ -128,15 +136,28 @@ An initial value may be provided when executing a query operation. ExecuteQuery(query, schema, variableValues, initialValue): +- Let {subsequentPayloads} be an empty list. - Let {queryType} be the root Query type in {schema}. - Assert: {queryType} is an Object type. - Let {selectionSet} be the top level Selection Set in {query}. - Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - queryType, initialValue, variableValues)} _normally_ (allowing - parallelization). + queryType, initialValue, variableValues, subsequentPayloads)} _normally_ + (allowing parallelization). - Let {errors} be the list of all _field error_ raised while executing the selection set. -- Return an unordered map containing {data} and {errors}. +- If {subsequentPayloads} is empty: + - Return an unordered map containing {data} and {errors}. +- If {subsequentPayloads} is not empty: + - Yield an unordered map containing {data}, {errors}, and an entry named + {hasNext} with the value {true}. + - Let {iterator} be the result of running + {YieldSubsequentPayloads(subsequentPayloads)}. + - For each {payload} yielded by {iterator}: + - If a termination signal is received: + - Send a termination signal to {iterator}. + - Return. + - Otherwise: + - Yield {payload}. ### Mutation @@ -150,14 +171,27 @@ mutations ensures against race conditions during these side-effects. ExecuteMutation(mutation, schema, variableValues, initialValue): +- Let {subsequentPayloads} be an empty list. - Let {mutationType} be the root Mutation type in {schema}. - Assert: {mutationType} is an Object type. - Let {selectionSet} be the top level Selection Set in {mutation}. - Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - mutationType, initialValue, variableValues)} _serially_. + mutationType, initialValue, variableValues, subsequentPayloads)} _serially_. - Let {errors} be the list of all _field error_ raised while executing the selection set. -- Return an unordered map containing {data} and {errors}. +- If {subsequentPayloads} is empty: + - Return an unordered map containing {data} and {errors}. +- If {subsequentPayloads} is not empty: + - Yield an unordered map containing {data}, {errors}, and an entry named + {hasNext} with the value {true}. + - Let {iterator} be the result of running + {YieldSubsequentPayloads(subsequentPayloads)}. + - For each {payload} yielded by {iterator}: + - If a termination signal is received: + - Send a termination signal to {iterator}. + - Return. + - Otherwise: + - Yield {payload}. ### Subscription @@ -290,22 +324,36 @@ MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues): - Return a new event stream {responseStream} which yields events as follows: - For each {event} on {sourceStream}: - - Let {response} be the result of running + - Let {executionResult} be the result of running {ExecuteSubscriptionEvent(subscription, schema, variableValues, event)}. - - Yield an event containing {response}. + - For each {response} yielded by {executionResult}: + - Yield an event containing {response}. - When {responseStream} completes: complete this event stream. ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue): +- Let {subsequentPayloads} be an empty list. - Let {subscriptionType} be the root Subscription type in {schema}. - Assert: {subscriptionType} is an Object type. - Let {selectionSet} be the top level Selection Set in {subscription}. - Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - subscriptionType, initialValue, variableValues)} _normally_ (allowing - parallelization). + subscriptionType, initialValue, variableValues, subsequentPayloads)} + _normally_ (allowing parallelization). - Let {errors} be the list of all _field error_ raised while executing the selection set. -- Return an unordered map containing {data} and {errors}. +- If {subsequentPayloads} is empty: + - Return an unordered map containing {data} and {errors}. +- If {subsequentPayloads} is not empty: + - Yield an unordered map containing {data}, {errors}, and an entry named + {hasNext} with the value {true}. + - Let {iterator} be the result of running + {YieldSubsequentPayloads(subsequentPayloads)}. + - For each {payload} yielded by {iterator}: + - If a termination signal is received: + - Send a termination signal to {iterator}. + - Return. + - Otherwise: + - Yield {payload}. Note: The {ExecuteSubscriptionEvent()} algorithm is intentionally similar to {ExecuteQuery()} since this is how each event result is produced. @@ -321,6 +369,44 @@ Unsubscribe(responseStream): - Cancel {responseStream} +## Yield Subsequent Payloads + +If an operation contains subsequent payload records resulting from `@stream` or +`@defer` directives, the {YieldSubsequentPayloads} algorithm defines how the +payloads should be processed. + +YieldSubsequentPayloads(subsequentPayloads): + +- While {subsequentPayloads} is not empty: +- If a termination signal is received: + - For each {record} in {subsequentPayloads}: + - If {record} is a Stream Record: + - Let {iterator} be the correspondent fields on the Stream Record + structure. + - Send a termination signal to {iterator}. + - Return. +- Let {record} be the first complete item in {subsequentPayloads}. + - Remove {record} from {subsequentPayloads}. + - Assert: {record} must be a Deferred Fragment Record or a Stream Record. + - If {record} is a Deferred Fragment Record: + - Let {payload} be the result of running + {ResolveDeferredFragmentRecord(record, variableValues, + subsequentPayloads)}. + - If {record} is a Stream Record: + - Let {payload} be the result of running {ResolveStreamRecord(record, + variableValues, subsequentPayloads)}. + - If {payload} is {null}: + - If {subsequentPayloads} is empty: + - Yield a map containing a field `hasNext` with the value {false}. + - Return. + - If {subsequentPayloads} is not empty: + - Continue to the next record in {subsequentPayloads}. + - If {record} is not the final element in {subsequentPayloads} + - Add an entry to {payload} named `hasNext` with the value {true}. + - If {record} is the final element in {subsequentPayloads} + - Add an entry to {payload} named `hasNext` with the value {false}. + - Yield {payload} + ## Executing Selection Sets To execute a selection set, the object value being evaluated and the object type @@ -331,10 +417,13 @@ First, the selection set is turned into a grouped field set; then, each represented field in the grouped field set produces an entry into a response map. -ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): +ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues, +subsequentPayloads, parentPath): -- Let {groupedFieldSet} be the result of {CollectFields(objectType, - selectionSet, variableValues)}. +- If {subsequentPayloads} is not provided, initialize it to the empty set. +- If {parentPath} is not provided, initialize it to an empty list. +- Let {groupedFieldSet} be the result of {CollectFields(objectType, objectValue, + selectionSet, variableValues, subsequentPayloads, parentPath)}. - Initialize {resultMap} to an empty ordered map. - For each {groupedFieldSet} as {responseKey} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value @@ -343,7 +432,7 @@ ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues): {objectType}. - If {fieldType} is defined: - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, - fields, variableValues)}. + fields, variableValues, subsequentPayloads, parentPath)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. - Return {resultMap}. @@ -464,7 +553,13 @@ Before execution, the selection set is converted to a grouped field set by calling {CollectFields()}. Each entry in the grouped field set is a list of fields that share a response key (the alias if defined, otherwise the field name). This ensures all fields with the same response key (including those in -referenced fragments) are executed at the same time. +referenced fragments) are executed at the same time. A deferred selection set's +fields will not be included in the grouped field set. Rather, a record +representing the deferred fragment and additional context will be stored in a +list. The executor revisits and resumes execution for the list of deferred +fragment records after the initial execution is initiated. This deferred +execution would ‘re-execute’ fields with the same response key that were present +in the grouped field set. As an example, collecting the fields of this selection set would collect two instances of the field `a` and one of field `b`: @@ -489,7 +584,8 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. -CollectFields(objectType, selectionSet, variableValues, visitedFragments): +CollectFields(objectType, objectValue, selectionSet, variableValues, +deferredFragments, parentPath, visitedFragments): - If {visitedFragments} is not provided, initialize it to the empty set. - Initialize {groupedFields} to an empty ordered map of lists. @@ -512,8 +608,12 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - Append {selection} to the {groupForResponseKey}. - If {selection} is a {FragmentSpread}: - Let {fragmentSpreadName} be the name of {selection}. - - If {fragmentSpreadName} is in {visitedFragments}, continue with the next - {selection} in {selectionSet}. + - If {fragmentSpreadName} provides the directive `@defer` and it's {if} + argument is {true} or is a variable in {variableValues} with the value + {true}: + - Let {deferDirective} be that directive. + - If {fragmentSpreadName} is in {visitedFragments} and {deferDirective} is + not defined, continue with the next {selection} in {selectionSet}. - Add {fragmentSpreadName} to {visitedFragments}. - Let {fragment} be the Fragment in the current Document whose name is {fragmentSpreadName}. @@ -523,6 +623,12 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - If {DoesFragmentTypeApply(objectType, fragmentType)} is false, continue with the next {selection} in {selectionSet}. - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. + - If {deferDirective} is defined: + - Let {deferredFragment} be the result of calling + {DeferFragment(objectType, objectValue, fragmentSelectionSet, + parentPath)}. + - Append {deferredFragment} to {deferredFragments}. + - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, visitedFragments)}. @@ -538,9 +644,18 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): fragmentType)} is false, continue with the next {selection} in {selectionSet}. - Let {fragmentSelectionSet} be the top-level selection set of {selection}. + - If {InlineFragment} provides the directive `@defer`, let {deferDirective} + be that directive. + - If {deferDirective}'s {if} argument is {true} or is a variable in + {variableValues} with the value {true}: + - Let {deferredFragment} be the result of calling + {DeferFragment(objectType, objectValue, fragmentSelectionSet, + parentPath)}. + - Append {deferredFragment} to {deferredFragments}. + - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments)}. + visitedFragments, parentPath)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -549,6 +664,9 @@ CollectFields(objectType, selectionSet, variableValues, visitedFragments): - Append all items in {fragmentGroup} to {groupForResponseKey}. - Return {groupedFields}. +Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` +directives may be applied in either order since they apply commutatively. + DoesFragmentTypeApply(objectType, fragmentType): - If {fragmentType} is an Object Type: @@ -561,8 +679,47 @@ DoesFragmentTypeApply(objectType, fragmentType): - if {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. -Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` -directives may be applied in either order since they apply commutatively. +DeferFragment(objectType, objectValue, fragmentSelectionSet, parentPath): + +- Let {label} be the value or the variable to {deferDirective}'s {label} + argument. +- Let {deferredFragmentRecord} be the result of calling + {CreateDeferredFragmentRecord(label, objectType, objectValue, + fragmentSelectionSet, parentPath)}. +- return {deferredFragmentRecord}. + +#### Deferred Fragment Record + +Let {deferredFragmentRecord} be an inline fragment or fragment spread with +`@defer` provided. + +Deferred Fragment Record is a structure containing: + +- {label}: value derived from the `@defer` directive. +- {objectType}: of the {deferredFragmentRecord}. +- {objectValue}: of the {deferredFragmentRecord}. +- {fragmentSelectionSet}: the top level selection set of + {deferredFragmentRecord}. +- {path}: a list of field names and indices from root to + {deferredFragmentRecord}. + +CreateDeferredFragmentRecord(label, objectType, objectValue, +fragmentSelectionSet, path): + +- If {path} is not provided, initialize it to an empty list. +- Construct a deferred fragment record based on the parameters passed in. + +ResolveDeferredFragmentRecord(deferredFragmentRecord, variableValues, +subsequentPayloads): + +- Let {label}, {objectType}, {objectValue}, {fragmentSelectionSet}, {path} be + the corresponding fields in the deferred fragment record structure. +- Let {payload} be the result of calling + {ExecuteSelectionSet(fragmentSelectionSet, objectType, objectValue, + variableValues, subsequentPayloads, path)}. +- Add an entry to {payload} named `label` with the value {label}. +- Add an entry to {payload} named `path` with the value {path}. +- Return {payload}. ## Executing Fields @@ -572,16 +729,29 @@ coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value. -ExecuteField(objectType, objectValue, fieldType, fields, variableValues): +ExecuteField(objectType, objectValue, fieldType, fields, variableValues, +subsequentPayloads, parentPath): - Let {field} be the first entry in {fields}. - Let {fieldName} be the field name of {field}. - Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} +- If {field} provides the directive `@stream`, let {streamDirective} be that + directive. + - Let {initialCount} be the value or variable provided to {streamDirective}'s + {initialCount} argument. + - Let {resolvedValue} be {ResolveFieldGenerator(objectType, objectValue, + fieldName, argumentValues, initialCount)}. + - Let {result} be the result of calling {CompleteValue(fieldType, fields, + resolvedValue, variableValues, subsequentPayloads, parentPath)}. + - Append {fieldName} to the {path} field of every {subsequentPayloads}. + - Return {result}. - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. -- Return the result of {CompleteValue(fieldType, fields, resolvedValue, - variableValues)}. +- Let {result} be the result of calling {CompleteValue(fieldType, fields, + resolvedValue, variableValues, subsequentPayloads)}. +- Append {fieldName} to the {path} for every {subsequentPayloads}. +- Return {result}. ### Coercing Field Arguments @@ -644,11 +814,20 @@ must only allow usage of variables of appropriate types. While nearly all of GraphQL execution can be described generically, ultimately the internal system exposing the GraphQL interface must provide values. This is exposed via {ResolveFieldValue}, which produces a value for a given field on a -type for a real value. +type for a real value. In addition, {ResolveFieldGenerator} will be exposed to +produce an iterator for a field with `List` return type. The internal system may +optionally define a generator function. In the case where the generator is not +defined, the GraphQL executor provides a default generator. For example, a +trivial generator that yields the entire list upon the first iteration. + +As an example, a {ResolveFieldValue} might accept the {objectType} `Person`, the +{field} {"soulMate"}, and the {objectValue} representing John Lennon. It would +be expected to yield the value representing Yoko Ono. -As an example, this might accept the {objectType} `Person`, the {field} -{"soulMate"}, and the {objectValue} representing John Lennon. It would be -expected to yield the value representing Yoko Ono. +A {ResolveFieldGenerator} might accept the {objectType} `MusicBand`, the {field} +{"members"}, and the {objectValue} representing Beatles. It would be expected to +yield a iterator of values representing, John Lennon, Paul McCartney, Ringo +Starr and George Harrison. ResolveFieldValue(objectType, objectValue, fieldName, argumentValues): @@ -657,18 +836,74 @@ ResolveFieldValue(objectType, objectValue, fieldName, argumentValues): - Return the result of calling {resolver}, providing {objectValue} and {argumentValues}. +ResolveFieldGenerator(objectType, objectValue, fieldName, argumentValues, +initialCount): + +- If {objectType} provide an internal function {generatorResolver} for + generating partially resolved value of a list field named {fieldName}: + - Let {generatorResolver} be the internal function. + - Return the iterator from calling {generatorResolver}, providing + {objectValue}, {argumentValues} and {initialCount}. +- Create {generator} from {ResolveFieldValue(objectType, objectValue, fieldName, + argumentValues)}. +- Return {generator}. + Note: It is common for {resolver} to be asynchronous due to relying on reading an underlying database or networked service to produce a value. This necessitates the rest of a GraphQL executor to handle an asynchronous execution -flow. +flow. In addition, a common implementation of {generator} is to leverage +asynchronous iterators or asynchronous generators provided by many programming +languages. ### Value Completion After resolving the value for a field, it is completed by ensuring it adheres to the expected return type. If the return type is another Object type, then the -field execution process continues recursively. - -CompleteValue(fieldType, fields, result, variableValues): +field execution process continues recursively. In the case where a value +returned for a list type field is an iterator due to `@stream` specified on the +field, value completion iterates over the iterator until the number of items +yield by the iterator satisfies `initialCount` specified on the `@stream` +directive. Unresolved items in the iterator will be stored in a stream record +which the executor resumes to execute after the initial execution finishes. + +#### Stream Record + +Let {streamField} be a list field with a `@stream` directive provided. + +A Stream Record is a structure containing: + +- {label}: value derived from the `@stream` directive's `label` argument. +- {iterator}: created by {ResolveFieldGenerator}. +- {resolvedItems}: items resolved from the {iterator} but not yet delivered. +- {index}: indicating the position of the item in the complete list. +- {path}: a list of field names and indices from root to {streamField}. +- {fields}: the group of fields grouped by CollectFields() for {streamField}. +- {innerType}: inner type of {streamField}'s type. + +CreateStreamRecord(label, initialCount, iterator, resolvedItems, index, fields, +innerType): + +- Construct a stream record based on the parameters passed in. + +ResolveStreamRecord(streamRecord, variableValues, subsequentPayloads): + +- Let {label}, {iterator}, {resolvedItems}, {index}, {path}, {fields}, + {innerType} be the correspondent fields on the Stream Record structure. +- Wait for the next item from {iterator}. +- If an item is not retrieved because {iterator} has completed: + - Return {null} +- Let {item} be the item retrieved from {iterator}. +- Append {index} to {path}. +- Increment {index}. +- Let {payload} be the result of calling CompleteValue(innerType, fields, item, + variableValues, subsequentPayloads, path)}. +- Add an entry to {payload} named `label` with the value {label}. +- Add an entry to {payload} named `path` with the value {path}. +- Append {streamRecord} to {subsequentPayloads}. +- Return {payload}. + +CompleteValue(fieldType, fields, result, variableValues, subsequentPayloads, +parentPath): - If the {fieldType} is a Non-Null type: - Let {innerType} be the inner type of {fieldType}. @@ -679,11 +914,34 @@ CompleteValue(fieldType, fields, result, variableValues): - If {result} is {null} (or another internal value similar to {null} such as {undefined}), return {null}. - If {fieldType} is a List type: + - If {result} is an iterator: + - Let {field} be the first entry in {fields}. + - Let {innerType} be the inner type of {fieldType}. + - Let {streamDirective} be the `@stream` directive provided on {field}. + - Let {initialCount} be the value or variable provided to + {streamDirective}'s {initialCount} argument. + - Let {label} be the value or variable provided to {streamDirective}'s + {label} argument. + - Let {resolvedItems} be an empty list + - For each {members} in {result}: + - Append all items from {members} to {resolvedItems}. + - If the length of {resolvedItems} is greater or equal to {initialCount}: + - Let {initialItems} be the sublist of the first {initialCount} items + from {resolvedItems}. + - Let {remainingItems} be the sublist of the items in {resolvedItems} + after the first {initialCount} items. + - Let {streamRecord} be the result of calling {CreateStreamRecord(label, + initialCount, result, remainingItems, initialCount, fields, innerType, + parentPath)} + - Append {streamRecord} to {subsequentPayloads}. + - Let {result} be {initialItems}. + - Exit for each loop. - If {result} is not a collection of values, raise a _field error_. - Let {innerType} be the inner type of {fieldType}. - Return a list where each list item is the result of calling - {CompleteValue(innerType, fields, resultItem, variableValues)}, where - {resultItem} is each item in {result}. + {CompleteValue(innerType, fields, resultItem, variableValues, + subsequentPayloads, parentPath)}, where {resultItem} is each item in + {result}. - If {fieldType} is a Scalar or Enum type: - Return the result of {CoerceResult(fieldType, result)}. - If {fieldType} is an Object, Interface, or Union type: @@ -693,8 +951,8 @@ CompleteValue(fieldType, fields, result, variableValues): - Let {objectType} be {ResolveAbstractType(fieldType, result)}. - Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}. - Return the result of evaluating {ExecuteSelectionSet(subSelectionSet, - objectType, result, variableValues)} _normally_ (allowing for - parallelization). + objectType, result, variableValues, subsequentPayloads, parentPath)} + _normally_ (allowing for parallelization). **Coercing Results** diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 8dcd9234c..db28604eb 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -10,11 +10,11 @@ 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 an event 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 -the request completed without raising any errors, this entry must not be +If the operation encountered any errors, the response map must contain an entry +with key `errors`. The value of this entry is described in the "Errors" section. +If the request completed without raising any errors, this entry must not be present. If the request included execution, the response map must contain an entry with @@ -22,6 +22,24 @@ 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 an event stream, the first value +will be the initial response. All subsequent values may contain `label` and +`path` entries. These two entries are used by clients to identify the the +`@defer` or `@stream` directive from the GraphQL operation that triggered this +value to be returned by the event stream. The combination of these two entries +must be unique across all values returned by the event stream. + +If the response of the GraphQL operation is an event 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 is not required for GraphQL +operations that return a single response map. + +The GraphQL server may determine there are no more values in the event stream +after a previous value with `hasNext` equal to `true` has been emitted. In this +case the last value in the event stream should be a map without `data`, `label`, +and `path` 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 @@ -42,6 +60,11 @@ requested operation. If the operation was a query, this output will be an object of the query root operation type; if the operation was a mutation, this output will be an object of the mutation root operation type. +If the result of the operation is an event stream, the `data` entry in +subsequent values will be an object 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 was raised before execution begins, the `data` entry should not be present in the result. @@ -107,14 +130,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" section. For example, if fetching one of the friends' names fails in the following operation: @@ -244,6 +261,32 @@ discouraged. } ``` +## 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 GraphQL response, it indicates that the +`data` field is not the root query or mutation result, but is rather associated +to a particular field in the root result. + +When the `path` field is present on an "Error result", it indicates the response +field which experienced the error. + +## Label + +If the response of the GraphQL operation is an event stream, subsequent values +may contain a string field `label`. This `label` is the same label passed to the +`@defer` or `@stream` directive that triggered this value. 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 From 3f00596b0cf460f08ccbdc68aa79184a9251da97 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 17 Feb 2021 13:03:38 -0500 Subject: [PATCH 02/64] fix typos # Conflicts: # spec/Section 6 -- Execution.md --- spec/Section 6 -- Execution.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index c415e4882..39f2dfd40 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -826,8 +826,8 @@ be expected to yield the value representing Yoko Ono. A {ResolveFieldGenerator} might accept the {objectType} `MusicBand`, the {field} {"members"}, and the {objectValue} representing Beatles. It would be expected to -yield a iterator of values representing, John Lennon, Paul McCartney, Ringo -Starr and George Harrison. +yield a iterator of values representing John Lennon, Paul McCartney, Ringo Starr +and George Harrison. ResolveFieldValue(objectType, objectValue, fieldName, argumentValues): From c861c36446cd10a23d0f0b37e995a2ac7c6f2760 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 17 Feb 2021 13:06:20 -0500 Subject: [PATCH 03/64] clear up that it is legal to support either defer or stream individually # Conflicts: # spec/Section 3 -- Type System.md --- spec/Section 3 -- Type System.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index d1c45e1a5..cb231a086 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1944,9 +1944,9 @@ 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 they 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. +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 From d56f4e4892f278c883e0f2a63b539123122583a1 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 17 Feb 2021 13:12:04 -0500 Subject: [PATCH 04/64] Add sumary of arguments to Type System --- spec/Section 3 -- Type System.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index cb231a086..3fff10d6d 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2201,6 +2201,15 @@ fragment someFragment on User { } ``` +#### @defer Arguments + +- `if: Boolean` - When true, fragment may be deferred. If omitted, defaults to + `true`. +- `label: String` - A unique label across all `@defer` and `@stream` directives + in an operation. This label should be used by GraphQL clients to identify the + data from patch responses and associate it with the correct fragments. If + provided, the GraphQL Server must add it to the payload. + ### @stream ```graphql @@ -2221,3 +2230,14 @@ query myQuery($shouldStream: Boolean) { } } ``` + +#### @stream Arguments + +- `if: Boolean` - When true, field may be streamed. If omitted, defaults to + `true`. +- `label: String` - A unique label across all `@defer` and `@stream` directives + in an operation. This label should be used by GraphQL clients to identify the + data from patch responses and associate it with the correct fragments. If + provided, the GraphQL Server must add it to the payload. +- `initialCount: Int` - The number of list items the server should return as + part of the initial response. If omitted, defaults to `0`. From b284063a0af200c7876d09cf73b85bad434a7088 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Sat, 15 May 2021 11:24:10 -0400 Subject: [PATCH 05/64] Update Section 3 -- Type System.md # Conflicts: # spec/Section 3 -- Type System.md --- spec/Section 3 -- Type System.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 3fff10d6d..f6091d8fb 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2203,8 +2203,9 @@ fragment someFragment on User { #### @defer Arguments -- `if: Boolean` - When true, fragment may be deferred. If omitted, defaults to - `true`. +- `if: Boolean` - When `true`, fragment may be deferred. When `false`, fragment + will not be deferred and data will be included in the initial response. If + omitted, defaults to `true`. - `label: String` - A unique label across all `@defer` and `@stream` directives in an operation. This label should be used by GraphQL clients to identify the data from patch responses and associate it with the correct fragments. If @@ -2233,8 +2234,9 @@ query myQuery($shouldStream: Boolean) { #### @stream Arguments -- `if: Boolean` - When true, field may be streamed. If omitted, defaults to - `true`. +- `if: Boolean` - When `true`, field may be streamed. When `false`, the field + will not be streamed and all list items will be included in the initial + response. If omitted, defaults to `true`. - `label: String` - A unique label across all `@defer` and `@stream` directives in an operation. This label should be used by GraphQL clients to identify the data from patch responses and associate it with the correct fragments. If From 135900221da2eb49443f7bcfb9bb5d1171439ce9 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 19 Nov 2021 15:41:51 -0500 Subject: [PATCH 06/64] clarification on defer/stream requirement # Conflicts: # spec/Section 3 -- Type System.md --- spec/Section 3 -- Type System.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index f6091d8fb..47af2af1e 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2203,9 +2203,9 @@ fragment someFragment on User { #### @defer Arguments -- `if: Boolean` - When `true`, fragment may be deferred. When `false`, fragment - will not be deferred and data will be included in the initial response. If - omitted, defaults to `true`. +- `if: Boolean` - When `true`, fragment _should_ be deferred. When `false`, + fragment will not be deferred and data will be included in the initial + response. If omitted, defaults to `true`. - `label: String` - A unique label across all `@defer` and `@stream` directives in an operation. This label should be used by GraphQL clients to identify the data from patch responses and associate it with the correct fragments. If @@ -2234,8 +2234,8 @@ query myQuery($shouldStream: Boolean) { #### @stream Arguments -- `if: Boolean` - When `true`, field may be streamed. When `false`, the field - will not be streamed and all list items will be included in the initial +- `if: Boolean` - When `true`, field _should_ be streamed. When `false`, the + field will not be streamed and all list items will be included in the initial response. If omitted, defaults to `true`. - `label: String` - A unique label across all `@defer` and `@stream` directives in an operation. This label should be used by GraphQL clients to identify the @@ -2243,3 +2243,15 @@ query myQuery($shouldStream: Boolean) { provided, the GraphQL Server must add it to the payload. - `initialCount: Int` - The number of list items the server should return as part of the initial response. If omitted, defaults to `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 servers honor the `@defer` and `@stream` +directives on each execution. However, the specification allows advanced +use-cases where the server 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. From ce89ed0d9c1d0d0bda764d1d322e0f66c45aa193 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Sat, 20 Nov 2021 11:58:41 -0500 Subject: [PATCH 07/64] clarify negative values of initialCount # Conflicts: # spec/Section 3 -- Type System.md --- spec/Section 3 -- Type System.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 47af2af1e..d0fc4c372 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2242,7 +2242,8 @@ query myQuery($shouldStream: Boolean) { data from patch responses and associate it with the correct fragments. If provided, the GraphQL Server must add it to the payload. - `initialCount: Int` - The number of list items the server should return as - part of the initial response. If omitted, defaults to `0`. + part of the initial response. If omitted, defaults to `0`. If the value of + this argument is less than `0`, it is treated the same as `0`. Note: The ability to defer and/or stream parts of a response can have a potentially significant impact on application performance. Developers generally From 8297aff7842149490ea9023b5f62e7581aaca50c Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 25 Nov 2021 11:06:48 -0500 Subject: [PATCH 08/64] allow extensions only subsequent payloads # Conflicts: # spec/Section 7 -- Response.md --- spec/Section 7 -- Response.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index db28604eb..19b073161 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -43,7 +43,9 @@ and `path` 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 an +event stream, implementors may send subsequent payloads containing only +`hasNext` and `extensions` entries. 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 From 67351bd11a677940968ac4319a865a863ea18e3c Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 26 Nov 2021 09:25:24 -0500 Subject: [PATCH 09/64] fix typo # Conflicts: # spec/Section 7 -- Response.md --- spec/Section 7 -- Response.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 19b073161..353f40d2c 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -24,10 +24,10 @@ validation error, this entry must not be present. When the response of the GraphQL operation is an event stream, the first value will be the initial response. All subsequent values may contain `label` and -`path` entries. These two entries are used by clients to identify the the -`@defer` or `@stream` directive from the GraphQL operation that triggered this -value to be returned by the event stream. The combination of these two entries -must be unique across all values returned by the event stream. +`path` entries. These two entries are used by clients to identify the `@defer` +or `@stream` directive from the GraphQL operation that triggered this value to +be returned by the event stream. The combination of these two entries must be +unique across all values returned by the event stream. If the response of the GraphQL operation is an event stream, each response map must contain an entry with key `hasNext`. The value of this entry is `true` for From 5365ed4f48877e0387045c362db3aa5c14b21935 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 18 Aug 2022 17:27:07 -0400 Subject: [PATCH 10/64] Raise a field error if initialCount is less than zero # Conflicts: # spec/Section 3 -- Type System.md # spec/Section 6 -- Execution.md --- spec/Section 3 -- Type System.md | 4 ++-- spec/Section 6 -- Execution.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index d0fc4c372..4a25974b0 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2242,8 +2242,8 @@ query myQuery($shouldStream: Boolean) { data from patch responses and associate it with the correct fragments. If provided, the GraphQL Server must add it to the payload. - `initialCount: Int` - The number of list items the server should return as - part of the initial response. If omitted, defaults to `0`. If the value of - this argument is less than `0`, it is treated the same as `0`. + 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 diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 39f2dfd40..1c2b8d5b3 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -920,6 +920,7 @@ parentPath): - Let {streamDirective} be the `@stream` directive provided on {field}. - Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument. + - If {initialCount} is less than zero, raise a _field error_. - Let {label} be the value or variable provided to {streamDirective}'s {label} argument. - Let {resolvedItems} be an empty list From 947b944c0d711a66692a81ee5632cfce40cbf441 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 6 Dec 2021 13:15:13 -0500 Subject: [PATCH 11/64] data is not necessarily an object in subsequent payloads # Conflicts: # spec/Section 7 -- Response.md --- spec/Section 7 -- Response.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 353f40d2c..47ee7e893 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -63,9 +63,9 @@ of the query root operation type; if the operation was a mutation, this output will be an object of the mutation root operation type. If the result of the operation is an event stream, the `data` entry in -subsequent values will be an object 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. +subsequent values 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 was raised before execution begins, the `data` entry should not be present in the result. From 23ed896393df4715ce56ddc001d46b827996d9cf Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 6 Dec 2021 17:20:37 -0500 Subject: [PATCH 12/64] add Defer And Stream Directives Are Used On Valid Root Field rule --- spec/Section 5 -- Validation.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 1c5729e6b..44b8b9d26 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1528,6 +1528,34 @@ 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 + } +} +``` + ### Stream Directives Are Used On List Fields **Formal Specification** From cb2330f178c659e1a1a3f112edbd8ea0ed61b9e2 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 18 Aug 2022 17:21:50 -0400 Subject: [PATCH 13/64] wait for parent async record to ensure correct order of payloads # Conflicts: # spec/Section 6 -- Execution.md --- cspell.yml | 1 - spec/Section 6 -- Execution.md | 192 ++++++++++++++++++--------------- 2 files changed, 108 insertions(+), 85 deletions(-) diff --git a/cspell.yml b/cspell.yml index 66902e41a..8bc4a231c 100644 --- a/cspell.yml +++ b/cspell.yml @@ -11,7 +11,6 @@ words: - openwebfoundation - parallelization - structs - - sublist - subselection # Fictional characters / examples - alderaan diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 1c2b8d5b3..900f6b230 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -417,13 +417,13 @@ First, the selection set is turned into a grouped field set; then, each represented field in the grouped field set produces an entry into a response map. -ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues, -subsequentPayloads, parentPath): +ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues, path, +subsequentPayloads, asyncRecord): +- If {path} is not provided, initialize it to an empty list. - If {subsequentPayloads} is not provided, initialize it to the empty set. -- If {parentPath} is not provided, initialize it to an empty list. - Let {groupedFieldSet} be the result of {CollectFields(objectType, objectValue, - selectionSet, variableValues, subsequentPayloads, parentPath)}. + selectionSet, variableValues, path subsequentPayloads, asyncRecord)}. - Initialize {resultMap} to an empty ordered map. - For each {groupedFieldSet} as {responseKey} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value @@ -432,7 +432,7 @@ subsequentPayloads, parentPath): {objectType}. - If {fieldType} is defined: - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, - fields, variableValues, subsequentPayloads, parentPath)}. + fields, variableValues, path, subsequentPayloads, asyncRecord)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. - Return {resultMap}. @@ -584,8 +584,8 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. -CollectFields(objectType, objectValue, selectionSet, variableValues, -deferredFragments, parentPath, visitedFragments): +CollectFields(objectType, objectValue, selectionSet, variableValues, path, +subsequentPayloads, asyncRecord, visitedFragments): - If {visitedFragments} is not provided, initialize it to the empty set. - Initialize {groupedFields} to an empty ordered map of lists. @@ -624,14 +624,16 @@ deferredFragments, parentPath, visitedFragments): with the next {selection} in {selectionSet}. - Let {fragmentSelectionSet} be the top-level selection set of {fragment}. - If {deferDirective} is defined: - - Let {deferredFragment} be the result of calling - {DeferFragment(objectType, objectValue, fragmentSelectionSet, - parentPath)}. - - Append {deferredFragment} to {deferredFragments}. + - Let {label} be the value or the variable to {deferDirective}'s {label} + argument. + - Let {deferredFragmentRecord} be the result of calling + {CreateDeferredFragmentRecord(label, objectType, objectValue, + fragmentSelectionSet, path, asyncRecord)}. + - Append {deferredFragmentRecord} to {subsequentPayloads}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments)}. + {CollectFields(objectType, objectValue, fragmentSelectionSet, + variableValues, path, subsequentPayloads, asyncRecord, visitedFragments)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -648,14 +650,16 @@ deferredFragments, parentPath, visitedFragments): be that directive. - If {deferDirective}'s {if} argument is {true} or is a variable in {variableValues} with the value {true}: - - Let {deferredFragment} be the result of calling - {DeferFragment(objectType, objectValue, fragmentSelectionSet, - parentPath)}. - - Append {deferredFragment} to {deferredFragments}. + - Let {label} be the value or the variable to {deferDirective}'s {label} + argument. + - Let {deferredFragmentRecord} be the result of calling + {CreateDeferredFragmentRecord(label, objectType, objectValue, + fragmentSelectionSet, path, asyncRecord)}. + - Append {deferredFragmentRecord} to {subsequentPayloads}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, parentPath)}. + {CollectFields(objectType, objectValue, fragmentSelectionSet, + variableValues, path, subsequentPayloads, asyncRecord, visitedFragments)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -679,44 +683,53 @@ DoesFragmentTypeApply(objectType, fragmentType): - if {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. -DeferFragment(objectType, objectValue, fragmentSelectionSet, parentPath): +#### Async Payload Record + +An Async Payload Record is either a Deferred Fragment Record or a Stream Record. +All Async Payload Records are structures containing: -- Let {label} be the value or the variable to {deferDirective}'s {label} - argument. -- Let {deferredFragmentRecord} be the result of calling - {CreateDeferredFragmentRecord(label, objectType, objectValue, - fragmentSelectionSet, parentPath)}. -- return {deferredFragmentRecord}. +- {label}: value derived from the corresponding `@defer` or `@stream` directive. +- {parentRecord}: optionally an Async Payload Record. +- {errors}: a list of field errors encountered during execution. +- {dataExecution}: A result that can notify when the corresponding execution has + completed. +- {path}: a list of field names and indices from root to the location of the + corresponding `@defer` or `@stream` directive. #### Deferred Fragment Record Let {deferredFragmentRecord} be an inline fragment or fragment spread with `@defer` provided. -Deferred Fragment Record is a structure containing: +Deferred Fragment Record is a structure containing all the entries of Async +Payload Record, and additionally: -- {label}: value derived from the `@defer` directive. - {objectType}: of the {deferredFragmentRecord}. - {objectValue}: of the {deferredFragmentRecord}. - {fragmentSelectionSet}: the top level selection set of {deferredFragmentRecord}. -- {path}: a list of field names and indices from root to - {deferredFragmentRecord}. CreateDeferredFragmentRecord(label, objectType, objectValue, -fragmentSelectionSet, path): +fragmentSelectionSet, path, parentRecord): -- If {path} is not provided, initialize it to an empty list. - Construct a deferred fragment record based on the parameters passed in. +- Initialize {errors} to an empty list. ResolveDeferredFragmentRecord(deferredFragmentRecord, variableValues, subsequentPayloads): -- Let {label}, {objectType}, {objectValue}, {fragmentSelectionSet}, {path} be - the corresponding fields in the deferred fragment record structure. -- Let {payload} be the result of calling - {ExecuteSelectionSet(fragmentSelectionSet, objectType, objectValue, - variableValues, subsequentPayloads, path)}. +- Let {label}, {objectType}, {objectValue}, {fragmentSelectionSet}, {path}, + {parentRecord} be the corresponding fields in the deferred fragment record + structure. +- Let {dataExecution} be the asynchronous future value of: + - Let {payload} be the result of {ExecuteSelectionSet(fragmentSelectionSet, + objectType, objectValue, variableValues, path, subsequentPayloads, + deferredFragmentRecord)}. + - If {parentRecord} is defined: + - Wait for the result of {dataExecution} on {parentRecord}. + - Return {payload}. +- Set {dataExecution} on {deferredFragmentRecord}. +- Let {payload} be the result of waiting for {dataExecution}. - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {path}. - Return {payload}. @@ -729,11 +742,12 @@ coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value. -ExecuteField(objectType, objectValue, fieldType, fields, variableValues, -subsequentPayloads, parentPath): +ExecuteField(objectType, objectValue, fieldType, fields, variableValues, path, +subsequentPayloads, asyncRecord): - Let {field} be the first entry in {fields}. - Let {fieldName} be the field name of {field}. +- Append {fieldName} to {path}. - Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} - If {field} provides the directive `@stream`, let {streamDirective} be that @@ -741,16 +755,14 @@ subsequentPayloads, parentPath): - Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument. - Let {resolvedValue} be {ResolveFieldGenerator(objectType, objectValue, - fieldName, argumentValues, initialCount)}. + fieldName, argumentValues)}. - Let {result} be the result of calling {CompleteValue(fieldType, fields, - resolvedValue, variableValues, subsequentPayloads, parentPath)}. - - Append {fieldName} to the {path} field of every {subsequentPayloads}. + resolvedValue, variableValues, path, subsequentPayloads, asyncRecord)}. - Return {result}. - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. - Let {result} be the result of calling {CompleteValue(fieldType, fields, - resolvedValue, variableValues, subsequentPayloads)}. -- Append {fieldName} to the {path} for every {subsequentPayloads}. + resolvedValue, variableValues, path, subsequentPayloads, asyncRecord)}. - Return {result}. ### Coercing Field Arguments @@ -836,14 +848,13 @@ ResolveFieldValue(objectType, objectValue, fieldName, argumentValues): - Return the result of calling {resolver}, providing {objectValue} and {argumentValues}. -ResolveFieldGenerator(objectType, objectValue, fieldName, argumentValues, -initialCount): +ResolveFieldGenerator(objectType, objectValue, fieldName, argumentValues): - If {objectType} provide an internal function {generatorResolver} for generating partially resolved value of a list field named {fieldName}: - Let {generatorResolver} be the internal function. - Return the iterator from calling {generatorResolver}, providing - {objectValue}, {argumentValues} and {initialCount}. + {objectValue} and {argumentValues}. - Create {generator} from {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. - Return {generator}. @@ -863,52 +874,58 @@ field execution process continues recursively. In the case where a value returned for a list type field is an iterator due to `@stream` specified on the field, value completion iterates over the iterator until the number of items yield by the iterator satisfies `initialCount` specified on the `@stream` -directive. Unresolved items in the iterator will be stored in a stream record -which the executor resumes to execute after the initial execution finishes. +directive. #### Stream Record Let {streamField} be a list field with a `@stream` directive provided. -A Stream Record is a structure containing: +A Stream Record is a structure containing all the entries of Async Payload +Record, and additionally: -- {label}: value derived from the `@stream` directive's `label` argument. - {iterator}: created by {ResolveFieldGenerator}. -- {resolvedItems}: items resolved from the {iterator} but not yet delivered. - {index}: indicating the position of the item in the complete list. -- {path}: a list of field names and indices from root to {streamField}. - {fields}: the group of fields grouped by CollectFields() for {streamField}. - {innerType}: inner type of {streamField}'s type. -CreateStreamRecord(label, initialCount, iterator, resolvedItems, index, fields, -innerType): +CreateStreamRecord(label, iterator, index, fields, innerType, path, +parentRecord): - Construct a stream record based on the parameters passed in. +- Initialize {errors} to an empty list. ResolveStreamRecord(streamRecord, variableValues, subsequentPayloads): -- Let {label}, {iterator}, {resolvedItems}, {index}, {path}, {fields}, +- Let {label}, {parentRecord}, {iterator}, {index}, {path}, {fields}, {innerType} be the correspondent fields on the Stream Record structure. -- Wait for the next item from {iterator}. -- If an item is not retrieved because {iterator} has completed: - - Return {null} -- Let {item} be the item retrieved from {iterator}. -- Append {index} to {path}. -- Increment {index}. -- Let {payload} be the result of calling CompleteValue(innerType, fields, item, - variableValues, subsequentPayloads, path)}. +- Let {indexPath} be {path} with {index} appended. +- Let {dataExecution} be the asynchronous future value of: + - Wait for the next item from {iterator}. + - If an item is not retrieved because {iterator} has completed: + - Return {null}. + - Let {item} be the item retrieved from {iterator}. + - Let {payload} be the result of calling {CompleteValue(innerType, fields, + item, variableValues, indexPath, subsequentPayloads, parentRecord)}. + - Increment {index}. + - Let {nextStreamRecord} be the result of calling {CreateStreamRecord(label, + iterator, index, fields, innerType, path, streamRecord)}. + - Append {nextStreamRecord} to {subsequentPayloads}. + - If {parentRecord} is defined: + - Wait for the result of {dataExecution} on {parentRecord}. + - Return {payload}. +- Set {dataExecution} on {streamRecord}. +- Let {payload} be the result of waiting for {dataExecution}. - Add an entry to {payload} named `label` with the value {label}. -- Add an entry to {payload} named `path` with the value {path}. -- Append {streamRecord} to {subsequentPayloads}. +- Add an entry to {payload} named `path` with the value {indexPath}. - Return {payload}. -CompleteValue(fieldType, fields, result, variableValues, subsequentPayloads, -parentPath): +CompleteValue(fieldType, fields, result, variableValues, path, +subsequentPayloads, asyncRecord): - If the {fieldType} is a Non-Null type: - Let {innerType} be the inner type of {fieldType}. - Let {completedResult} be the result of calling {CompleteValue(innerType, - fields, result, variableValues)}. + fields, result, variableValues, path)}. - If {completedResult} is {null}, raise a _field error_. - Return {completedResult}. - If {result} is {null} (or another internal value similar to {null} such as @@ -923,26 +940,33 @@ parentPath): - If {initialCount} is less than zero, raise a _field error_. - Let {label} be the value or variable provided to {streamDirective}'s {label} argument. - - Let {resolvedItems} be an empty list - - For each {members} in {result}: - - Append all items from {members} to {resolvedItems}. - - If the length of {resolvedItems} is greater or equal to {initialCount}: - - Let {initialItems} be the sublist of the first {initialCount} items - from {resolvedItems}. - - Let {remainingItems} be the sublist of the items in {resolvedItems} - after the first {initialCount} items. + - Let {initialItems} be an empty list + - Let {index} be zero. + - While {result} is not closed: + - If {streamDirective} was not provided or {index} is not greater than or + equal to {initialCount}: + - Wait for the next item from {result}. + - Let {resultItem} be the item retrieved from {result}. + - Let {indexPath} be {path} with {index} appended. + - Let {resolvedItem} be the result of calling {CompleteValue(innerType, + fields, resultItem, variableValues, indexPath, subsequentPayloads, + asyncRecord)}. + - Append {resolvedItem} to {initialItems}. + - Increment {index}. + - If {streamDirective} was provided and {index} is greater than or equal + to {initialCount}: - Let {streamRecord} be the result of calling {CreateStreamRecord(label, - initialCount, result, remainingItems, initialCount, fields, innerType, - parentPath)} + result, index, fields, innerType, path, asyncRecord)}. - Append {streamRecord} to {subsequentPayloads}. - Let {result} be {initialItems}. - - Exit for each loop. + - Exit while loop. + - Return {initialItems}. - If {result} is not a collection of values, raise a _field error_. - Let {innerType} be the inner type of {fieldType}. - Return a list where each list item is the result of calling - {CompleteValue(innerType, fields, resultItem, variableValues, - subsequentPayloads, parentPath)}, where {resultItem} is each item in - {result}. + {CompleteValue(innerType, fields, resultItem, variableValues, indexPath, + subsequentPayloads, asyncRecord)}, where {resultItem} is each item in + {result} and {indexPath} is {path} with the index of the item appended. - If {fieldType} is a Scalar or Enum type: - Return the result of {CoerceResult(fieldType, result)}. - If {fieldType} is an Object, Interface, or Union type: @@ -952,7 +976,7 @@ parentPath): - Let {objectType} be {ResolveAbstractType(fieldType, result)}. - Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}. - Return the result of evaluating {ExecuteSelectionSet(subSelectionSet, - objectType, result, variableValues, subsequentPayloads, parentPath)} + objectType, result, variableValues, path, subsequentPayloads, asyncRecord)} _normally_ (allowing for parallelization). **Coercing Results** From a20f9ef3ef0e35a954e80c4a535bc69fdda1d032 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 20 Dec 2021 16:00:43 -0500 Subject: [PATCH 14/64] Simplify execution, payloads should begin execution immediately # Conflicts: # spec/Section 6 -- Execution.md --- spec/Section 6 -- Execution.md | 115 ++++++++++----------------------- 1 file changed, 34 insertions(+), 81 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 900f6b230..f7920e622 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -380,21 +380,13 @@ YieldSubsequentPayloads(subsequentPayloads): - While {subsequentPayloads} is not empty: - If a termination signal is received: - For each {record} in {subsequentPayloads}: - - If {record} is a Stream Record: - - Let {iterator} be the correspondent fields on the Stream Record - structure. + - If {record} contains {iterator}: - Send a termination signal to {iterator}. - Return. -- Let {record} be the first complete item in {subsequentPayloads}. +- Let {record} be the first item in {subsequentPayloads} with a completed + {dataExecution}. - Remove {record} from {subsequentPayloads}. - - Assert: {record} must be a Deferred Fragment Record or a Stream Record. - - If {record} is a Deferred Fragment Record: - - Let {payload} be the result of running - {ResolveDeferredFragmentRecord(record, variableValues, - subsequentPayloads)}. - - If {record} is a Stream Record: - - Let {payload} be the result of running {ResolveStreamRecord(record, - variableValues, subsequentPayloads)}. + - Let {payload} be the completed result returned by {dataExecution}. - If {payload} is {null}: - If {subsequentPayloads} is empty: - Yield a map containing a field `hasNext` with the value {false}. @@ -626,10 +618,9 @@ subsequentPayloads, asyncRecord, visitedFragments): - If {deferDirective} is defined: - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - - Let {deferredFragmentRecord} be the result of calling - {CreateDeferredFragmentRecord(label, objectType, objectValue, - fragmentSelectionSet, path, asyncRecord)}. - - Append {deferredFragmentRecord} to {subsequentPayloads}. + - Call {ExecuteDeferredFragment(label, objectType, objectValue, + fragmentSelectionSet, path, variableValues, asyncRecord, + subsequentPayloads)}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, objectValue, fragmentSelectionSet, @@ -652,10 +643,8 @@ subsequentPayloads, asyncRecord, visitedFragments): {variableValues} with the value {true}: - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - - Let {deferredFragmentRecord} be the result of calling - {CreateDeferredFragmentRecord(label, objectType, objectValue, - fragmentSelectionSet, path, asyncRecord)}. - - Append {deferredFragmentRecord} to {subsequentPayloads}. + - Call {ExecuteDeferredFragment(label, objectType, objectValue, + fragmentSelectionSet, path, asyncRecord, subsequentPayloads)}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, objectValue, fragmentSelectionSet, @@ -689,50 +678,31 @@ An Async Payload Record is either a Deferred Fragment Record or a Stream Record. All Async Payload Records are structures containing: - {label}: value derived from the corresponding `@defer` or `@stream` directive. -- {parentRecord}: optionally an Async Payload Record. +- {path}: a list of field names and indices from root to the location of the + corresponding `@defer` or `@stream` directive. +- {iterator}: The underlying iterator if created from a `@stream` directive. - {errors}: a list of field errors encountered during execution. - {dataExecution}: A result that can notify when the corresponding execution has completed. -- {path}: a list of field names and indices from root to the location of the - corresponding `@defer` or `@stream` directive. - -#### Deferred Fragment Record - -Let {deferredFragmentRecord} be an inline fragment or fragment spread with -`@defer` provided. - -Deferred Fragment Record is a structure containing all the entries of Async -Payload Record, and additionally: -- {objectType}: of the {deferredFragmentRecord}. -- {objectValue}: of the {deferredFragmentRecord}. -- {fragmentSelectionSet}: the top level selection set of - {deferredFragmentRecord}. +#### Execute Deferred Fragment -CreateDeferredFragmentRecord(label, objectType, objectValue, -fragmentSelectionSet, path, parentRecord): +ExecuteDeferredFragment(label, objectType, objectValue, fragmentSelectionSet, +path, variableValues, parentRecord, subsequentPayloads): -- Construct a deferred fragment record based on the parameters passed in. -- Initialize {errors} to an empty list. - -ResolveDeferredFragmentRecord(deferredFragmentRecord, variableValues, -subsequentPayloads): - -- Let {label}, {objectType}, {objectValue}, {fragmentSelectionSet}, {path}, - {parentRecord} be the corresponding fields in the deferred fragment record - structure. +- Let {deferRecord} be an async payload record created from {label} and {path}. +- Initialize {errors} on {deferRecord} to an empty list. - Let {dataExecution} be the asynchronous future value of: - Let {payload} be the result of {ExecuteSelectionSet(fragmentSelectionSet, objectType, objectValue, variableValues, path, subsequentPayloads, - deferredFragmentRecord)}. + deferRecord)}. - If {parentRecord} is defined: - Wait for the result of {dataExecution} on {parentRecord}. + - Add an entry to {payload} named `label` with the value {label}. + - Add an entry to {payload} named `path` with the value {path}. - Return {payload}. - Set {dataExecution} on {deferredFragmentRecord}. -- Let {payload} be the result of waiting for {dataExecution}. -- Add an entry to {payload} named `label` with the value {label}. -- Add an entry to {payload} named `path` with the value {path}. -- Return {payload}. +- Append {deferRecord} to {subsequentPayloads}. ## Executing Fields @@ -876,28 +846,14 @@ field, value completion iterates over the iterator until the number of items yield by the iterator satisfies `initialCount` specified on the `@stream` directive. -#### Stream Record - -Let {streamField} be a list field with a `@stream` directive provided. - -A Stream Record is a structure containing all the entries of Async Payload -Record, and additionally: - -- {iterator}: created by {ResolveFieldGenerator}. -- {index}: indicating the position of the item in the complete list. -- {fields}: the group of fields grouped by CollectFields() for {streamField}. -- {innerType}: inner type of {streamField}'s type. - -CreateStreamRecord(label, iterator, index, fields, innerType, path, -parentRecord): - -- Construct a stream record based on the parameters passed in. -- Initialize {errors} to an empty list. +#### Execute Stream Field -ResolveStreamRecord(streamRecord, variableValues, subsequentPayloads): +ExecuteStreamRecord(label, iterator, index, fields, innerType, path +streamRecord, variableValues, subsequentPayloads): -- Let {label}, {parentRecord}, {iterator}, {index}, {path}, {fields}, - {innerType} be the correspondent fields on the Stream Record structure. +- Let {streamRecord} be an async payload record created from {label}, {path}, + and {iterator}. +- Initialize {errors} on {streamRecord} to an empty list. - Let {indexPath} be {path} with {index} appended. - Let {dataExecution} be the asynchronous future value of: - Wait for the next item from {iterator}. @@ -907,17 +863,15 @@ ResolveStreamRecord(streamRecord, variableValues, subsequentPayloads): - Let {payload} be the result of calling {CompleteValue(innerType, fields, item, variableValues, indexPath, subsequentPayloads, parentRecord)}. - Increment {index}. - - Let {nextStreamRecord} be the result of calling {CreateStreamRecord(label, - iterator, index, fields, innerType, path, streamRecord)}. - - Append {nextStreamRecord} to {subsequentPayloads}. + - Call {ExecuteStreamRecord(label, iterator, index, fields, innerType, path, + streamRecord, variableValues, subsequentPayloads)}. - If {parentRecord} is defined: - Wait for the result of {dataExecution} on {parentRecord}. + - Add an entry to {payload} named `label` with the value {label}. + - Add an entry to {payload} named `path` with the value {indexPath}. - Return {payload}. - Set {dataExecution} on {streamRecord}. -- Let {payload} be the result of waiting for {dataExecution}. -- Add an entry to {payload} named `label` with the value {label}. -- Add an entry to {payload} named `path` with the value {indexPath}. -- Return {payload}. +- Append {streamRecord} to {subsequentPayloads}. CompleteValue(fieldType, fields, result, variableValues, path, subsequentPayloads, asyncRecord): @@ -955,9 +909,8 @@ subsequentPayloads, asyncRecord): - Increment {index}. - If {streamDirective} was provided and {index} is greater than or equal to {initialCount}: - - Let {streamRecord} be the result of calling {CreateStreamRecord(label, - result, index, fields, innerType, path, asyncRecord)}. - - Append {streamRecord} to {subsequentPayloads}. + - Call {ExecuteStreamRecord(label, result, index, fields, innerType, + path, asyncRecord, subsequentPayloads)}. - Let {result} be {initialItems}. - Exit while loop. - Return {initialItems}. From 192f7357bd2bfb9a11cc8e01959700bb81930235 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 20 Dec 2021 16:08:58 -0500 Subject: [PATCH 15/64] Clarify error handling # Conflicts: # spec/Section 6 -- Execution.md --- spec/Section 6 -- Execution.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f7920e622..ca848af82 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -393,9 +393,9 @@ YieldSubsequentPayloads(subsequentPayloads): - Return. - If {subsequentPayloads} is not empty: - Continue to the next record in {subsequentPayloads}. - - If {record} is not the final element in {subsequentPayloads} + - If {record} is not the final element in {subsequentPayloads}: - Add an entry to {payload} named `hasNext` with the value {true}. - - If {record} is the final element in {subsequentPayloads} + - If {record} is the final element in {subsequentPayloads}: - Add an entry to {payload} named `hasNext` with the value {false}. - Yield {payload} @@ -693,11 +693,16 @@ path, variableValues, parentRecord, subsequentPayloads): - Let {deferRecord} be an async payload record created from {label} and {path}. - Initialize {errors} on {deferRecord} to an empty list. - Let {dataExecution} be the asynchronous future value of: - - Let {payload} be the result of {ExecuteSelectionSet(fragmentSelectionSet, + - Let {payload} be an unordered map. + - Let {data} be the result of {ExecuteSelectionSet(fragmentSelectionSet, objectType, objectValue, variableValues, path, subsequentPayloads, deferRecord)}. + - Append any encountered field errors to {errors}. - If {parentRecord} is defined: - Wait for the result of {dataExecution} on {parentRecord}. + - If {errors} is not empty: + - Add an entry to {payload} named `errors` with the value {errors}. + - Add an entry to {payload} named `data` with the value {data}. - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {path}. - Return {payload}. @@ -848,8 +853,8 @@ directive. #### Execute Stream Field -ExecuteStreamRecord(label, iterator, index, fields, innerType, path -streamRecord, variableValues, subsequentPayloads): +ExecuteStreamField(label, iterator, index, fields, innerType, path streamRecord, +variableValues, subsequentPayloads): - Let {streamRecord} be an async payload record created from {label}, {path}, and {iterator}. @@ -859,14 +864,19 @@ streamRecord, variableValues, subsequentPayloads): - Wait for the next item from {iterator}. - If an item is not retrieved because {iterator} has completed: - Return {null}. + - Let {payload} be an unordered map. - Let {item} be the item retrieved from {iterator}. - - Let {payload} be the result of calling {CompleteValue(innerType, fields, - item, variableValues, indexPath, subsequentPayloads, parentRecord)}. + - Let {data} be the result of calling {CompleteValue(innerType, fields, item, + variableValues, indexPath, subsequentPayloads, parentRecord)}. + - Append any encountered field errors to {errors}. - Increment {index}. - - Call {ExecuteStreamRecord(label, iterator, index, fields, innerType, path, + - Call {ExecuteStreamField(label, iterator, index, fields, innerType, path, streamRecord, variableValues, subsequentPayloads)}. - If {parentRecord} is defined: - Wait for the result of {dataExecution} on {parentRecord}. + - If {errors} is not empty: + - Add an entry to {payload} named `errors` with the value {errors}. + - Add an entry to {payload} named `data` with the value {data}. - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {indexPath}. - Return {payload}. @@ -909,7 +919,7 @@ subsequentPayloads, asyncRecord): - Increment {index}. - If {streamDirective} was provided and {index} is greater than or equal to {initialCount}: - - Call {ExecuteStreamRecord(label, result, index, fields, innerType, + - Call {ExecuteStreamField(label, result, index, fields, innerType, path, asyncRecord, subsequentPayloads)}. - Let {result} be {initialItems}. - Exit while loop. From 1e402c02abdc7767a7bef3bcc7e29fa0bec7c16a Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 30 Dec 2021 12:57:17 -0500 Subject: [PATCH 16/64] add isCompletedIterator to AsyncPayloadRecord to track completed iterators # Conflicts: # spec/Section 6 -- Execution.md --- spec/Section 6 -- Execution.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index ca848af82..94c33485e 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -386,13 +386,13 @@ YieldSubsequentPayloads(subsequentPayloads): - Let {record} be the first item in {subsequentPayloads} with a completed {dataExecution}. - Remove {record} from {subsequentPayloads}. - - Let {payload} be the completed result returned by {dataExecution}. - - If {payload} is {null}: + - If {isCompletedIterator} on {record} is {true}: - If {subsequentPayloads} is empty: - Yield a map containing a field `hasNext` with the value {false}. - Return. - If {subsequentPayloads} is not empty: - Continue to the next record in {subsequentPayloads}. + - Let {payload} be the completed result returned by {dataExecution}. - If {record} is not the final element in {subsequentPayloads}: - Add an entry to {payload} named `hasNext` with the value {true}. - If {record} is the final element in {subsequentPayloads}: @@ -681,6 +681,8 @@ All Async Payload Records are structures containing: - {path}: a list of field names and indices from root to the location of the corresponding `@defer` or `@stream` directive. - {iterator}: The underlying iterator if created from a `@stream` directive. +- {isCompletedIterator}: a boolean indicating the payload record was generated + from an iterator that has completed. - {errors}: a list of field errors encountered during execution. - {dataExecution}: A result that can notify when the corresponding execution has completed. @@ -863,6 +865,7 @@ variableValues, subsequentPayloads): - Let {dataExecution} be the asynchronous future value of: - Wait for the next item from {iterator}. - If an item is not retrieved because {iterator} has completed: + - Set {isCompletedIterator} to {true} on {streamRecord}. - Return {null}. - Let {payload} be an unordered map. - Let {item} be the item retrieved from {iterator}. From 32fe87894cd63eeb46bd033798fb69ea89da18bb Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 21 Jan 2022 07:42:28 -0500 Subject: [PATCH 17/64] fix typo --- spec/Section 6 -- Execution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 94c33485e..77f6110bd 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -600,7 +600,7 @@ subsequentPayloads, asyncRecord, visitedFragments): - Append {selection} to the {groupForResponseKey}. - If {selection} is a {FragmentSpread}: - Let {fragmentSpreadName} be the name of {selection}. - - If {fragmentSpreadName} provides the directive `@defer` and it's {if} + - If {fragmentSpreadName} provides the directive `@defer` and its {if} argument is {true} or is a variable in {variableValues} with the value {true}: - Let {deferDirective} be that directive. From 05f600751594e313c406a408d198a45a68cfb8e6 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 2 Feb 2022 14:45:30 -0500 Subject: [PATCH 18/64] deferDirective and visitedFragments --- spec/Section 6 -- Execution.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 77f6110bd..874ffbb0b 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -604,9 +604,10 @@ subsequentPayloads, asyncRecord, visitedFragments): argument is {true} or is a variable in {variableValues} with the value {true}: - Let {deferDirective} be that directive. - - If {fragmentSpreadName} is in {visitedFragments} and {deferDirective} is - not defined, continue with the next {selection} in {selectionSet}. - - Add {fragmentSpreadName} to {visitedFragments}. + - If {deferDirective} is not defined: + - If {fragmentSpreadName} is in {visitedFragments}, continue with the next + {selection} in {selectionSet}. + - Add {fragmentSpreadName} to {visitedFragments}. - Let {fragment} be the Fragment in the current Document whose name is {fragmentSpreadName}. - If no such {fragment} exists, continue with the next {selection} in From 4b6677afdd70207767c094862c173412d562b7c3 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 7 Feb 2022 15:33:51 -0500 Subject: [PATCH 19/64] stream if argument, indexPath -> itemPath --- spec/Section 6 -- Execution.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 874ffbb0b..59b57ee80 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -862,7 +862,7 @@ variableValues, subsequentPayloads): - Let {streamRecord} be an async payload record created from {label}, {path}, and {iterator}. - Initialize {errors} on {streamRecord} to an empty list. -- Let {indexPath} be {path} with {index} appended. +- Let {itemPath} be {path} with {index} appended. - Let {dataExecution} be the asynchronous future value of: - Wait for the next item from {iterator}. - If an item is not retrieved because {iterator} has completed: @@ -871,7 +871,7 @@ variableValues, subsequentPayloads): - Let {payload} be an unordered map. - Let {item} be the item retrieved from {iterator}. - Let {data} be the result of calling {CompleteValue(innerType, fields, item, - variableValues, indexPath, subsequentPayloads, parentRecord)}. + variableValues, itemPath, subsequentPayloads, parentRecord)}. - Append any encountered field errors to {errors}. - Increment {index}. - Call {ExecuteStreamField(label, iterator, index, fields, innerType, path, @@ -882,7 +882,7 @@ variableValues, subsequentPayloads): - Add an entry to {payload} named `errors` with the value {errors}. - Add an entry to {payload} named `data` with the value {data}. - Add an entry to {payload} named `label` with the value {label}. - - Add an entry to {payload} named `path` with the value {indexPath}. + - Add an entry to {payload} named `path` with the value {itemPath}. - Return {payload}. - Set {dataExecution} on {streamRecord}. - Append {streamRecord} to {subsequentPayloads}. @@ -902,7 +902,9 @@ subsequentPayloads, asyncRecord): - If {result} is an iterator: - Let {field} be the first entry in {fields}. - Let {innerType} be the inner type of {fieldType}. - - Let {streamDirective} be the `@stream` directive provided on {field}. + - If {field} provides the directive `@stream` and its {if} argument is + {true} or is a variable in {variableValues} with the value {true} and : + - Let {streamDirective} be that directive. - Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument. - If {initialCount} is less than zero, raise a _field error_. @@ -911,18 +913,18 @@ subsequentPayloads, asyncRecord): - Let {initialItems} be an empty list - Let {index} be zero. - While {result} is not closed: - - If {streamDirective} was not provided or {index} is not greater than or + - If {streamDirective} is not defined or {index} is not greater than or equal to {initialCount}: - Wait for the next item from {result}. - Let {resultItem} be the item retrieved from {result}. - - Let {indexPath} be {path} with {index} appended. + - Let {itemPath} be {path} with {index} appended. - Let {resolvedItem} be the result of calling {CompleteValue(innerType, - fields, resultItem, variableValues, indexPath, subsequentPayloads, + fields, resultItem, variableValues, itemPath, subsequentPayloads, asyncRecord)}. - Append {resolvedItem} to {initialItems}. - Increment {index}. - - If {streamDirective} was provided and {index} is greater than or equal - to {initialCount}: + - If {streamDirective} is defined and {index} is greater than or equal to + {initialCount}: - Call {ExecuteStreamField(label, result, index, fields, innerType, path, asyncRecord, subsequentPayloads)}. - Let {result} be {initialItems}. @@ -931,9 +933,9 @@ subsequentPayloads, asyncRecord): - If {result} is not a collection of values, raise a _field error_. - Let {innerType} be the inner type of {fieldType}. - Return a list where each list item is the result of calling - {CompleteValue(innerType, fields, resultItem, variableValues, indexPath, + {CompleteValue(innerType, fields, resultItem, variableValues, itemPath, subsequentPayloads, asyncRecord)}, where {resultItem} is each item in - {result} and {indexPath} is {path} with the index of the item appended. + {result} and {itemPath} is {path} with the index of the item appended. - If {fieldType} is a Scalar or Enum type: - Return the result of {CoerceResult(fieldType, result)}. - If {fieldType} is an Object, Interface, or Union type: From a5628767328f63707b2df53196c718b7ae9ab584 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 7 Feb 2022 15:43:48 -0500 Subject: [PATCH 20/64] Clarify stream only applies to outermost list of multi-dimensional arrays --- spec/Section 6 -- Execution.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 59b57ee80..35fc37f0e 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -903,7 +903,9 @@ subsequentPayloads, asyncRecord): - Let {field} be the first entry in {fields}. - Let {innerType} be the inner type of {fieldType}. - If {field} provides the directive `@stream` and its {if} argument is - {true} or is a variable in {variableValues} with the value {true} and : + {true} or is a variable in {variableValues} with the value {true} and + {innerType} is the outermost return type of the list type defined for + {field}: - Let {streamDirective} be that directive. - Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument. From 274110d7f27b974c118263751de6dae3e43c5c8a Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 7 Mar 2022 16:30:36 -0500 Subject: [PATCH 21/64] =?UTF-8?q?add=20validation=20=E2=80=9CDefer=20And?= =?UTF-8?q?=20Stream=20Directive=20Labels=20Are=20Unique=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spec/Section 5 -- Validation.md | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 44b8b9d26..c6bb6c4b3 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1556,6 +1556,71 @@ mutation { } ``` +### Defer And Stream Directive Labels Are Unique + +** Formal Specification ** + +- For every {directive} in a 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. + - Let {labels} be all label values passed to defer or stream directive label + arguments. + - {labels} must be a set of one. + +**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** From 1c79905289074ad1b5a88c7986bb301b293b782e Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 8 Mar 2022 10:41:32 -0500 Subject: [PATCH 22/64] Clarification on labels --- spec/Section 3 -- Type System.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 4a25974b0..49ecd38e0 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2206,10 +2206,11 @@ fragment someFragment on User { - `if: Boolean` - When `true`, fragment _should_ be deferred. When `false`, fragment will not be deferred and data will be included in the initial response. If omitted, defaults to `true`. -- `label: String` - A unique label across all `@defer` and `@stream` directives - in an operation. This label should be used by GraphQL clients to identify the - data from patch responses and associate it with the correct fragments. If - provided, the GraphQL Server must add it to the payload. +- `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 Server 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 @@ -2237,10 +2238,12 @@ query myQuery($shouldStream: Boolean) { - `if: Boolean` - When `true`, field _should_ be streamed. When `false`, the field will not be streamed and all list items will be included in the initial response. If omitted, defaults to `true`. -- `label: String` - A unique label across all `@defer` and `@stream` directives - in an operation. This label should be used by GraphQL clients to identify the - data from patch responses and associate it with the correct fragments. If - provided, the GraphQL Server must add it to the payload. +- `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 Server 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 server 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`. From f912d672bba3b31a4fdea32fc773ec0e472dc5b9 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 23 Mar 2022 14:42:50 -0400 Subject: [PATCH 23/64] fix wrong quotes --- spec/Section 3 -- Type System.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 49ecd38e0..771c5c0b0 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2188,10 +2188,10 @@ delivered in a subsequent response. `@include` and `@skip` take precedence over ```graphql example query myQuery($shouldDefer: Boolean) { - user { - name - ...someFragment @defer(label: 'someLabel', if: $shouldDefer) - } + user { + name + ...someFragment @defer(label: "someLabel", if: $shouldDefer) + } } fragment someFragment on User { id From a2d730ea5aa33ab89bf54472ec921f9a03f749cc Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 23 Mar 2022 14:45:42 -0400 Subject: [PATCH 24/64] remove label/path requirement --- spec/Section 7 -- Response.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 47ee7e893..690c8a016 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -26,8 +26,8 @@ When the response of the GraphQL operation is an event stream, the first value will be the initial response. All subsequent values may contain `label` and `path` entries. These two entries are used by clients to identify the `@defer` or `@stream` directive from the GraphQL operation that triggered this value to -be returned by the event stream. The combination of these two entries must be -unique across all values returned by the event stream. +be returned by the event stream. When a label is provided, the combination of +these two entries will be unique across all values returned by the event stream. If the response of the GraphQL operation is an event stream, each response map must contain an entry with key `hasNext`. The value of this entry is `true` for From e2b522741f8be239e27975ba20bbcfa602c16e12 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 9 Jun 2022 17:26:33 -0500 Subject: [PATCH 25/64] add missing line --- spec/Section 6 -- Execution.md | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 35fc37f0e..4b9ba73c1 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -30,6 +30,7 @@ request is determined by the result of executing this operation according to the "Executing Operations” section below. ExecuteRequest(schema, document, operationName, variableValues, initialValue): + Note: the execution assumes implementing language supports coroutines. Alternatively, the socket can provide a write buffer pointer to allow {ExecuteRequest()} to directly write payloads into the buffer. From b25627f42c8e98e8d9ad8bce9417bdb9db69f614 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 9 Jun 2022 17:28:59 -0500 Subject: [PATCH 26/64] fix ExecuteRequest --- spec/Section 6 -- Execution.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 4b9ba73c1..18eb21a45 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -39,13 +39,8 @@ Alternatively, the socket can provide a write buffer pointer to allow - Let {coercedVariableValues} be the result of {CoerceVariableValues(schema, operation, variableValues)}. - If {operation} is a query operation: - - Let {executionResult} be the result of calling {ExecuteQuery(operation, - schema, coercedVariableValues, initialValue, subsequentPayloads)}. - - If {executionResult} is an iterator: - - For each {payload} in {executionResult}: - - Yield {payload}. - - Otherwise: - - Return {executionResult}. + - Return {ExecuteQuery(operation, schema, coercedVariableValues, + initialValue)}. - Otherwise if {operation} is a mutation operation: - Return {ExecuteMutation(operation, schema, coercedVariableValues, initialValue)}. From 58dda179c9ba0f09331318e40ce89e295609ecce Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 9 Jun 2022 17:31:06 -0500 Subject: [PATCH 27/64] fix response --- spec/Section 7 -- Response.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 690c8a016..3743d4f8d 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -12,9 +12,9 @@ the case that any _field error_ was raised on a field and was replaced with A response to a GraphQL request must be a map or an event stream of maps. -If the operation encountered any errors, the response map must contain an entry -with key `errors`. The value of this entry is described in the "Errors" section. -If the request completed without raising any errors, this entry must not be +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 +the request completed without raising any errors, this entry must not be present. If the request included execution, the response map must contain an entry with From 1d0e7558f625161c9d93e9fe7aaaecd48aefedba Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 3 Aug 2022 14:41:10 -0400 Subject: [PATCH 28/64] Align deferred fragment field collection with reference implementation --- spec/Section 6 -- Execution.md | 57 ++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 18eb21a45..2ae221126 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -410,8 +410,9 @@ subsequentPayloads, asyncRecord): - If {path} is not provided, initialize it to an empty list. - If {subsequentPayloads} is not provided, initialize it to the empty set. -- Let {groupedFieldSet} be the result of {CollectFields(objectType, objectValue, - selectionSet, variableValues, path subsequentPayloads, asyncRecord)}. +- Let {groupedFieldSet} and {deferredGroupedFieldsList} be the result of + {CollectFields(objectType, objectValue, selectionSet, variableValues, path, + asyncRecord)}. - Initialize {resultMap} to an empty ordered map. - For each {groupedFieldSet} as {responseKey} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value @@ -422,6 +423,10 @@ subsequentPayloads, asyncRecord): - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, fields, variableValues, path, subsequentPayloads, asyncRecord)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. +- For each {deferredGroupFieldSet} and {label} in {deferredGroupedFieldsList} + - Call {ExecuteDeferredFragment(label, objectType, objectValue, + deferredGroupFieldSet, path, variableValues, asyncRecord, + subsequentPayloads)} - Return {resultMap}. Note: {resultMap} is ordered by which fields appear first in the operation. This @@ -573,10 +578,12 @@ is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. CollectFields(objectType, objectValue, selectionSet, variableValues, path, -subsequentPayloads, asyncRecord, visitedFragments): +asyncRecord, visitedFragments, deferredGroupedFieldsList): - If {visitedFragments} is not provided, initialize it to the empty set. - Initialize {groupedFields} to an empty ordered map of lists. +- If {deferredGroupedFieldsList} is not provided, initialize it to an empty + list. - For each {selection} in {selectionSet}: - If {selection} provides the directive `@skip`, let {skipDirective} be that directive. @@ -615,13 +622,17 @@ subsequentPayloads, asyncRecord, visitedFragments): - If {deferDirective} is defined: - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - - Call {ExecuteDeferredFragment(label, objectType, objectValue, - fragmentSelectionSet, path, variableValues, asyncRecord, - subsequentPayloads)}. + - Let {deferredGroupedFields} be the result of calling + {CollectFields(objectType, objectValue, fragmentSelectionSet, + variableValues, path, asyncRecord, visitedFragments, + deferredGroupedFieldsList)}. + - Append a record containing {label} and {deferredGroupedFields} to + {deferredGroupedFieldsList}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, objectValue, fragmentSelectionSet, - variableValues, path, subsequentPayloads, asyncRecord, visitedFragments)}. + variableValues, path, asyncRecord, visitedFragments, + deferredGroupedFieldsList)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -640,19 +651,24 @@ subsequentPayloads, asyncRecord, visitedFragments): {variableValues} with the value {true}: - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - - Call {ExecuteDeferredFragment(label, objectType, objectValue, - fragmentSelectionSet, path, asyncRecord, subsequentPayloads)}. + - Let {deferredGroupedFields} be the result of calling + {CollectFields(objectType, objectValue, fragmentSelectionSet, + variableValues, path, asyncRecord, visitedFragments, + deferredGroupedFieldsList)}. + - Append a record containing {label} and {deferredGroupedFields} to + {deferredGroupedFieldsList}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, objectValue, fragmentSelectionSet, - variableValues, path, subsequentPayloads, asyncRecord, visitedFragments)}. + variableValues, path, asyncRecord, visitedFragments, + deferredGroupedFieldsList)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. - Let {groupForResponseKey} be the list in {groupedFields} for {responseKey}; if no such list exists, create it as an empty list. - Append all items in {fragmentGroup} to {groupForResponseKey}. -- Return {groupedFields}. +- Return {groupedFields} and {deferredGroupedFieldsList}. Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` directives may be applied in either order since they apply commutatively. @@ -686,22 +702,29 @@ All Async Payload Records are structures containing: #### Execute Deferred Fragment -ExecuteDeferredFragment(label, objectType, objectValue, fragmentSelectionSet, -path, variableValues, parentRecord, subsequentPayloads): +ExecuteDeferredFragment(label, objectType, objectValue, groupedFieldSet, path, +variableValues, parentRecord, subsequentPayloads): - Let {deferRecord} be an async payload record created from {label} and {path}. - Initialize {errors} on {deferRecord} to an empty list. - Let {dataExecution} be the asynchronous future value of: - Let {payload} be an unordered map. - - Let {data} be the result of {ExecuteSelectionSet(fragmentSelectionSet, - objectType, objectValue, variableValues, path, subsequentPayloads, - deferRecord)}. + - Initialize {resultMap} to an empty ordered map. + - For each {groupedFieldSet} as {responseKey} and {fields}: + - Let {fieldName} be the name of the first entry in {fields}. Note: This + value is unaffected if an alias is used. + - Let {fieldType} be the return type defined for the field {fieldName} of + {objectType}. + - If {fieldType} is defined: + - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, + fields, variableValues, path, subsequentPayloads, asyncRecord)}. + - Set {responseValue} as the value for {responseKey} in {resultMap}. - Append any encountered field errors to {errors}. - If {parentRecord} is defined: - Wait for the result of {dataExecution} on {parentRecord}. - If {errors} is not empty: - Add an entry to {payload} named `errors` with the value {errors}. - - Add an entry to {payload} named `data` with the value {data}. + - Add an entry to {payload} named `data` with the value {resultMap}. - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {path}. - Return {payload}. From 7b703dd78a0e17777539cd878cba33afb92d9995 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 18 Aug 2022 14:20:15 -0400 Subject: [PATCH 29/64] spec updates to reflect latest discussions --- spec/Section 6 -- Execution.md | 64 ++++++++++++++---------- spec/Section 7 -- Response.md | 91 +++++++++++++++++++++++++--------- 2 files changed, 107 insertions(+), 48 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 2ae221126..83d631f43 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -144,10 +144,10 @@ ExecuteQuery(query, schema, variableValues, initialValue): - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: - - Yield an unordered map containing {data}, {errors}, and an entry named - {hasNext} with the value {true}. + - Let {intialResponse} be an unordered map containing {data}, {errors}, and an + entry named {hasNext} with the value {true}. - Let {iterator} be the result of running - {YieldSubsequentPayloads(subsequentPayloads)}. + {YieldSubsequentPayloads(intialResponse, subsequentPayloads)}. - For each {payload} yielded by {iterator}: - If a termination signal is received: - Send a termination signal to {iterator}. @@ -178,10 +178,10 @@ ExecuteMutation(mutation, schema, variableValues, initialValue): - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: - - Yield an unordered map containing {data}, {errors}, and an entry named - {hasNext} with the value {true}. + - Let {intialResponse} be an unordered map containing {data}, {errors}, and an + entry named {hasNext} with the value {true}. - Let {iterator} be the result of running - {YieldSubsequentPayloads(subsequentPayloads)}. + {YieldSubsequentPayloads(intialResponse, subsequentPayloads)}. - For each {payload} yielded by {iterator}: - If a termination signal is received: - Send a termination signal to {iterator}. @@ -340,10 +340,10 @@ ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue): - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: - - Yield an unordered map containing {data}, {errors}, and an entry named - {hasNext} with the value {true}. + - Let {intialResponse} be an unordered map containing {data}, {errors}, and an + entry named {hasNext} with the value {true}. - Let {iterator} be the result of running - {YieldSubsequentPayloads(subsequentPayloads)}. + {YieldSubsequentPayloads(intialResponse, subsequentPayloads)}. - For each {payload} yielded by {iterator}: - If a termination signal is received: - Send a termination signal to {iterator}. @@ -371,29 +371,42 @@ If an operation contains subsequent payload records resulting from `@stream` or `@defer` directives, the {YieldSubsequentPayloads} algorithm defines how the payloads should be processed. -YieldSubsequentPayloads(subsequentPayloads): +YieldSubsequentPayloads(intialResponse, subsequentPayloads): +- Let {initialRecords} be any items in {subsequentPayloads} with a completed + {dataExecution}. +- Initialize {initialIncremental} to an empty list. +- For each {record} in {initialRecords}: + - Remove {record} from {subsequentPayloads}. + - If {isCompletedIterator} on {record} is {true}: + - Continue to the next record in {records}. + - Let {payload} be the completed result returned by {dataExecution}. + - Append {payload} to {initialIncremental}. +- If {initialIncremental} is not empty: + - Add an entry to {intialResponse} named `incremental` containing the value + {incremental}. +- Yield {intialResponse}. - While {subsequentPayloads} is not empty: - If a termination signal is received: - For each {record} in {subsequentPayloads}: - If {record} contains {iterator}: - Send a termination signal to {iterator}. - Return. -- Let {record} be the first item in {subsequentPayloads} with a completed +- Wait for at least one record in {subsequentPayloads} to have a completed {dataExecution}. - - Remove {record} from {subsequentPayloads}. - - If {isCompletedIterator} on {record} is {true}: - - If {subsequentPayloads} is empty: - - Yield a map containing a field `hasNext` with the value {false}. - - Return. - - If {subsequentPayloads} is not empty: - - Continue to the next record in {subsequentPayloads}. - - Let {payload} be the completed result returned by {dataExecution}. - - If {record} is not the final element in {subsequentPayloads}: - - Add an entry to {payload} named `hasNext` with the value {true}. - - If {record} is the final element in {subsequentPayloads}: - - Add an entry to {payload} named `hasNext` with the value {false}. - - Yield {payload} +- Let {subsequentResponse} be an unordered map with an entry {incremental} + initialized to an empty list. +- Let {records} be the items in {subsequentPayloads} with a completed + {dataExecution}. + - For each {record} in {records}: + - Remove {record} from {subsequentPayloads}. + - If {isCompletedIterator} on {record} is {true}: + - Continue to the next record in {records}. + - Let {payload} be the completed result returned by {dataExecution}. + - Append {payload} to {incremental}. + - If {subsequentPayloads} is empty: + - Add an entry to {response} named `hasNext` with the value {false}. + - Yield {response} ## Executing Selection Sets @@ -899,7 +912,8 @@ variableValues, subsequentPayloads): - Wait for the result of {dataExecution} on {parentRecord}. - If {errors} is not empty: - Add an entry to {payload} named `errors` with the value {errors}. - - Add an entry to {payload} named `data` with the value {data}. + - Add an entry to {payload} named `items` with a list containing the value + {data}. - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {itemPath}. - Return {payload}. diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 3743d4f8d..b2fe30a9f 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -23,11 +23,14 @@ 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 an event stream, the first value -will be the initial response. All subsequent values may contain `label` and -`path` entries. These two entries are used by clients to identify the `@defer` -or `@stream` directive from the GraphQL operation that triggered this value to -be returned by the event stream. When a label is provided, the combination of -these two entries will be unique across all values returned by the event stream. +will be the initial response. All subsequent values may contain an `incremental` +entry, containing a list of Defer or Stream responses. + +The `label` and `path` entries on Defer and Stream responses 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 event stream. When a label is provided, the combination of these +two entries will be unique across all values returned by the event stream. If the response of the GraphQL operation is an event stream, each response map must contain an entry with key `hasNext`. The value of this entry is `true` for @@ -45,11 +48,13 @@ 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. When the response of the GraphQL operation is an event stream, implementors may send subsequent payloads containing only -`hasNext` and `extensions` entries. +`hasNext` and `extensions` entries. Defer and Stream responses 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 @@ -62,11 +67,6 @@ requested operation. If the operation was a query, this output will be an object of the query root operation type; if the operation was a mutation, this output will be an object of the mutation root operation type. -If the result of the operation is an event stream, the `data` entry in -subsequent values 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 was raised before execution begins, the `data` entry should not be present in the result. @@ -263,7 +263,43 @@ discouraged. } ``` -## Path +### Incremental + +The `incremental` entry in the response is a non-empty list of Defer or Stream +responses. If the response of the GraphQL operation is an event stream, this +field may appear on both the initial and subsequent values. + +#### Stream response + +A stream response is a map that may appear as an item in the `incremental` entry +of a response. A stream response is the result of an associated `@stream` +directive in the operation. A stream response must contain `items` and `path` +entries and may contain `label`, `errors`, and `extensions` entries. + +##### Items + +The `items` entry in a stream response 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 `items` is set to +`null`, it indicates that an error has caused a `null` to bubble up to a field +higher than the list field with the associated `@stream` directive. + +#### Defer response + +A defer response is a map that may appear as an item in the `incremental` entry +of a response. A defer response is the result of an associated `@defer` +directive in the operation. A defer response must contain `data` and `path` +entries and may contain `label`, `errors`, and `extensions` entries. + +##### Data + +The `data` entry in a Defer response 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 `data` is set to `null`, it +indicates that 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. + +#### 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 @@ -273,21 +309,30 @@ 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 GraphQL response, it indicates that the -`data` field is not the root query or mutation result, but is rather associated -to a particular field in the root result. +When the `path` field is present on a Stream response, 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 response, 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 +#### Label -If the response of the GraphQL operation is an event stream, subsequent values -may contain a string field `label`. This `label` is the same label passed to the -`@defer` or `@stream` directive that triggered this value. 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. +Stream and Defer responses 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 From 47e1afa3d8fc248912afeb1bf6aba168dfa088e3 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 18 Aug 2022 14:26:33 -0400 Subject: [PATCH 30/64] Note about mutation execution order --- spec/Section 6 -- Execution.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 83d631f43..f4756002c 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -553,6 +553,11 @@ A correct executor must generate the following result for that selection set: } ``` +When subsections contain a `@stream` or `@defer` directive, these subsections +are no longer required to execute serially. Exeuction of the deferred or +streamed sections of the subsection may be executed in parallel, as defined in +{ExecuteStreamField} and {ExecuteDeferredFragment}. + ### Field Collection Before execution, the selection set is converted to a grouped field set by From fc297c30f76644d6ebb027f4cfa1ed4a8188120d Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 18 Aug 2022 14:38:06 -0400 Subject: [PATCH 31/64] minor change for uniqueness --- spec/Section 7 -- Response.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index b2fe30a9f..32dedbe14 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -30,7 +30,8 @@ The `label` and `path` entries on Defer and Stream responses 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 event stream. When a label is provided, the combination of these -two entries will be unique across all values returned by the event stream. +two entries will be unique across all Defer and Stream responses returned in the +event stream. If the response of the GraphQL operation is an event stream, each response map must contain an entry with key `hasNext`. The value of this entry is `true` for From 1a6d01877c13c227822fffe2ffc8ecfd7abc4075 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 18 Aug 2022 16:57:09 -0400 Subject: [PATCH 32/64] fix typos --- spec/Section 6 -- Execution.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f4756002c..1945225c7 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -144,10 +144,10 @@ ExecuteQuery(query, schema, variableValues, initialValue): - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: - - Let {intialResponse} be an unordered map containing {data}, {errors}, and an - entry named {hasNext} with the value {true}. + - Let {initialResponse} be an unordered map containing {data}, {errors}, and + an entry named {hasNext} with the value {true}. - Let {iterator} be the result of running - {YieldSubsequentPayloads(intialResponse, subsequentPayloads)}. + {YieldSubsequentPayloads(initialResponse, subsequentPayloads)}. - For each {payload} yielded by {iterator}: - If a termination signal is received: - Send a termination signal to {iterator}. @@ -178,10 +178,10 @@ ExecuteMutation(mutation, schema, variableValues, initialValue): - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: - - Let {intialResponse} be an unordered map containing {data}, {errors}, and an - entry named {hasNext} with the value {true}. + - Let {initialResponse} be an unordered map containing {data}, {errors}, and + an entry named {hasNext} with the value {true}. - Let {iterator} be the result of running - {YieldSubsequentPayloads(intialResponse, subsequentPayloads)}. + {YieldSubsequentPayloads(initialResponse, subsequentPayloads)}. - For each {payload} yielded by {iterator}: - If a termination signal is received: - Send a termination signal to {iterator}. @@ -340,10 +340,10 @@ ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue): - If {subsequentPayloads} is empty: - Return an unordered map containing {data} and {errors}. - If {subsequentPayloads} is not empty: - - Let {intialResponse} be an unordered map containing {data}, {errors}, and an - entry named {hasNext} with the value {true}. + - Let {initialResponse} be an unordered map containing {data}, {errors}, and + an entry named {hasNext} with the value {true}. - Let {iterator} be the result of running - {YieldSubsequentPayloads(intialResponse, subsequentPayloads)}. + {YieldSubsequentPayloads(initialResponse, subsequentPayloads)}. - For each {payload} yielded by {iterator}: - If a termination signal is received: - Send a termination signal to {iterator}. @@ -371,7 +371,7 @@ If an operation contains subsequent payload records resulting from `@stream` or `@defer` directives, the {YieldSubsequentPayloads} algorithm defines how the payloads should be processed. -YieldSubsequentPayloads(intialResponse, subsequentPayloads): +YieldSubsequentPayloads(initialResponse, subsequentPayloads): - Let {initialRecords} be any items in {subsequentPayloads} with a completed {dataExecution}. @@ -383,9 +383,9 @@ YieldSubsequentPayloads(intialResponse, subsequentPayloads): - Let {payload} be the completed result returned by {dataExecution}. - Append {payload} to {initialIncremental}. - If {initialIncremental} is not empty: - - Add an entry to {intialResponse} named `incremental` containing the value + - Add an entry to {initialResponse} named `incremental` containing the value {incremental}. -- Yield {intialResponse}. +- Yield {initialResponse}. - While {subsequentPayloads} is not empty: - If a termination signal is received: - For each {record} in {subsequentPayloads}: @@ -554,7 +554,7 @@ A correct executor must generate the following result for that selection set: ``` When subsections contain a `@stream` or `@defer` directive, these subsections -are no longer required to execute serially. Exeuction of the deferred or +are no longer required to execute serially. Execution of the deferred or streamed sections of the subsection may be executed in parallel, as defined in {ExecuteStreamField} and {ExecuteDeferredFragment}. From f4b38d1e47ce3584a9a8cb4c9675f950d34e1b6d Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 23 Aug 2022 12:25:56 -0400 Subject: [PATCH 33/64] if: Boolean! = true --- spec/Section 3 -- Type System.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 771c5c0b0..e0ff53adc 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2174,7 +2174,7 @@ scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") ```graphql directive @defer( label: String - if: Boolean + if: Boolean! = true ) on FRAGMENT_SPREAD | INLINE_FRAGMENT ``` @@ -2203,9 +2203,9 @@ fragment someFragment on User { #### @defer Arguments -- `if: Boolean` - When `true`, fragment _should_ be deferred. When `false`, - fragment will not be deferred and data will be included in the initial - response. If omitted, defaults to `true`. +- `if: Boolean! = true` - When `true`, fragment _should_ be deferred. When + `false`, fragment will not be deferred and data will be included in the + initial response. If omitted, defaults to `true`. - `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 Server must add it to the corresponding payload. `label` @@ -2215,7 +2215,11 @@ fragment someFragment on User { ### @stream ```graphql -directive @stream(label: String, initialCount: Int = 0, if: Boolean) on FIELD +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 @@ -2235,15 +2239,14 @@ query myQuery($shouldStream: Boolean) { #### @stream Arguments -- `if: Boolean` - When `true`, field _should_ be streamed. When `false`, the - field will not be streamed and all list items will be included in the initial - response. If omitted, defaults to `true`. +- `if: Boolean! = true` - When `true`, field _should_ be streamed. When `false`, + the field will not be streamed and all list items will be included in the + initial response. If omitted, defaults to `true`. - `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 Server 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 server 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`. From 437863f496325b3c96fd822798d7739868ea7acd Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 23 Aug 2022 19:59:38 -0400 Subject: [PATCH 34/64] address pr feedback --- spec/Section 6 -- Execution.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 1945225c7..b472b3dfc 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -387,26 +387,30 @@ YieldSubsequentPayloads(initialResponse, subsequentPayloads): {incremental}. - Yield {initialResponse}. - While {subsequentPayloads} is not empty: -- If a termination signal is received: - - For each {record} in {subsequentPayloads}: - - If {record} contains {iterator}: - - Send a termination signal to {iterator}. - - Return. -- Wait for at least one record in {subsequentPayloads} to have a completed - {dataExecution}. -- Let {subsequentResponse} be an unordered map with an entry {incremental} - initialized to an empty list. -- Let {records} be the items in {subsequentPayloads} with a completed - {dataExecution}. + - If a termination signal is received: + - For each {record} in {subsequentPayloads}: + - If {record} contains {iterator}: + - Send a termination signal to {iterator}. + - Return. + - Wait for at least one record in {subsequentPayloads} to have a completed + {dataExecution}. + - Let {subsequentResponse} be an unordered map with an entry {incremental} + initialized to an empty list. + - Let {records} be the items in {subsequentPayloads} with a completed + {dataExecution}. - For each {record} in {records}: - Remove {record} from {subsequentPayloads}. - If {isCompletedIterator} on {record} is {true}: - Continue to the next record in {records}. - Let {payload} be the completed result returned by {dataExecution}. - - Append {payload} to {incremental}. + - Append {payload} to the {incremental} entry on {subsequentResponse}. - If {subsequentPayloads} is empty: - - Add an entry to {response} named `hasNext` with the value {false}. - - Yield {response} + - Add an entry to {subsequentResponse} named `hasNext` with the value + {false}. + - Otherwise, if {subsequentPayloads} is not empty: + - Add an entry to {subsequentResponse} named `hasNext` with the value + {true}. + - Yield {subsequentResponse} ## Executing Selection Sets From 43602e557a3679a4901b675e4d147098a201c56b Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 23 Aug 2022 20:02:18 -0400 Subject: [PATCH 35/64] clarify null behavior of if --- spec/Section 3 -- Type System.md | 4 ++-- spec/Section 6 -- Execution.md | 37 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index e0ff53adc..1852c4bbf 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2205,7 +2205,7 @@ fragment someFragment on User { - `if: Boolean! = true` - When `true`, fragment _should_ be deferred. When `false`, fragment will not be deferred and data will be included in the - initial response. If omitted, defaults to `true`. + initial response. Defaults to `true` when omitted or null. - `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 Server must add it to the corresponding payload. `label` @@ -2241,7 +2241,7 @@ query myQuery($shouldStream: Boolean) { - `if: Boolean! = true` - When `true`, field _should_ be streamed. When `false`, the field will not be streamed and all list items will be included in the - initial response. If omitted, defaults to `true`. + initial response. Defaults to `true` when omitted or null. - `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 Server must add it to the corresponding payload. `label` diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index b472b3dfc..f93a3c86e 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -626,8 +626,8 @@ asyncRecord, visitedFragments, deferredGroupedFieldsList): - If {selection} is a {FragmentSpread}: - Let {fragmentSpreadName} be the name of {selection}. - If {fragmentSpreadName} provides the directive `@defer` and its {if} - argument is {true} or is a variable in {variableValues} with the value - {true}: + argument is not {false} and is not a variable in {variableValues} with the + value {false}: - Let {deferDirective} be that directive. - If {deferDirective} is not defined: - If {fragmentSpreadName} is in {visitedFragments}, continue with the next @@ -667,19 +667,20 @@ asyncRecord, visitedFragments, deferredGroupedFieldsList): fragmentType)} is false, continue with the next {selection} in {selectionSet}. - Let {fragmentSelectionSet} be the top-level selection set of {selection}. - - If {InlineFragment} provides the directive `@defer`, let {deferDirective} - be that directive. - - If {deferDirective}'s {if} argument is {true} or is a variable in - {variableValues} with the value {true}: - - Let {label} be the value or the variable to {deferDirective}'s {label} - argument. - - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, objectValue, fragmentSelectionSet, - variableValues, path, asyncRecord, visitedFragments, - deferredGroupedFieldsList)}. - - Append a record containing {label} and {deferredGroupedFields} to - {deferredGroupedFieldsList}. - - Continue with the next {selection} in {selectionSet}. + - If {InlineFragment} provides the directive `@defer` and its {if} argument + is not {false} and is not a variable in {variableValues} with the value + {false}: + - Let {deferDirective} be that directive. + - If {deferDirective} is defined: + - Let {label} be the value or the variable to {deferDirective}'s {label} + argument. + - Let {deferredGroupedFields} be the result of calling + {CollectFields(objectType, objectValue, fragmentSelectionSet, + variableValues, path, asyncRecord, visitedFragments, + deferredGroupedFieldsList)}. + - Append a record containing {label} and {deferredGroupedFields} to + {deferredGroupedFieldsList}. + - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, objectValue, fragmentSelectionSet, variableValues, path, asyncRecord, visitedFragments, @@ -944,9 +945,9 @@ subsequentPayloads, asyncRecord): - If {result} is an iterator: - Let {field} be the first entry in {fields}. - Let {innerType} be the inner type of {fieldType}. - - If {field} provides the directive `@stream` and its {if} argument is - {true} or is a variable in {variableValues} with the value {true} and - {innerType} is the outermost return type of the list type defined for + - If {field} provides the directive `@stream` and its {if} argument is not + {false} and is not a variable in {variableValues} with the value {false} + and {innerType} is the outermost return type of the list type defined for {field}: - Let {streamDirective} be that directive. - Let {initialCount} be the value or variable provided to From 8c8a7f502a5bfef4a06895101a94e96109e97f6f Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 11:03:48 -0400 Subject: [PATCH 36/64] Add error boundary behavior --- spec/Section 6 -- Execution.md | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f93a3c86e..f0f933b58 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -455,8 +455,11 @@ If during {ExecuteSelectionSet()} a field with a non-null {fieldType} raises a _field error_ then that error must propagate to this entire selection set, either resolving to {null} if allowed or further propagated to a parent field. -If this occurs, any sibling fields which have not yet executed or have not yet -yielded a value may be cancelled to avoid unnecessary work. +If this occurs, any defer or stream executions with a path that starts with the +same path as the resolved {null} must not return their results to the client. +These defer or stream executions or any sibling fields which have not yet +executed or have not yet yielded a value may be cancelled to avoid unnecessary +work. Note: See [Handling Field Errors](#sec-Handling-Field-Errors) for more about this behavior. @@ -747,7 +750,11 @@ variableValues, parentRecord, subsequentPayloads): - Wait for the result of {dataExecution} on {parentRecord}. - If {errors} is not empty: - Add an entry to {payload} named `errors` with the value {errors}. - - Add an entry to {payload} named `data` with the value {resultMap}. + - If a field error was raised, causing a {null} to be propagated to + {responseValue}, and {objectType} is a Non-Nullable type: + - Add an entry to {payload} named `data` with the value {null}. + - Otherwise: + - Add an entry to {payload} named `data` with the value {resultMap}. - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {path}. - Return {payload}. @@ -922,8 +929,12 @@ variableValues, subsequentPayloads): - Wait for the result of {dataExecution} on {parentRecord}. - If {errors} is not empty: - Add an entry to {payload} named `errors` with the value {errors}. - - Add an entry to {payload} named `items` with a list containing the value - {data}. + - If a field error was raised, causing a {null} to be propagated to {data}, + and {innerType} is a Non-Nullable type: + - Add an entry to {payload} named `items` with the value {null}. + - Otherwise: + - Add an entry to {payload} named `items` with a list containing the value + {data}. - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {itemPath}. - Return {payload}. @@ -1099,6 +1110,21 @@ resolves to {null}, then the entire list must resolve to {null}. If the `List` type is also wrapped in a `Non-Null`, the field error continues to propagate upwards. +When a field error is raised inside `ExecuteDeferredFragment` or +`ExecuteStreamField`, the defer and stream payloads act as error boundaries. +That is, the null resulting from a `Non-Null` type cannot propagate outside of +the boundary of the defer or stream payload. + +If a fragment with the `defer` directive is spread on a Non-Nullable object +type, and a field error has caused a {null} to propagate to the associated +object, the {null} should not propagate any further, and the associated Defer +Payload's `data` field must be set to {null}. + +If the `stream` directive is present on a list field with a Non-Nullable inner +type, and a field error has caused a {null} to propagate to the list item, the +{null} should not propagate any further, and the associated Stream Payload's +`item` field must be set to {null}. + If all fields from the root of the request to the source of the field error return `Non-Null` types, then the {"data"} entry in the response should be {null}. From 7e142aa7471a21aee9fa5ad91fd8d4082597981e Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 11:22:29 -0400 Subject: [PATCH 37/64] defer/stream response => payload --- spec/Section 7 -- Response.md | 54 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 32dedbe14..b8db8364e 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -24,13 +24,13 @@ validation error, this entry must not be present. When the response of the GraphQL operation is an event stream, the first value will be the initial response. All subsequent values may contain an `incremental` -entry, containing a list of Defer or Stream responses. +entry, containing a list of Defer or Stream payloads. -The `label` and `path` entries on Defer and Stream responses are used by clients +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 event stream. When a label is provided, the combination of these -two entries will be unique across all Defer and Stream responses returned in the +two entries will be unique across all Defer and Stream payloads returned in the event stream. If the response of the GraphQL operation is an event stream, each response map @@ -49,7 +49,7 @@ 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. When the response of the GraphQL operation is an event stream, implementors may send subsequent payloads containing only -`hasNext` and `extensions` entries. Defer and Stream responses may also contain +`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. @@ -267,38 +267,38 @@ discouraged. ### Incremental The `incremental` entry in the response is a non-empty list of Defer or Stream -responses. If the response of the GraphQL operation is an event stream, this +payloads. If the response of the GraphQL operation is an event stream, this field may appear on both the initial and subsequent values. -#### Stream response +#### Stream payload -A stream response is a map that may appear as an item in the `incremental` entry -of a response. A stream response is the result of an associated `@stream` -directive in the operation. A stream response must contain `items` and `path` +A stream payload is a map that may appear as an item in the `incremental` entry +of a response. A stream payload is the result of an associated `@stream` +directive in the operation. A stream payload must contain `items` and `path` entries and may contain `label`, `errors`, and `extensions` entries. ##### Items -The `items` entry in a stream response 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 `items` is set to -`null`, it indicates that an error has caused a `null` to bubble up to a field -higher than the list field with the associated `@stream` directive. +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 `items` is set to `null`, +it indicates that an error has caused a `null` to bubble up to a field higher +than the list field with the associated `@stream` directive. -#### Defer response +#### Defer payload -A defer response is a map that may appear as an item in the `incremental` entry -of a response. A defer response is the result of an associated `@defer` -directive in the operation. A defer response must contain `data` and `path` -entries and may contain `label`, `errors`, and `extensions` entries. +A defer payload is a map that may appear as an item in the `incremental` entry +of a response. A defer payload is the result of an associated `@defer` directive +in the operation. A defer payload must contain `data` and `path` entries and may +contain `label`, `errors`, and `extensions` entries. ##### Data -The `data` entry in a Defer response 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 `data` is set to `null`, it -indicates that 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. +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 `data` is set to `null`, it indicates +that 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. #### Path @@ -310,7 +310,7 @@ 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 response, it indicates that the +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` @@ -319,7 +319,7 @@ 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 response, it indicates that the +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. @@ -329,7 +329,7 @@ field which experienced the error. #### Label -Stream and Defer responses may contain a string field `label`. This `label` is +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 From 24968e0d5c2abfdd4f97232111c9bcfcf4756352 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 11:12:12 -0400 Subject: [PATCH 38/64] event stream => response stream --- spec/Section 7 -- Response.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index b8db8364e..bc656d353 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -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 or an event stream of maps. +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,33 +22,33 @@ 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 an event stream, the first value +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 event stream. When a label is provided, the combination of these -two entries will be unique across all Defer and Stream payloads returned in the -event stream. +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 an event stream, each response map +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 is not required for GraphQL operations that return a single response map. -The GraphQL server may determine there are no more values in the event stream +The GraphQL server 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 event stream should be a map without `data`, `label`, -and `path` entries, and a `hasNext` entry with a value of `false`. +case the last value in the response stream should be a map without `data`, +`label`, and `path` 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. When the response of the GraphQL operation is an -event stream, implementors may send subsequent payloads containing only +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. @@ -267,7 +267,7 @@ discouraged. ### Incremental The `incremental` entry in the response is a non-empty list of Defer or Stream -payloads. If the response of the GraphQL operation is an event stream, this +payloads. If the response of the GraphQL operation is a response stream, this field may appear on both the initial and subsequent values. #### Stream payload From 1174b4b1f11cd6065b6058bcd96f586228352e2d Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 11:14:27 -0400 Subject: [PATCH 39/64] link to path section --- spec/Section 7 -- Response.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index bc656d353..0be0b5e03 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -134,7 +134,7 @@ 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. The value of this -field is described in the "Path" section. +field is described in the [Path](#sec-Path) section. For example, if fetching one of the friends' names fails in the following operation: From 0ee9f407db1c0786d44c0f046f7db29dd8510482 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 11:15:16 -0400 Subject: [PATCH 40/64] use case no dash --- spec/Section 3 -- Type System.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 1852c4bbf..ccbe4b01c 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2255,8 +2255,8 @@ 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 servers honor the `@defer` and `@stream` -directives on each execution. However, the specification allows advanced -use-cases where the server can determine that it is more performant to not defer +directives on each execution. However, the specification allows advanced use +cases where the server 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 From 7a61ccb6b127e49388ceac59c563d539c8e8e142 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 11:15:56 -0400 Subject: [PATCH 41/64] remove "or null" --- spec/Section 3 -- Type System.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index ccbe4b01c..0d4b3aab0 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2205,7 +2205,7 @@ fragment someFragment on User { - `if: Boolean! = true` - When `true`, fragment _should_ be deferred. When `false`, fragment will not be deferred and data will be included in the - initial response. Defaults to `true` when omitted or null. + 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 Server must add it to the corresponding payload. `label` @@ -2241,7 +2241,7 @@ query myQuery($shouldStream: Boolean) { - `if: Boolean! = true` - When `true`, field _should_ be streamed. When `false`, the field will not be streamed and all list items will be included in the - initial response. Defaults to `true` when omitted or null. + 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 Server must add it to the corresponding payload. `label` From 46c74767778c98871c0526282724e958d91fda53 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 11:31:04 -0400 Subject: [PATCH 42/64] add detailed incremental example --- spec/Section 7 -- Response.md | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 0be0b5e03..477c964ca 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -270,6 +270,86 @@ The `incremental` entry in the response is a non-empty list of Defer or Stream payloads. If the response of the GraphQL operation is a response stream, this field may appear on both the initial and subsequent values. +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" }] + } + }, + "hasNext": true +} +``` + +Response 2, contains the defer payload and the first stream payload. + +```json example +{ + "incremental": [ + { + "label": "homeWorldDefer", + "path": ["person"], + "data": { "homeWorld": { "name": "Tatooine" } } + }, + { + "label": "filmsStream", + "path": ["person", "films", 1], + "items": [{ "title": "The Empire Strikes Back" }] + } + ], + "hasNext": true +} +``` + +Response 3, contains an additional stream payload. + +```json example +{ + "incremental": [ + { + "label": "filmsStream", + "path": ["person", "films", 2], + "items": [{ "title": "Return of the Jedi" }] + } + ], + "hasNext": true +} +``` + +Response 4, contains no incremental payloads, {hasNext} set to {false} indicates +the end of the stream. + +```json example +{ + "hasNext": false +} +``` + #### Stream payload A stream payload is a map that may appear as an item in the `incremental` entry From d07dc885c7d0958a059bcfd095b06a7a2f10d435 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 12:22:32 -0400 Subject: [PATCH 43/64] update label validation rule --- spec/Section 5 -- Validation.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index c6bb6c4b3..b2293d467 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1560,17 +1560,17 @@ mutation { ** Formal Specification ** -- For every {directive} in a 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. - - Let {labels} be all label values passed to defer or stream directive label - arguments. - - {labels} must be a set of one. +- 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** From 686115924e4430e8e5f9f44e2a661dd22940724c Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 17:27:57 -0400 Subject: [PATCH 44/64] clarify hasNext on incremental example --- spec/Section 7 -- Response.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 477c964ca..5cf6fc567 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -41,8 +41,8 @@ operations that return a single response map. The GraphQL server 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`, -`label`, and `path` entries, and a `hasNext` entry with a value of `false`. +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 @@ -326,7 +326,10 @@ Response 2, contains the defer payload and the first stream payload. } ``` -Response 3, contains an additional stream payload. +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 {true} and this +would be the final response. ```json example { @@ -341,8 +344,9 @@ Response 3, contains an additional stream payload. } ``` -Response 4, contains no incremental payloads, {hasNext} set to {false} indicates -the end of the stream. +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 { From f760bc1199f6466e056eb377745c6221216de6b5 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 17:45:10 -0400 Subject: [PATCH 45/64] clarify canceling of subsequent payloads --- spec/Section 6 -- Execution.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f0f933b58..cdd5ef5c7 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -455,11 +455,16 @@ If during {ExecuteSelectionSet()} a field with a non-null {fieldType} raises a _field error_ then that error must propagate to this entire selection set, either resolving to {null} if allowed or further propagated to a parent field. -If this occurs, any defer or stream executions with a path that starts with the -same path as the resolved {null} must not return their results to the client. -These defer or stream executions or any sibling fields which have not yet -executed or have not yet yielded a value may be cancelled to avoid unnecessary -work. +If this occurs, any sibling fields which have not yet executed or have not yet +yielded a value may be cancelled to avoid unnecessary work. + +Additionally, the path of each {asyncRecord} in {subsequentPayloads} must be +compared with the path of the field that ultimately resolved to {null}. If the +path of any {asyncRecord} starts with, but is not equal to, the path of the +resolved {null}, the {asyncRecord} must be removed from {subsequentPayloads} and +its result must not be sent to clients. If these async records have not yet +executed or have not yet yielded a value they may also be cancelled to avoid +unnecessary work. Note: See [Handling Field Errors](#sec-Handling-Field-Errors) for more about this behavior. From f021320f58ca93f110d6df7ace34f09dd9a12a15 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Thu, 8 Sep 2022 18:20:12 -0400 Subject: [PATCH 46/64] Add examples for non-null cases --- spec/Section 6 -- Execution.md | 149 ++++++++++++++++++++++++++++++--- 1 file changed, 139 insertions(+), 10 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index cdd5ef5c7..0e743fe35 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -460,11 +460,35 @@ yielded a value may be cancelled to avoid unnecessary work. Additionally, the path of each {asyncRecord} in {subsequentPayloads} must be compared with the path of the field that ultimately resolved to {null}. If the -path of any {asyncRecord} starts with, but is not equal to, the path of the -resolved {null}, the {asyncRecord} must be removed from {subsequentPayloads} and -its result must not be sent to clients. If these async records have not yet -executed or have not yet yielded a value they may also be cancelled to avoid -unnecessary work. +path of any {asyncRecord} starts with the path of the resolved {null}, the +{asyncRecord} must be removed from {subsequentPayloads} and its result must not +be sent to clients. If these async records have not yet executed or have not yet +yielded a value they may also be cancelled to avoid unnecessary work. + +For example, assume the field `alwaysThrows` is a `Non-Null` type that always +raises a field error: + +```graphql example +{ + myObject { + ... @defer { + name + } + alwaysThrows + } +} +``` + +In this case, only one response should be sent. The async payload record +associated with the `@defer` directive should be removed and it's execution may +be cancelled. + +```json example +{ + "data": { "myObject": null }, + "hasNext": false +} +``` Note: See [Handling Field Errors](#sec-Handling-Field-Errors) for more about this behavior. @@ -756,7 +780,7 @@ variableValues, parentRecord, subsequentPayloads): - If {errors} is not empty: - Add an entry to {payload} named `errors` with the value {errors}. - If a field error was raised, causing a {null} to be propagated to - {responseValue}, and {objectType} is a Non-Nullable type: + {responseValue}: - Add an entry to {payload} named `data` with the value {null}. - Otherwise: - Add an entry to {payload} named `data` with the value {resultMap}. @@ -1120,16 +1144,121 @@ When a field error is raised inside `ExecuteDeferredFragment` or That is, the null resulting from a `Non-Null` type cannot propagate outside of the boundary of the defer or stream payload. -If a fragment with the `defer` directive is spread on a Non-Nullable object -type, and a field error has caused a {null} to propagate to the associated -object, the {null} should not propagate any further, and the associated Defer -Payload's `data` field must be set to {null}. +If a field error is raised while executing the selection set of a fragment with +the `defer` directive, causing a {null} to propagate to the object containing +this fragment, the {null} should not propagate any further. In this case, the +associated Defer Payload's `data` field must be set to {null}. + +For example, assume the `month` field is a `Non-Null` type that raises a field +error: + +```graphql example +{ + birthday { + ... @defer { + month + } + } +} +``` + +Response 1, the initial response is sent: + +```json example +{ + "data": { "birthday": {} }, + "hasNext": true +} +``` + +Response 2, the defer payload is sent. The {data} entry has been set to {null}, +as this {null} as propagated as high as the error boundary will allow. + +```json example +{ + "incremental": [ + { + "path": ["birthday"], + "data": null + } + ], + "hasNext": false +} +``` If the `stream` directive is present on a list field with a Non-Nullable inner type, and a field error has caused a {null} to propagate to the list item, the {null} should not propagate any further, and the associated Stream Payload's `item` field must be set to {null}. +For example, assume the `films` field is a `List` type with an `Non-Null` inner +type. In this case, the second list item raises a field error: + +```graphql example +{ + films @stream(initialCount: 1) +} +``` + +Response 1, the initial response is sent: + +```json example +{ + "data": { "films": ["A New Hope"] }, + "hasNext": true +} +``` + +Response 2, the first stream payload is sent. The {items} entry has been set to +{null}, as this {null} as propagated as high as the error boundary will allow. + +```json example +{ + "incremental": [ + { + "path": ["films", 1], + "items": null + } + ], + "hasNext": false +} +``` + +In this alternative example, assume the `films` field is a `List` type without a +`Non-Null` inner type. In this case, the second list item also raises a field +error: + +```graphql example +{ + films @stream(initialCount: 1) +} +``` + +Response 1, the initial response is sent: + +```json example +{ + "data": { "films": ["A New Hope"] }, + "hasNext": true +} +``` + +Response 2, the first stream payload is sent. The {items} entry has been set to +a list containing {null}, as this {null} has only propagated as high as the list +item. + +```json example +{ + "incremental": [ + { + "path": ["films", 1], + "items": [null] + } + ], + "hasNext": false +} +``` + If all fields from the root of the request to the source of the field error return `Non-Null` types, then the {"data"} entry in the response should be {null}. From befc0576a3c4e910707cf43f72df02be120c8650 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 9 Sep 2022 06:58:54 -0400 Subject: [PATCH 47/64] typo --- spec/Section 6 -- Execution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 0e743fe35..3078bfe09 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -480,7 +480,7 @@ raises a field error: ``` In this case, only one response should be sent. The async payload record -associated with the `@defer` directive should be removed and it's execution may +associated with the `@defer` directive should be removed and its execution may be cancelled. ```json example From c658f77a913c400c8c393ba440fd4278c43b7ef6 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 9 Sep 2022 06:59:06 -0400 Subject: [PATCH 48/64] improve non-null example --- spec/Section 6 -- Execution.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 3078bfe09..1b50f37bd 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -1155,9 +1155,12 @@ error: ```graphql example { birthday { - ... @defer { + ... @defer(label: "monthDefer") { month } + ... @defer(label: "yearDefer") { + year + } } } ``` @@ -1171,14 +1174,16 @@ Response 1, the initial response is sent: } ``` -Response 2, the defer payload is sent. The {data} entry has been set to {null}, -as this {null} as propagated as high as the error boundary will allow. +Response 2, the defer payload for label "monthDefer" is sent. The {data} entry +has been set to {null}, as this {null} as propagated as high as the error +boundary will allow. ```json example { "incremental": [ { "path": ["birthday"], + "label": "monthDefer", "data": null } ], @@ -1186,6 +1191,22 @@ as this {null} as propagated as high as the error boundary will allow. } ``` +Response 3, the defer payload for label "yearDefer" is sent. The data in this +payload is unaffected by the previous null error. + +```json example +{ + "incremental": [ + { + "path": ["birthday"], + "label": "yearDefer", + "data": { "year": "2022" } + } + ], + "hasNext": false +} +``` + If the `stream` directive is present on a list field with a Non-Nullable inner type, and a field error has caused a {null} to propagate to the list item, the {null} should not propagate any further, and the associated Stream Payload's From 7ae29d8959c44d9641d81a8c6aa15a46e453d28c Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Fri, 9 Sep 2022 07:26:51 -0400 Subject: [PATCH 49/64] Add FilterSubsequentPayloads algorithm --- spec/Section 6 -- Execution.md | 48 +++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 1b50f37bd..3d0ccfd1d 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -458,12 +458,45 @@ either resolving to {null} if allowed or further propagated to a parent field. If this occurs, any sibling fields which have not yet executed or have not yet yielded a value may be cancelled to avoid unnecessary work. -Additionally, the path of each {asyncRecord} in {subsequentPayloads} must be -compared with the path of the field that ultimately resolved to {null}. If the -path of any {asyncRecord} starts with the path of the resolved {null}, the -{asyncRecord} must be removed from {subsequentPayloads} and its result must not -be sent to clients. If these async records have not yet executed or have not yet -yielded a value they may also be cancelled to avoid unnecessary work. +Additionally, async payload records in {subsequentPayloads} must be filtered if +their path points to a location that has resolved to {null} due to propagation +of a field error. This is described in +[Filter Subsequent Payloads](#sec-Filter-Subsequent-Payloads). These async +payload records must be removed from {subsequentPayloads} and their result must +not be sent to clients. If these async records have not yet executed or have not +yet yielded a value they may also be cancelled to avoid unnecessary work. + +Note: See [Handling Field Errors](#sec-Handling-Field-Errors) for more about +this behavior. + +### Filter Subsequent Payloads + +When a field error is raised, there may be async payload records in +{subsequentPayloads} with a path that points to a location that has been removed +or set to null due to null propagation. These async payload records must be +removed from subsequent payloads and their results must not be sent to clients. + +In {FilterSubsequentPayloads}, {nullPath} is the path which has resolved to null +after propagation as a result of a field error. {currentAsyncRecord} is the +async payload record where the field error was raised. {currentAsyncRecord} will +not be set for field errors that were raised during the initial execution +outside of {ExecuteDeferredFragment} or {ExecuteStreamField}. + +FilterSubsequentPayloads(subsequentPayloads, nullPath, currentAsyncRecord): + +- For each {asyncRecord} in {subsequentPayloads}: + - If {asyncRecord} is the same record as {currentAsyncRecord}: + - Continue to the next record in {subsequentPayloads}. + - Initialize {index} to zero. + - While {index} is less then the length of {nullPath}: + - Initialize {nullPathItem} to the element at {index} in {nullPath}. + - Initialize {asyncRecordPathItem} to the element at {index} in the {path} + of {asyncRecord}. + - If {nullPathItem} is not equivalent to {asyncRecordPathItem}: + - Continue to the next record in {subsequentPayloads}. + - Increment {index} by one. + - Remove {asyncRecord} from {subsequentPayloads}. Optionally, cancel any + incomplete work in the execution of {asyncRecord}. For example, assume the field `alwaysThrows` is a `Non-Null` type that always raises a field error: @@ -490,9 +523,6 @@ be cancelled. } ``` -Note: See [Handling Field Errors](#sec-Handling-Field-Errors) for more about -this behavior. - ### Normal and Serial Execution Normally the executor can execute the entries in a grouped field set in whatever From bece8a056dc50ddb1ee5f9e3a66acc141fd2192c Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 12 Oct 2022 17:22:54 -0400 Subject: [PATCH 50/64] link to note on should --- spec/Section 3 -- Type System.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 0d4b3aab0..97b17e99c 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2203,9 +2203,10 @@ fragment someFragment on User { #### @defer Arguments -- `if: Boolean! = true` - When `true`, fragment _should_ be deferred. When - `false`, fragment will not be deferred and data will be included in the - initial response. Defaults to `true` when omitted. +- `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 Server must add it to the corresponding payload. `label` @@ -2239,9 +2240,10 @@ query myQuery($shouldStream: Boolean) { #### @stream Arguments -- `if: Boolean! = true` - When `true`, field _should_ be streamed. When `false`, - the field will not be streamed and all list items will be included in the - initial response. Defaults to `true` when omitted. +- `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 Server must add it to the corresponding payload. `label` From 056c77df6db372d94559972d687d6c36ec532eed Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 1 Nov 2022 15:35:52 -0400 Subject: [PATCH 51/64] update on hasNext --- spec/Section 7 -- Response.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 5cf6fc567..e00f5a254 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -36,7 +36,7 @@ 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 is not required for GraphQL +the last response of the stream. This entry must not be present for GraphQL operations that return a single response map. The GraphQL server may determine there are no more values in the response stream From fae5cdde93ac609a95a4634caef1e84c9592c9f0 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 7 Nov 2022 16:07:31 -0500 Subject: [PATCH 52/64] small fixes (#3) * add comma * remove unused parameter --- spec/Section 6 -- Execution.md | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 3d0ccfd1d..1cf078077 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -428,8 +428,7 @@ subsequentPayloads, asyncRecord): - If {path} is not provided, initialize it to an empty list. - If {subsequentPayloads} is not provided, initialize it to the empty set. - Let {groupedFieldSet} and {deferredGroupedFieldsList} be the result of - {CollectFields(objectType, objectValue, selectionSet, variableValues, path, - asyncRecord)}. + {CollectFields(objectType, selectionSet, variableValues, path, asyncRecord)}. - Initialize {resultMap} to an empty ordered map. - For each {groupedFieldSet} as {responseKey} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value @@ -661,8 +660,8 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. -CollectFields(objectType, objectValue, selectionSet, variableValues, path, -asyncRecord, visitedFragments, deferredGroupedFieldsList): +CollectFields(objectType, selectionSet, variableValues, path, asyncRecord, +visitedFragments, deferredGroupedFieldsList): - If {visitedFragments} is not provided, initialize it to the empty set. - Initialize {groupedFields} to an empty ordered map of lists. @@ -707,16 +706,14 @@ asyncRecord, visitedFragments, deferredGroupedFieldsList): - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, objectValue, fragmentSelectionSet, - variableValues, path, asyncRecord, visitedFragments, - deferredGroupedFieldsList)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, path, + asyncRecord, visitedFragments, deferredGroupedFieldsList)}. - Append a record containing {label} and {deferredGroupedFields} to {deferredGroupedFieldsList}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, objectValue, fragmentSelectionSet, - variableValues, path, asyncRecord, visitedFragments, - deferredGroupedFieldsList)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, path, + asyncRecord, visitedFragments, deferredGroupedFieldsList)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -737,16 +734,14 @@ asyncRecord, visitedFragments, deferredGroupedFieldsList): - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, objectValue, fragmentSelectionSet, - variableValues, path, asyncRecord, visitedFragments, - deferredGroupedFieldsList)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, path, + asyncRecord, visitedFragments, deferredGroupedFieldsList)}. - Append a record containing {label} and {deferredGroupedFields} to {deferredGroupedFieldsList}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, objectValue, fragmentSelectionSet, - variableValues, path, asyncRecord, visitedFragments, - deferredGroupedFieldsList)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, path, + asyncRecord, visitedFragments, deferredGroupedFieldsList)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -964,8 +959,8 @@ directive. #### Execute Stream Field -ExecuteStreamField(label, iterator, index, fields, innerType, path streamRecord, -variableValues, subsequentPayloads): +ExecuteStreamField(label, iterator, index, fields, innerType, path, +streamRecord, variableValues, subsequentPayloads): - Let {streamRecord} be an async payload record created from {label}, {path}, and {iterator}. From cc1a7a2567d7ecfc4610f9f452f37127a06bf9c1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Wed, 16 Nov 2022 17:15:20 +0200 Subject: [PATCH 53/64] remove ResolveFIeldGenerator (#4) * streamline stream execution Currently, these spec changes introduce a new internal function named `ResolveFieldGenerator` that is suggested parallels `ResolveFieldValue`. This function is used during field execution such that if the stream directive is specified, it is called instead of `ResolveFieldValue`. The reference implementation, however, does not require any such function, simply utilizing the result of `ResolveFieldValue`. With incremental delivery, collections completed by `CompleteValue` should be explicitly iterated using a well-defined iterator, such that the iterator can be passed to `ExecuteStreamField`. But this does not require a new internal function to be specified/exposed. Moreover, introducing this function causes a mixing of concerns between the `ExecuteField` and `CompleteValue` algorithms; Currently, if stream is specified for a field, `ExecuteField` extracts the iterator and passes it to `CompleteValue`, while if stream is not specified, the `ExecuteField` passes the collection, i.e. the iterable, not the iterator. In the stream case, this shunts some of the logic checking the validity of resolution results into field execution. In fact, it exposes a specification "bug" => in the stream case, no checking is actually done that `ResolveFieldGenerator` returns an iterator! This change removes `ResolveFieldGenerator` and with it some complexity, and brings it in line with the reference implementation. The reference implementation contains some simplification of the algorithm for the synchronous iterator case (we don't have to preserve the iterator on the StreamRecord, because there will be no early close required and we don't have to set isCompletedIterator, beacuse we don't have to create a dummy payload for termination of the asynchronous stream), We could consider also removing these bits as well, as they are an implementation detail in terms of how our dispatcher is managing its iterators, but that should be left for another change. * run prettier --- spec/Section 6 -- Execution.md | 117 +++++++++++++-------------------- 1 file changed, 44 insertions(+), 73 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 1cf078077..ac2520336 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -831,15 +831,6 @@ subsequentPayloads, asyncRecord): - Append {fieldName} to {path}. - Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} -- If {field} provides the directive `@stream`, let {streamDirective} be that - directive. - - Let {initialCount} be the value or variable provided to {streamDirective}'s - {initialCount} argument. - - Let {resolvedValue} be {ResolveFieldGenerator(objectType, objectValue, - fieldName, argumentValues)}. - - Let {result} be the result of calling {CompleteValue(fieldType, fields, - resolvedValue, variableValues, path, subsequentPayloads, asyncRecord)}. - - Return {result}. - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. - Let {result} be the result of calling {CompleteValue(fieldType, fields, @@ -907,20 +898,17 @@ must only allow usage of variables of appropriate types. While nearly all of GraphQL execution can be described generically, ultimately the internal system exposing the GraphQL interface must provide values. This is exposed via {ResolveFieldValue}, which produces a value for a given field on a -type for a real value. In addition, {ResolveFieldGenerator} will be exposed to -produce an iterator for a field with `List` return type. The internal system may -optionally define a generator function. In the case where the generator is not -defined, the GraphQL executor provides a default generator. For example, a -trivial generator that yields the entire list upon the first iteration. +type for a real value. -As an example, a {ResolveFieldValue} might accept the {objectType} `Person`, the -{field} {"soulMate"}, and the {objectValue} representing John Lennon. It would -be expected to yield the value representing Yoko Ono. +As an example, this might accept the {objectType} `Person`, the {field} +{"soulMate"}, and the {objectValue} representing John Lennon. It would be +expected to yield the value representing Yoko Ono. -A {ResolveFieldGenerator} might accept the {objectType} `MusicBand`, the {field} -{"members"}, and the {objectValue} representing Beatles. It would be expected to -yield a iterator of values representing John Lennon, Paul McCartney, Ringo Starr -and George Harrison. +List values are resolved similarly. For example, {ResolveFieldValue} might also +accept the {objectType} `MusicBand`, the {field} {"members"}, and the +{objectValue} representing the Beatles. It would be expected to yield a +collection of values representing John Lennon, Paul McCartney, Ringo Starr and +George Harrison. ResolveFieldValue(objectType, objectValue, fieldName, argumentValues): @@ -929,33 +917,23 @@ ResolveFieldValue(objectType, objectValue, fieldName, argumentValues): - Return the result of calling {resolver}, providing {objectValue} and {argumentValues}. -ResolveFieldGenerator(objectType, objectValue, fieldName, argumentValues): - -- If {objectType} provide an internal function {generatorResolver} for - generating partially resolved value of a list field named {fieldName}: - - Let {generatorResolver} be the internal function. - - Return the iterator from calling {generatorResolver}, providing - {objectValue} and {argumentValues}. -- Create {generator} from {ResolveFieldValue(objectType, objectValue, fieldName, - argumentValues)}. -- Return {generator}. - Note: It is common for {resolver} to be asynchronous due to relying on reading an underlying database or networked service to produce a value. This necessitates the rest of a GraphQL executor to handle an asynchronous execution -flow. In addition, a common implementation of {generator} is to leverage -asynchronous iterators or asynchronous generators provided by many programming -languages. +flow. In addition, an implementation for collections may leverage asynchronous +iterators or asynchronous generators provided by many programming languages. +This may be particularly helpful when used in conjunction with the `@stream` +directive. ### Value Completion After resolving the value for a field, it is completed by ensuring it adheres to the expected return type. If the return type is another Object type, then the -field execution process continues recursively. In the case where a value -returned for a list type field is an iterator due to `@stream` specified on the -field, value completion iterates over the iterator until the number of items -yield by the iterator satisfies `initialCount` specified on the `@stream` -directive. +field execution process continues recursively. If the return type is a List +type, each member of the resolved collection is completed using the same value +completion process. In the case where `@stream` is specified on a field of list +type, value completion iterates over the collection until the number of items +yielded items satisfies `initialCount` specified on the `@stream` directive. #### Execute Stream Field @@ -1007,45 +985,38 @@ subsequentPayloads, asyncRecord): - If {result} is {null} (or another internal value similar to {null} such as {undefined}), return {null}. - If {fieldType} is a List type: - - If {result} is an iterator: - - Let {field} be the first entry in {fields}. - - Let {innerType} be the inner type of {fieldType}. - - If {field} provides the directive `@stream` and its {if} argument is not - {false} and is not a variable in {variableValues} with the value {false} - and {innerType} is the outermost return type of the list type defined for - {field}: - - Let {streamDirective} be that directive. + - If {result} is not a collection of values, raise a _field error_. + - Let {field} be the first entry in {fields}. + - Let {innerType} be the inner type of {fieldType}. + - If {field} provides the directive `@stream` and its {if} argument is not + {false} and is not a variable in {variableValues} with the value {false} and + {innerType} is the outermost return type of the list type defined for + {field}: + - Let {streamDirective} be that directive. - Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument. - If {initialCount} is less than zero, raise a _field error_. - Let {label} be the value or variable provided to {streamDirective}'s {label} argument. - - Let {initialItems} be an empty list - - Let {index} be zero. + - Let {iterator} be an iterator for {result}. + - Let {items} be an empty list. + - Let {index} be zero. - While {result} is not closed: - - If {streamDirective} is not defined or {index} is not greater than or - equal to {initialCount}: - - Wait for the next item from {result}. - - Let {resultItem} be the item retrieved from {result}. - - Let {itemPath} be {path} with {index} appended. - - Let {resolvedItem} be the result of calling {CompleteValue(innerType, - fields, resultItem, variableValues, itemPath, subsequentPayloads, - asyncRecord)}. - - Append {resolvedItem} to {initialItems}. - - Increment {index}. - - If {streamDirective} is defined and {index} is greater than or equal to - {initialCount}: - - Call {ExecuteStreamField(label, result, index, fields, innerType, - path, asyncRecord, subsequentPayloads)}. - - Let {result} be {initialItems}. - - Exit while loop. - - Return {initialItems}. - - If {result} is not a collection of values, raise a _field error_. - - Let {innerType} be the inner type of {fieldType}. - - Return a list where each list item is the result of calling - {CompleteValue(innerType, fields, resultItem, variableValues, itemPath, - subsequentPayloads, asyncRecord)}, where {resultItem} is each item in - {result} and {itemPath} is {path} with the index of the item appended. + - If {streamDirective} is defined and {index} is greater than or equal to + {initialCount}: + - Call {ExecuteStreamField(label, iterator, index, fields, innerType, + path, asyncRecord, subsequentPayloads)}. + - Return {items}. + - Otherwise: + - Retrieve the next item from {result} via the {iterator}. + - Let {resultItem} be the item retrieved from {result}. + - Let {itemPath} be {path} with {index} appended. + - Let {resolvedItem} be the result of calling {CompleteValue(innerType, + fields, resultItem, variableValues, itemPath, subsequentPayloads, + asyncRecord)}. + - Append {resolvedItem} to {initialItems}. + - Increment {index}. + - Return {items}. - If {fieldType} is a Scalar or Enum type: - Return the result of {CoerceResult(fieldType, result)}. - If {fieldType} is an Object, Interface, or Union type: From 185374a8185baa813c84724fbf1d51e3708e44cf Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 18 Nov 2022 16:31:34 +0200 Subject: [PATCH 54/64] fix typos (#6) * fix whitespace * complete renaming of initialItems --- spec/Section 6 -- Execution.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index ac2520336..35b4f2c1a 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -1001,7 +1001,7 @@ subsequentPayloads, asyncRecord): - Let {iterator} be an iterator for {result}. - Let {items} be an empty list. - Let {index} be zero. - - While {result} is not closed: + - While {result} is not closed: - If {streamDirective} is defined and {index} is greater than or equal to {initialCount}: - Call {ExecuteStreamField(label, iterator, index, fields, innerType, @@ -1014,9 +1014,9 @@ subsequentPayloads, asyncRecord): - Let {resolvedItem} be the result of calling {CompleteValue(innerType, fields, resultItem, variableValues, itemPath, subsequentPayloads, asyncRecord)}. - - Append {resolvedItem} to {initialItems}. + - Append {resolvedItem} to {items}. - Increment {index}. - - Return {items}. + - Return {items}. - If {fieldType} is a Scalar or Enum type: - Return the result of {CoerceResult(fieldType, result)}. - If {fieldType} is an Object, Interface, or Union type: From 57ec651126620c556106c762595aa83d3ee36804 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 21 Nov 2022 21:33:58 +0200 Subject: [PATCH 55/64] Add error handling for stream iterators (#5) * Add error handling for stream iterators * also add iterator error handling within CompleteValue * incorporate feedback --- spec/Section 6 -- Execution.md | 37 +++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 35b4f2c1a..93b974a47 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -950,25 +950,29 @@ streamRecord, variableValues, subsequentPayloads): - Set {isCompletedIterator} to {true} on {streamRecord}. - Return {null}. - Let {payload} be an unordered map. - - Let {item} be the item retrieved from {iterator}. - - Let {data} be the result of calling {CompleteValue(innerType, fields, item, - variableValues, itemPath, subsequentPayloads, parentRecord)}. - - Append any encountered field errors to {errors}. - - Increment {index}. - - Call {ExecuteStreamField(label, iterator, index, fields, innerType, path, - streamRecord, variableValues, subsequentPayloads)}. - - If {parentRecord} is defined: - - Wait for the result of {dataExecution} on {parentRecord}. - - If {errors} is not empty: - - Add an entry to {payload} named `errors` with the value {errors}. - - If a field error was raised, causing a {null} to be propagated to {data}, - and {innerType} is a Non-Nullable type: + - If an item is not retrieved because of an error: + - Append the encountered error to {errors}. - Add an entry to {payload} named `items` with the value {null}. - Otherwise: - - Add an entry to {payload} named `items` with a list containing the value - {data}. + - Let {item} be the item retrieved from {iterator}. + - Let {data} be the result of calling {CompleteValue(innerType, fields, + item, variableValues, itemPath, subsequentPayloads, parentRecord)}. + - Append any encountered field errors to {errors}. + - Increment {index}. + - Call {ExecuteStreamField(label, iterator, index, fields, innerType, path, + streamRecord, variableValues, subsequentPayloads)}. + - If a field error was raised, causing a {null} to be propagated to {data}, + and {innerType} is a Non-Nullable type: + - Add an entry to {payload} named `items` with the value {null}. + - Otherwise: + - Add an entry to {payload} named `items` with a list containing the value + {data}. + - If {errors} is not empty: + - Add an entry to {payload} named `errors` with the value {errors}. - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {itemPath}. + - If {parentRecord} is defined: + - Wait for the result of {dataExecution} on {parentRecord}. - Return {payload}. - Set {dataExecution} on {streamRecord}. - Append {streamRecord} to {subsequentPayloads}. @@ -1008,7 +1012,8 @@ subsequentPayloads, asyncRecord): path, asyncRecord, subsequentPayloads)}. - Return {items}. - Otherwise: - - Retrieve the next item from {result} via the {iterator}. + - Wait for the next item from {result} via the {iterator}. + - If an item is not retrieved because of an error, raise a _field error_. - Let {resultItem} be the item retrieved from {result}. - Let {itemPath} be {path} with {index} appended. - Let {resolvedItem} be the result of calling {CompleteValue(innerType, From feab837fb252a2c133ecb736e319830cce965158 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 22 Nov 2022 13:25:12 -0500 Subject: [PATCH 56/64] Raise a field error if defer/stream encountered during subscription execution --- spec/Section 6 -- Execution.md | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 93b974a47..2a15f5304 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -328,28 +328,15 @@ MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues): ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue): -- Let {subsequentPayloads} be an empty list. - Let {subscriptionType} be the root Subscription type in {schema}. - Assert: {subscriptionType} is an Object type. - Let {selectionSet} be the top level Selection Set in {subscription}. - Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - subscriptionType, initialValue, variableValues, subsequentPayloads)} - _normally_ (allowing parallelization). + subscriptionType, initialValue, variableValues)} _normally_ (allowing + parallelization). - Let {errors} be the list of all _field error_ raised while executing the selection set. -- If {subsequentPayloads} is empty: - - Return an unordered map containing {data} and {errors}. -- If {subsequentPayloads} is not empty: - - Let {initialResponse} be an unordered map containing {data}, {errors}, and - an entry named {hasNext} with the value {true}. - - Let {iterator} be the result of running - {YieldSubsequentPayloads(initialResponse, subsequentPayloads)}. - - For each {payload} yielded by {iterator}: - - If a termination signal is received: - - Send a termination signal to {iterator}. - - Return. - - Otherwise: - - Yield {payload}. +- Return an unordered map containing {data} and {errors}. Note: The {ExecuteSubscriptionEvent()} algorithm is intentionally similar to {ExecuteQuery()} since this is how each event result is produced. @@ -690,6 +677,8 @@ visitedFragments, deferredGroupedFieldsList): argument is not {false} and is not a variable in {variableValues} with the value {false}: - Let {deferDirective} be that directive. + - If this execution is for a subscription operation, raise a _field + error_. - If {deferDirective} is not defined: - If {fragmentSpreadName} is in {visitedFragments}, continue with the next {selection} in {selectionSet}. @@ -730,6 +719,8 @@ visitedFragments, deferredGroupedFieldsList): is not {false} and is not a variable in {variableValues} with the value {false}: - Let {deferDirective} be that directive. + - If this execution is for a subscription operation, raise a _field + error_. - If {deferDirective} is defined: - Let {label} be the value or the variable to {deferDirective}'s {label} argument. @@ -997,6 +988,7 @@ subsequentPayloads, asyncRecord): {innerType} is the outermost return type of the list type defined for {field}: - Let {streamDirective} be that directive. + - If this execution is for a subscription operation, raise a _field error_. - Let {initialCount} be the value or variable provided to {streamDirective}'s {initialCount} argument. - If {initialCount} is less than zero, raise a _field error_. From f5398fb8e74ceaa58d74b5c08d303a06b1e3a766 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Tue, 22 Nov 2022 13:25:32 -0500 Subject: [PATCH 57/64] Add validation rule for defer/stream on subscriptions --- spec/Section 5 -- Validation.md | 49 +++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index b2293d467..d13ba9976 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -1556,6 +1556,55 @@ mutation { } ``` +### 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 ** From 53215c5c8da3ea2c37aeb54dd65539de48f7c865 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Wed, 23 Nov 2022 15:48:58 -0500 Subject: [PATCH 58/64] clarify label is not required --- spec/Section 6 -- Execution.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 2a15f5304..26fa4efb9 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -800,7 +800,8 @@ variableValues, parentRecord, subsequentPayloads): - Add an entry to {payload} named `data` with the value {null}. - Otherwise: - Add an entry to {payload} named `data` with the value {resultMap}. - - Add an entry to {payload} named `label` with the value {label}. + - If {label} is defined: + - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {path}. - Return {payload}. - Set {dataExecution} on {deferredFragmentRecord}. @@ -960,7 +961,8 @@ streamRecord, variableValues, subsequentPayloads): {data}. - If {errors} is not empty: - Add an entry to {payload} named `errors` with the value {errors}. - - Add an entry to {payload} named `label` with the value {label}. + - If {label} is defined: + - Add an entry to {payload} named `label` with the value {label}. - Add an entry to {payload} named `path` with the value {itemPath}. - If {parentRecord} is defined: - Wait for the result of {dataExecution} on {parentRecord}. From f73fdff3fbce4c2f8c54b4eabe9f1cddf7ab1d66 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 29 Nov 2022 15:50:09 +0200 Subject: [PATCH 59/64] fix parentRecord argument in ExecuteStreamField (#7) --- spec/Section 6 -- Execution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 26fa4efb9..ef4c199c6 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -930,7 +930,7 @@ yielded items satisfies `initialCount` specified on the `@stream` directive. #### Execute Stream Field ExecuteStreamField(label, iterator, index, fields, innerType, path, -streamRecord, variableValues, subsequentPayloads): +parentRecord, variableValues, subsequentPayloads): - Let {streamRecord} be an async payload record created from {label}, {path}, and {iterator}. From fe25ae6b34c966c9d9ff5228e459b5d2a087af38 Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Mon, 5 Dec 2022 14:03:10 -0500 Subject: [PATCH 60/64] fix typo Co-authored-by: Simon Gellis <82392336+simongellis-attentive@users.noreply.github.com> --- spec/Section 7 -- Response.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index e00f5a254..4eade1be1 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -328,7 +328,7 @@ Response 2, contains the defer payload and the first stream payload. 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 {true} and this +iterator did close synchronously, {hasNext} would be set to {false} and this would be the final response. ```json example From dbd266a32bb58640ecb62d2cf3268c37e73aebec Mon Sep 17 00:00:00 2001 From: Rob Richard Date: Sun, 15 Jan 2023 12:18:50 -0500 Subject: [PATCH 61/64] replace server with service --- spec/Section 3 -- Type System.md | 14 +++++++------- spec/Section 7 -- Response.md | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 97b17e99c..e5a955a9b 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -2209,8 +2209,8 @@ fragment someFragment on User { 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 Server must add it to the corresponding payload. `label` - must be unique label across all `@defer` and `@stream` directives in a + 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 @@ -2246,19 +2246,19 @@ query myQuery($shouldStream: Boolean) { 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 Server must add it to the corresponding payload. `label` - must be unique label across all `@defer` and `@stream` directives in a + 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 server should return as +- `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 servers honor the `@defer` and `@stream` +highly recommended that GraphQL services honor the `@defer` and `@stream` directives on each execution. However, the specification allows advanced use -cases where the server can determine that it is more performant to not defer +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 diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index 4eade1be1..db50408fa 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -39,10 +39,10 @@ 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 server 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 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 From 991f2234b8a0f6bda6ada029101feb2bc2855402 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 16 Jan 2023 18:28:31 +0200 Subject: [PATCH 62/64] CollectFields does not require path or asyncRecord (#11) --- spec/Section 6 -- Execution.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index ef4c199c6..5f690f311 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -415,7 +415,7 @@ subsequentPayloads, asyncRecord): - If {path} is not provided, initialize it to an empty list. - If {subsequentPayloads} is not provided, initialize it to the empty set. - Let {groupedFieldSet} and {deferredGroupedFieldsList} be the result of - {CollectFields(objectType, selectionSet, variableValues, path, asyncRecord)}. + {CollectFields(objectType, selectionSet, variableValues)}. - Initialize {resultMap} to an empty ordered map. - For each {groupedFieldSet} as {responseKey} and {fields}: - Let {fieldName} be the name of the first entry in {fields}. Note: This value @@ -647,8 +647,8 @@ The depth-first-search order of the field groups produced by {CollectFields()} is maintained through execution, ensuring that fields appear in the executed response in a stable and predictable order. -CollectFields(objectType, selectionSet, variableValues, path, asyncRecord, -visitedFragments, deferredGroupedFieldsList): +CollectFields(objectType, selectionSet, variableValues, visitedFragments, +deferredGroupedFieldsList): - If {visitedFragments} is not provided, initialize it to the empty set. - Initialize {groupedFields} to an empty ordered map of lists. @@ -695,14 +695,14 @@ visitedFragments, deferredGroupedFieldsList): - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, path, - asyncRecord, visitedFragments, deferredGroupedFieldsList)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments, deferredGroupedFieldsList)}. - Append a record containing {label} and {deferredGroupedFields} to {deferredGroupedFieldsList}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, path, - asyncRecord, visitedFragments, deferredGroupedFieldsList)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments, deferredGroupedFieldsList)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. @@ -725,21 +725,21 @@ visitedFragments, deferredGroupedFieldsList): - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, path, - asyncRecord, visitedFragments, deferredGroupedFieldsList)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments, deferredGroupedFieldsList)}. - Append a record containing {label} and {deferredGroupedFields} to {deferredGroupedFieldsList}. - Continue with the next {selection} in {selectionSet}. - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, path, - asyncRecord, visitedFragments, deferredGroupedFieldsList)}. + {CollectFields(objectType, fragmentSelectionSet, variableValues, + visitedFragments, deferredGroupedFieldsList)}. - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - Let {responseKey} be the response key shared by all fields in {fragmentGroup}. - Let {groupForResponseKey} be the list in {groupedFields} for {responseKey}; if no such list exists, create it as an empty list. - Append all items in {fragmentGroup} to {groupForResponseKey}. -- Return {groupedFields} and {deferredGroupedFieldsList}. +- Return {groupedFields}, {deferredGroupedFieldsList} and {visitedFragments}. Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` directives may be applied in either order since they apply commutatively. From c7804cfe07f2c74f144993e408e53e37e3146f90 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 21 May 2023 15:55:05 +0300 Subject: [PATCH 63/64] incremental delivery with deduplication of delivery and execution, allowing early delivery --- spec/Section 5 -- Validation.md | 8 +- spec/Section 6 -- Execution.md | 1181 +++++++++++++++++++++---------- spec/Section 7 -- Response.md | 61 +- 3 files changed, 846 insertions(+), 404 deletions(-) diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index d13ba9976..aef3ef598 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -474,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 {AnalyzeSelectionSet()} and {ProcessSubSelectionSets()}. For simple hand-written GraphQL, this rule is obviously a clear developer error, however nested fragments can make this difficult to detect manually. @@ -1530,7 +1530,7 @@ query ($foo: Boolean = true, $bar: Boolean = false) { ### Defer And Stream Directives Are Used On Valid Root Field -** Formal Specification ** +**Formal Specification** - For every {directive} in a document. - Let {directiveName} be the name of {directive}. @@ -1558,7 +1558,7 @@ mutation { ### Defer And Stream Directives Are Used On Valid Operations -** Formal Specification ** +**Formal Specification** - Let {subscriptionFragments} be the empty set. - For each {operation} in a document: @@ -1607,7 +1607,7 @@ subscription sub { ### Defer And Stream Directive Labels Are Unique -** Formal Specification ** +**Formal Specification** - Let {labelValues} be an empty set. - For every {directive} in the document: diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index 5f690f311..f60743648 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -132,28 +132,11 @@ An initial value may be provided when executing a query operation. ExecuteQuery(query, schema, variableValues, initialValue): -- Let {subsequentPayloads} be an empty list. - Let {queryType} be the root Query type in {schema}. - Assert: {queryType} is an Object type. - Let {selectionSet} be the top level Selection Set in {query}. -- Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - queryType, initialValue, variableValues, subsequentPayloads)} _normally_ - (allowing parallelization). -- Let {errors} be the list of all _field error_ raised while executing the - selection set. -- If {subsequentPayloads} is empty: - - Return an unordered map containing {data} and {errors}. -- If {subsequentPayloads} is not empty: - - Let {initialResponse} be an unordered map containing {data}, {errors}, and - an entry named {hasNext} with the value {true}. - - Let {iterator} be the result of running - {YieldSubsequentPayloads(initialResponse, subsequentPayloads)}. - - For each {payload} yielded by {iterator}: - - If a termination signal is received: - - Send a termination signal to {iterator}. - - Return. - - Otherwise: - - Yield {payload}. +- Return {ExecuteRootSelectionSet(variableValues, initialValue, queryType, + selectionSet)}. ### Mutation @@ -167,27 +150,11 @@ mutations ensures against race conditions during these side-effects. ExecuteMutation(mutation, schema, variableValues, initialValue): -- Let {subsequentPayloads} be an empty list. - Let {mutationType} be the root Mutation type in {schema}. - Assert: {mutationType} is an Object type. - Let {selectionSet} be the top level Selection Set in {mutation}. -- Let {data} be the result of running {ExecuteSelectionSet(selectionSet, - mutationType, initialValue, variableValues, subsequentPayloads)} _serially_. -- Let {errors} be the list of all _field error_ raised while executing the - selection set. -- If {subsequentPayloads} is empty: - - Return an unordered map containing {data} and {errors}. -- If {subsequentPayloads} is not empty: - - Let {initialResponse} be an unordered map containing {data}, {errors}, and - an entry named {hasNext} with the value {true}. - - Let {iterator} be the result of running - {YieldSubsequentPayloads(initialResponse, subsequentPayloads)}. - - For each {payload} yielded by {iterator}: - - If a termination signal is received: - - Send a termination signal to {iterator}. - - Return. - - Otherwise: - - Yield {payload}. +- Return {ExecuteRootSelectionSet(variableValues, initialValue, queryType, + selectionSet, true)}. ### Subscription @@ -285,15 +252,17 @@ CreateSourceEventStream(subscription, schema, variableValues, initialValue): - Let {subscriptionType} be the root Subscription type in {schema}. - Assert: {subscriptionType} is an Object type. - Let {selectionSet} be the top level Selection Set in {subscription}. -- Let {groupedFieldSet} be the result of {CollectFields(subscriptionType, - selectionSet, variableValues)}. +- Let {fieldsByTarget} be the result of calling + {AnalyzeSelectionSet(subscriptionType, selectionSet, variableValues)}. +- Let {groupedFieldSet} be the first entry in {fieldsByTarget}. - If {groupedFieldSet} does not have exactly one entry, raise a _request error_. -- Let {fields} be the value of the first entry in {groupedFieldSet}. -- Let {fieldName} be the name of the first entry in {fields}. Note: This value - is unaffected if an alias is used. -- Let {field} be the first entry in {fields}. +- Let {fieldGroup} be the value of the first entry in {groupedFieldSet}. +- Let {fieldDetails} be the first entry in {fieldGroup}. +- Let {node} be the corresponding entry on {fieldDetails}. +- Let {fieldName} be the name of {node}. Note: This value is unaffected if an + alias is used. - Let {argumentValues} be the result of {CoerceArgumentValues(subscriptionType, - field, variableValues)} + node, variableValues)} - Let {fieldStream} be the result of running {ResolveFieldEventStream(subscriptionType, initialValue, fieldName, argumentValues)}. @@ -320,10 +289,9 @@ MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues): - Return a new event stream {responseStream} which yields events as follows: - For each {event} on {sourceStream}: - - Let {executionResult} be the result of running + - Let {response} be the result of running {ExecuteSubscriptionEvent(subscription, schema, variableValues, event)}. - - For each {response} yielded by {executionResult}: - - Yield an event containing {response}. + - Yield an event containing {response}. - When {responseStream} completes: complete this event stream. ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue): @@ -331,15 +299,19 @@ ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue): - Let {subscriptionType} be the root Subscription type in {schema}. - Assert: {subscriptionType} is an Object type. - Let {selectionSet} be the top level Selection Set in {subscription}. -- Let {data} be the result of running {ExecuteSelectionSet(selectionSet, +- Let {fieldsByTarget} be the result of calling + {AnalyzeSelectionSet(subscriptionType, selectionSet, variableValues)}. +- Let {groupedFieldSet} be the first entry in {fieldsByTarget}. +- Let {data} be the result of running {ExecuteGroupedFieldSet(groupedFieldSet, subscriptionType, initialValue, variableValues)} _normally_ (allowing parallelization). - Let {errors} be the list of all _field error_ raised while executing the - selection set. + {groupedFieldSet}. - Return an unordered map containing {data} and {errors}. Note: The {ExecuteSubscriptionEvent()} algorithm is intentionally similar to -{ExecuteQuery()} since this is how each event result is produced. +{ExecuteQuery()} since this is how each event result is produced. Incremental +delivery, however, is not supported within ExecuteSubscriptionEvent. #### Unsubscribe @@ -352,137 +324,369 @@ Unsubscribe(responseStream): - Cancel {responseStream} -## Yield Subsequent Payloads - -If an operation contains subsequent payload records resulting from `@stream` or -`@defer` directives, the {YieldSubsequentPayloads} algorithm defines how the -payloads should be processed. - -YieldSubsequentPayloads(initialResponse, subsequentPayloads): - -- Let {initialRecords} be any items in {subsequentPayloads} with a completed - {dataExecution}. -- Initialize {initialIncremental} to an empty list. -- For each {record} in {initialRecords}: - - Remove {record} from {subsequentPayloads}. - - If {isCompletedIterator} on {record} is {true}: - - Continue to the next record in {records}. - - Let {payload} be the completed result returned by {dataExecution}. - - Append {payload} to {initialIncremental}. -- If {initialIncremental} is not empty: - - Add an entry to {initialResponse} named `incremental` containing the value - {incremental}. -- Yield {initialResponse}. -- While {subsequentPayloads} is not empty: - - If a termination signal is received: - - For each {record} in {subsequentPayloads}: - - If {record} contains {iterator}: - - Send a termination signal to {iterator}. - - Return. - - Wait for at least one record in {subsequentPayloads} to have a completed - {dataExecution}. - - Let {subsequentResponse} be an unordered map with an entry {incremental} - initialized to an empty list. - - Let {records} be the items in {subsequentPayloads} with a completed - {dataExecution}. - - For each {record} in {records}: - - Remove {record} from {subsequentPayloads}. - - If {isCompletedIterator} on {record} is {true}: - - Continue to the next record in {records}. - - Let {payload} be the completed result returned by {dataExecution}. - - Append {payload} to the {incremental} entry on {subsequentResponse}. - - If {subsequentPayloads} is empty: - - Add an entry to {subsequentResponse} named `hasNext` with the value - {false}. - - Otherwise, if {subsequentPayloads} is not empty: - - Add an entry to {subsequentResponse} named `hasNext` with the value - {true}. - - Yield {subsequentResponse} - -## Executing Selection Sets - -To execute a selection set, the object value being evaluated and the object type -need to be known, as well as whether it must be executed serially, or may be -executed in parallel. - -First, the selection set is turned into a grouped field set; then, each -represented field in the grouped field set produces an entry into a response -map. - -ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues, path, -subsequentPayloads, asyncRecord): +## Incremental Delivery + +If an operation contains `@defer` or `@stream` directives, execution may also +result in an Subsequent Result stream in addition to the initial response. The +procedure for yielding subsequent results is specified by the +{YieldSubsequentPayloads()} algorithm. + +## Executing the Root Selection Set + +To execute the root selection set, the object value being evaluated and the +object type need to be known, as well as whether it must be executed serially, +or may be executed in parallel. + +Executing the root selection set works similarly for queries (parallel), +mutations (serial), and subscriptions (where it is executed for each event in +the underlying Source Stream). + +First, the selection set is turned into a grouped field set; then, we execute +this grouped field set and return the resulting {data} and {errors}. + +ExecuteRootSelectionSet(variableValues, initialValue, objectType, selectionSet, +serial): + +- Let {fieldsByTarget}, {targetsByKey}, and {newDeferUsages} be the result of + calling {AnalyzeSelectionSet(objectType, selectionSet, variableValues)}. +- Let {groupedFieldSet} and {groupDetailsMap} be the result of calling + {BuildGroupedFieldSets(fieldsByTarget, targetsByKey)}. +- Let {newDeferMap} and {newIncrementalResults} be the result of + {GetNewDeferredFragments(newDeferUsages)}. +- Let {detailsList} be the result of + {GetDeferredGroupedFieldSetDetails(groupDetailsMap, newDeferMap)}. +- Let {data}, {nestedNewIncrementalResults}, {nestedForDeferredFragments}, and + {nestedFutures} be the result of running + {ExecuteGroupedFieldSet(groupedFieldSet, queryType, initialValue, + variableValues)} _serially_ if {serial} is {true}, _normally_ (allowing + parallelization) otherwise. +- In parallel, let {futures} and {forDeferredFragments} be the result of + {ExecuteDeferredGroupedFieldSets(queryType, initialValues, variableValues, + detailsList, newDeferMap)}. +- Append all members of {nestedNewIncrementalResults} to + {newIncrementalResults}. +- Append all members of {nestedForDeferredFragments} to {forDeferredFragments}. +- Append all members of {nestedFutures} to {futures}. +- Let {pendingMap} and {pending} be the result of + {GetPending(newIncrementalResults, forDeferredFragments)}. +- Let {errors} be the list of all _field error_ raised while executing the + {groupedFieldSet}. +- Initialize {initialResult} to an empty unordered map. +- If {errors} is not empty: + - Set the corresponding entry on {initialResult} to {errors}. +- Set {data} on {initialResult} to {data}. +- If {pending} is empty, return {initialResult}. +- Let {hasNext} be {true}. +- Set the corresponding entries on {initialResult} to {pending} and {hasNext}. +- Let {subsequentResults} be the result of {YieldSubsequentPayloads(pendingMap, + futures)}. +- Return {initialResult} and {subsequentResults}. + +GetNewDeferredFragments(newDeferUsages, deferMap, path): + +- Initialize {newDeferredFragments} to an empty list. +- If {newDeferUsages} is empty: + - Let {newDeferMap} be {deferMap}. +- Otherwise: + - Let {newDeferMap} be a new empty unordered map of Defer Usage records to + Deferred Fragment records. + - If {deferMap} is defined: + - For each {deferUsage} and {deferredFragment} in {deferMap}. + - Set the entry for {deferUsage} in {newDeferMap} to {deferredFragment}. + - For each {deferUsage} in {newDeferUsages}: + - Let {label} be the corresponding entry on {deferUsage}. + - Let {newDeferredFragment} be an unordered map containing {label} and + {path}. + - Set the entry for {deferUsage} in {newDeferMap} to {newDeferredFragment}. + - Append {newDeferredFragment} to {newIncrementalResults}. +- Return {newDeferMap} and {newDeferredFragments}. + +GetDeferredGroupedFieldSetDetails(groupDetailsMap, deferMap, path): + +- Initialize {detailsList} to an empty list. +- For each {deferUsageSet} and {details} in {groupDetailsMap}: + - Let {groupedFieldSet} and {shouldInitiateDefer} be the corresponding entries + on {details}. + - Let {deferredFragments} be an empty list. + - For each {deferUsage} in {deferUsageSet}: + - Let {deferredFragment} be the entry for {deferUsage} in {deferMap}. + - Append {deferredFragment} to {deferredFragments}. + - Let {deferredGroupedFieldSetDetails} be an unordered map containing {path}, + {deferredFragments}, {groupedFieldSet}, and {shouldInitiateDefer}. + - Append {deferredGroupedFieldSetDetails} to {detailsList}. +- Return {detailsList}. + +GetPending(newIncrementalResults, forDeferredFragments, oldPendingMap): + +- Initialize {newPendingMap} to an empty unordered map. +- If {oldPendingMap} is defined: + - For each {incrementalResult} and {pendingInfo} of {oldPendingMap}: + - Let {id} and {count} be the corresponding entries on {oldPendingInfo}. + - Let {pendingInfo} be a new unordered map consisting of {id} and {count}. + - Set the entry for {incrementalResult} in {newPendingMap} to {pendingInfo}. +- Initialize {pending} to an empty list. +- For each {newIncrementalResult} in {newIncrementalResults}: + - Let {id} be a unique identifier for this execution. + - If {newIncrementalResult} is a deferred fragment: + - Let {count} be {0}. + - Let {pendingInfo} be an unordered map consisting of {id} and {count}. + - Otherwise: + - Let {pendingInfo} be an unordered map consisting of {id}. + - Let {path} and {label} be the corresponding entries on + {newIncrementalResult}. + - Let {pendingResult} be an unordered map containing {path}, {label}, and + {id}. + - Append {pendingResult} to {pending}. + - Set the entry for {newIncrementalResult} in {newPendingMap} to {info}. +- For each {deferredFragment} in {forDeferredFragments}: + - Let {pendingInfo} be the entry in {newPendingMap} for {deferredFragment}. + - Let {count} be the corresponding entry on {pendingInfo}. + - Increment {count}. +- For each {newIncrementalResult} in {newIncrementalResults}: + - If {newIncrementalResult} is a deferred fragment: + - Let {pendingInfo} be the entry in {newPendingMap} for + {newIncrementalResult}. + - Let {id} and {count} be the corresponding entries on {pendingInfo}. + - If {count} is greater than {0}: + - Let {path} and {label} be the corresponding entries on + {newIncrementalResult}. + - Let {pendingResult} be an unordered map containing {path}, {label}, and + {id}. + - Append {pendingResult} to {pending}. + - Otherwise, remove the entry for {newIncrementalResult} on {newPendingMap}. +- Return {newPendingMap} and {pending}. + +YieldSubsequentPayloads(oldPendingMap, maybeUninitiatedFutures, futures, +unsent): + +- If {futures} is not defined, initialize it to the empty set. +- If {unsent} is not defined, initialize it to the empty set. +- For each {maybeUninitiatedFuture} in {maybeUninitiatedFutures}: + - If {maybeUninitiatedFuture} has not been initiated, initiate it. + - Add {maybeUninitiatedFuture} to {futures}. +- Wait for any future execution contained within {futures} to complete. +- Let {currentPendingMap} be {oldPendingMap}. +- Initialize {incrementalResults}, {completedResults}, + {currentNewIncrementalResults}, {currentForIncrementalResults}, and + {currentFutures} to empty lists. +- For each {future} in {futures}: + - If {future} has completed: + - Remove {future} from {futures}. + - Let {result} be the result of {future}. + - If {result} represents the result of completion of stream items: + - Let {currentPendingMap}, {incrementalResults}, {completedResults}, + {currentNewIncrementalResults}, {currentForIncrementalResults}, and + {currentFutures} be the result of + {UpdateIncrementalStateForStreamItems(result, currentPendingMap, + incrementalResults, completedResults, currentNewIncrementalResults, + currentForIncrementalResults, and currentFutures)}. + - Otherwise, {result} represents the result of execution of a deferred + grouped field set: + - Let {currentPendingMap}, {incrementalResults}, {completedResults}, + {currentNewIncrementalResults}, {currentForIncrementalResults}, and + {currentFutures} be the result of + {UpdateIncrementalStateForDeferredGroupedFieldSet(result, + currentPendingMap, incrementalResults, completedResults, + currentNewIncrementalResults, currentForIncrementalResults, and + currentFutures)}. +- If {completedResults} is empty: + - Yield the results of {YieldSubsequentPayloads(currentPendingMap, + currentFutures, futures, unsent)}. +- Otherwise: + - Let {nextPendingMap} and {pending} be the result of {GetPending( + newIncrementalResults, forDeferredFragments, currentPendingMap)}. + - If {nextPendingMap} is empty, let {hasNext} be {false}; otherwise, let it be + {true}. + - Let {current} be an unordered map consisting of {completedResults} and + {hasNext}. + - If {pending} is not empty: + - Set the entry for {pending} on {current} to {incrementalResults}. + - If {incrementalResults} is not empty: + - Set the entry for {incremental} on {current} to {incrementalResults}. + - Yield {current}. + - Yield the results of {YieldSubsequentPayloads(currentPendingMap, + currentFutures, futures, unsent)}. + +UpdateIncrementalStateForStreamItems(streamItems, pendingMap, +incrementalResults, completedResults, newIncrementalResults, +forIncrementalResults, futures): + +- Let {nextPendingMap} be an empty unordered map. +- For each {incrementalResult} and {pendingInfo} in {pendingMap}: + - Let {id}, {count}, and {completed} be the corresponding entries on + {pendingInfo}. + - Let {pendingInfo} be a new unordered map consisting of {id}, {count}, and + {completed}, if defined. + - Set the entry for {incrementalResult} in {nextPendingMap} to {pendingInfo}. +- Let {nextIncrementalResults}, {nextCompletedResults}, + {nextNewIncrementalResults}, {nextForIncrementalResults}, and {nextFutures} be + new lists containing all of the members of {incrementalResults}, + {completedResults}, {newIncrementalResults}, {forIncrementalResults}, and + {futures}, respectively. +- Let {stream}, {completedItems}, {errors}, {newIncrementalResults}, + {forIncrementalResults}, and {futures} be the corresponding entries on + {streamItems}. +- If {completedItems} is not defined: + - Let {pendingInfo} be the corresponding entry on {nextPendingMap} for + {stream}. + - Remove the entry for {stream} on {nextPendingMap}. + - Let {id} be the corresponding entry on {pendingInfo}. + - Let {completedResult} be an unordered map consisting of {id}. + - Append {completedResult} to {nextCompletedResults}. +- If {completedItems} is {null}: + - Let {pendingInfo} be the corresponding entry on {nextPendingMap} for + {stream}. + - Remove the entry for {stream} on {nextPendingMap}. + - Let {id} be the corresponding entry on {pendingInfo}. + - Let {completedResult} be an unordered map consisting of {id} and {errors}. + - Append {completedResult} to {nextCompletedResults}. +- Otherwise: + - Append all members of {newIncrementalResults} to + {nextNewIncrementalResults}. + - Append all members of {forIncrementalResults} to + {nextForIncrementalResults}. + - Append all members of {futures} to {nextFutures}. + - Let {incrementalResult} be an unordered map consisting of {data}. + - If {errors} is not empty, set the corresponding entry on {incrementalResult} + to {errors}. + - Append {incrementalResult} to {nextIncrementalResults}. +- Return {nextPendingMap}, {nextIncrementalResults}, {nextCompletedResults}, + {nextNewIncrementalResults}, {nextForIncrementalResults}, and {nextFutures}. + +UpdateIncrementalStateForDeferredGroupedFieldSet(deferredGroupedFieldSet, +pendingMap, incrementalResults, completedResults, newIncrementalResults, +forIncrementalResults, futures): + +- Let {nextPendingMap} be an empty unordered map. +- For each {incrementalResult} and {pendingInfo} in {pendingMap}: + - Let {id}, {count}, and {completed} be the corresponding entries on + {pendingInfo}. + - Let {pendingInfo} be a new unordered map consisting of {id}, {count}, and + {completed}, if defined. + - Set the entry for {incrementalResult} in {nextPendingMap} to {pendingInfo}. +- Let {nextIncrementalResults}, {nextCompletedResults}, + {nextNewIncrementalResults}, {nextForIncrementalResults}, and {nextFutures} be + new lists containing all of the members of {incrementalResults}, + {completedResults}, {newIncrementalResults}, {forIncrementalResults}, and + {futures}, respectively. +- Let {deferredFragments}, {data}, and {errors} be the corresponding entries on + {deferredGroupedFieldSet}. +- If {data} is {null}: + - For each {deferredFragment} of {deferredFragments}: + - Let {pendingInfo} be the corresponding entry on {nextPendingMap} for + {deferredFragment}. + - Remove the entry for {deferredFragment} on {nextPendingMap}. + - Let {id} be the corresponding entry on {pendingInfo}. + - Let {completedResult} be an unordered map consisting of {id} and {errors}. + - Append {completedResult} to {completedResults}. +- Otherwise: + - For each {deferredFragment} of {deferredFragments}: + - Let {pendingInfo} be the corresponding entry on {nextPendingMap} for + {deferredFragment}. + - Let {id}, {count}, and {completed} be the corresponding entries on + {pendingInfo}. + - Decrement {count}. + - If {completed} is not defined: + - Initialize {completed} to an empty list. + - Set the corresponding entry on {pendingInfo} to {completed}. + - Append {result} to {completed}. + - Add {result} to {unsent}. + - If {count} is equal to {0}: + - Let {completedResult} be an unordered map consisting of {id}. + - Append {completedResult} to {completedResults}. + - For each {deferredGroupedFieldSet} in {completed}: + - If {unsent} contains {deferredGroupedFieldSet}: + - Remove {deferredGroupedFieldSet} from {unsent}. + - Let {data}, {errors}, {newIncrementalResults}, + {forIncrementalResults}, and {futures} be the corresponding entries + on {deferredGroupedFieldSet}. + - Append all members of {newIncrementalResults} to + {nextNewIncrementalResults}. + - Append all members of {forIncrementalResults} to + {nextForIncrementalResults}. + - Append all members of {futures} to {nextFutures}. + - Let {idForDeferredGroupedFieldSet} and {subPath} be the results of + {GetIdAndSubPath(deferredGroupedFieldSet)}. + - Let {incrementalResult} be an unordered map consisting of {data}. + - Set the entry for {id} on {incrementalResult} to + {idForDeferredGroupedFieldSet}. + - If {subPath} is defined, set the corresponding entry on + {incrementalResult} to {subPath}. + - If {errors} is not empty, set the corresponding entry on + {incrementalResult} to {errors}. + - Append {incrementalResult} to {nextIncrementalResults}. +- Return {nextPendingMap}, {nextIncrementalResults}, {nextCompletedResults}, + {nextNewIncrementalResults}, {nextForIncrementalResults}, and {nextFutures}. + +GetIdAndSubPath(deferredGroupedFieldSet): + +- Let {deferredFragments} be the corresponding entry on + {deferredGroupedFieldSet}. +- Let {firstDeferredFragment} be the first member of {deferredFragments}. +- Let {currentId} and {currentPath} be the entries for {id} and {path} on + {firstDeferredFragment}, respectively. +- Let {currentPathLength} be the length of {firstPath}. +- For each remaining {deferredFragment} within {deferredFragments}. + - Let {fragmentPath} be the corresponding entry on {deferredFragment}. + - Let {fragmentPathLength} be the length of {path}. + - If {fragmentPathLength} is larger than {currentPathLength}: + - Set {currentPathLength} to {pathLength}. + - Set {currentId} to the entry for {id} on {deferredFragment}. +- Let {deferredGroupedFieldSetPath} be the entry for {path} on + {deferredGroupedFieldSet}. +- Let {subPath} be the subset of {path}, omitting the first {currentPathLength} + entries. +- Return {currentId} and {subPath}. + +## Executing a Grouped Field Set + +To execute a grouped field set, the object value being evaluated and the object +type need to be known, as well as whether it must be executed serially, or may +be executed in parallel. + +ExecuteGroupedFieldSet(groupedFieldSet, objectType, objectValue, variableValues, +path, deferMap): - If {path} is not provided, initialize it to an empty list. -- If {subsequentPayloads} is not provided, initialize it to the empty set. -- Let {groupedFieldSet} and {deferredGroupedFieldsList} be the result of - {CollectFields(objectType, selectionSet, variableValues)}. - Initialize {resultMap} to an empty ordered map. -- For each {groupedFieldSet} as {responseKey} and {fields}: - - Let {fieldName} be the name of the first entry in {fields}. Note: This value - is unaffected if an alias is used. +- Initialize {newIncrementalResults}, {forDeferredFragments}, and {futures} to + empty lists. +- For each {groupedFieldSet} as {responseKey} and {fieldGroup}: + - Let {fieldDetails} be the first entry in {fieldGroup}. + - Let {node} be the corresponding entry on {fieldDetails}. + - Let {fieldName} be the name of {node}. Note: This value is unaffected if an + alias is used. - Let {fieldType} be the return type defined for the field {fieldName} of {objectType}. - If {fieldType} is defined: - - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, - fields, variableValues, path, subsequentPayloads, asyncRecord)}. + - Let {responseValue}, {fieldIncrementalResults}, {forDeferredFragments}, + and {fieldFutures} be the result of {ExecuteField(objectType, objectValue, + fieldType, fieldGroup, variableValues, path)}. - Set {responseValue} as the value for {responseKey} in {resultMap}. -- For each {deferredGroupFieldSet} and {label} in {deferredGroupedFieldsList} - - Call {ExecuteDeferredFragment(label, objectType, objectValue, - deferredGroupFieldSet, path, variableValues, asyncRecord, - subsequentPayloads)} -- Return {resultMap}. + - Append all members of {fieldIncrementalResults} to + {newIncrementalResults}. + - Append all members of {fieldForDeferredFragments} to + {forDeferredFragments}. + - Append all members of {fieldFutures} to {futures}. +- Return {resultMap}, {newIncrementalResults}, {forDeferredFragments}, and + {futures}. Note: {resultMap} is ordered by which fields appear first in the operation. This -is explained in greater detail in the Field Collection section below. +is explained in greater detail in the Selection Set Analysis section below. **Errors and Non-Null Fields** -If during {ExecuteSelectionSet()} a field with a non-null {fieldType} raises a -_field error_ then that error must propagate to this entire selection set, +If during {ExecuteGroupedFieldSet()} a field with a non-null {fieldType} raises +a _field error_ then that error must propagate to this entire grouped field set, either resolving to {null} if allowed or further propagated to a parent field. If this occurs, any sibling fields which have not yet executed or have not yet yielded a value may be cancelled to avoid unnecessary work. -Additionally, async payload records in {subsequentPayloads} must be filtered if -their path points to a location that has resolved to {null} due to propagation -of a field error. This is described in -[Filter Subsequent Payloads](#sec-Filter-Subsequent-Payloads). These async -payload records must be removed from {subsequentPayloads} and their result must -not be sent to clients. If these async records have not yet executed or have not -yet yielded a value they may also be cancelled to avoid unnecessary work. - -Note: See [Handling Field Errors](#sec-Handling-Field-Errors) for more about -this behavior. - -### Filter Subsequent Payloads - -When a field error is raised, there may be async payload records in -{subsequentPayloads} with a path that points to a location that has been removed -or set to null due to null propagation. These async payload records must be -removed from subsequent payloads and their results must not be sent to clients. - -In {FilterSubsequentPayloads}, {nullPath} is the path which has resolved to null -after propagation as a result of a field error. {currentAsyncRecord} is the -async payload record where the field error was raised. {currentAsyncRecord} will -not be set for field errors that were raised during the initial execution -outside of {ExecuteDeferredFragment} or {ExecuteStreamField}. - -FilterSubsequentPayloads(subsequentPayloads, nullPath, currentAsyncRecord): - -- For each {asyncRecord} in {subsequentPayloads}: - - If {asyncRecord} is the same record as {currentAsyncRecord}: - - Continue to the next record in {subsequentPayloads}. - - Initialize {index} to zero. - - While {index} is less then the length of {nullPath}: - - Initialize {nullPathItem} to the element at {index} in {nullPath}. - - Initialize {asyncRecordPathItem} to the element at {index} in the {path} - of {asyncRecord}. - - If {nullPathItem} is not equivalent to {asyncRecordPathItem}: - - Continue to the next record in {subsequentPayloads}. - - Increment {index} by one. - - Remove {asyncRecord} from {subsequentPayloads}. Optionally, cancel any - incomplete work in the execution of {asyncRecord}. +Additionally, Subsequent Result records must not be yielded if their path points +to a location that has resolved to {null} due to propagation of a field error. +If these subsequent results have not yet executed or have not yet yielded a +value they may also be cancelled to avoid unnecessary work. For example, assume the field `alwaysThrows` is a `Non-Null` type that always raises a field error: @@ -498,17 +702,19 @@ raises a field error: } ``` -In this case, only one response should be sent. The async payload record -associated with the `@defer` directive should be removed and its execution may -be cancelled. +In this case, only one response should be sent. The result of the fragment +tagged with the `@defer` directive should be ignored and its execution, if +initiated, may be cancelled. ```json example { - "data": { "myObject": null }, - "hasNext": false + "data": { "myObject": null } } ``` +Note: See [Handling Field Errors](#sec-Handling-Field-Errors) for more about +this behavior. + ### Normal and Serial Execution Normally the executor can execute the entries in a grouped field set in whatever @@ -608,23 +814,18 @@ A correct executor must generate the following result for that selection set: When subsections contain a `@stream` or `@defer` directive, these subsections are no longer required to execute serially. Execution of the deferred or streamed sections of the subsection may be executed in parallel, as defined in -{ExecuteStreamField} and {ExecuteDeferredFragment}. +{ExecuteDeferredGroupedFieldSets} and {ExecuteStreamField}. -### Field Collection +### Selection Set Analysis Before execution, the selection set is converted to a grouped field set by -calling {CollectFields()}. Each entry in the grouped field set is a list of -fields that share a response key (the alias if defined, otherwise the field -name). This ensures all fields with the same response key (including those in -referenced fragments) are executed at the same time. A deferred selection set's -fields will not be included in the grouped field set. Rather, a record -representing the deferred fragment and additional context will be stored in a -list. The executor revisits and resumes execution for the list of deferred -fragment records after the initial execution is initiated. This deferred -execution would ‘re-execute’ fields with the same response key that were present -in the grouped field set. - -As an example, collecting the fields of this selection set would collect two +calling {AnalyzeSelectionSet()} and {BuildGroupedFieldSets()}. Each entry in the +grouped field set is a Field Group record describing all fields that share a +response key (the alias if defined, otherwise the field name). This ensures all +fields with the same response key (including those in referenced fragments) are +executed at the same time. + +As an example, analysis of the fields of this selection set would return two instances of the field `a` and one of field `b`: ```graphql example @@ -643,17 +844,63 @@ fragment ExampleFragment on Query { } ``` -The depth-first-search order of the field groups produced by {CollectFields()} -is maintained through execution, ensuring that fields appear in the executed -response in a stable and predictable order. - -CollectFields(objectType, selectionSet, variableValues, visitedFragments, -deferredGroupedFieldsList): - -- If {visitedFragments} is not provided, initialize it to the empty set. -- Initialize {groupedFields} to an empty ordered map of lists. -- If {deferredGroupedFieldsList} is not provided, initialize it to an empty - list. +The depth-first-search order of the field groups produced by selection set +processing is maintained through execution, ensuring that fields appear in the +executed response in a stable and predictable order. + +{AnalyzeSelectionSet()} also returns a list of references to any new deferred +fragments encountered the selection set. {BuildGroupedFieldSets()} also +potentially returns additional deferred grouped field sets related to new or +previously encountered deferred fragments. Additional grouped field sets are +constructed carefully so as to ensure that each field is executed exactly once +and so that fields are grouped according to the set of deferred fragments that +include them. + +Information derived from the presence of a `@defer` directive on a fragment is +returned as a Defer Usage record, unique to the label, a structure containing: + +- {label}: value of the corresponding argument to the `@defer` directive. +- {ancestors}: a list, where the first entry is the parent Defer Usage record + corresponding to the deferred fragment enclosing this deferred fragment and + the remaining entries are the values included within the {ancestors} entry of + that parent Defer Usage record, or, if this Defer Usage record is deferred + directly by the initial result, a list containing the single value + {undefined}. + +A Field Group record is a structure containing: + +- {fields}: a list of Field Details records for each encountered field. +- {targets}: the set of Defer Usage records corresponding to the deferred + fragments enclosing this field, as well as possibly the value {undefined} if + the field is included within the initial response. + +A Field Details record is a structure containing: + +- {node}: the field node itself. +- {target}: the Defer Usage record corresponding to the deferred fragment + enclosing this field or the value {undefined} if the field was not deferred. + +Information about additional deferred grouped field sets are returned as a list +of Grouped Field Set Details structures containing: + +- {groupedFieldSet}: the grouped field set itself. +- {shouldInitiateDefer}: a boolean value indicating whether the executor should + defer execution of {groupedFieldSet}. + +Deferred grouped field sets do not always require initiating deferral. For +example, when a parent field is deferred by multiple fragments, deferral is +initiated on the parent field. New grouped field sets for child fields will be +created if the child fields are not all present in all of the deferred +fragments, but these new grouped field sets, while representing deferred fields, +do not require additional deferral. + +AnalyzeSelectionSet(objectType, selectionSet, variableValues, visitedFragments, +parentTarget, newTarget): + +- If {visitedFragments} is not defined, initialize it to the empty set. +- Initialize {targetsByKey} to an empty unordered map of sets. +- Initialize {fieldsByTarget} to an empty unordered map of ordered maps. +- Initialize {newDeferUsages} to an empty list of Defer Usage records. - For each {selection} in {selectionSet}: - If {selection} provides the directive `@skip`, let {skipDirective} be that directive. @@ -668,7 +915,14 @@ deferredGroupedFieldsList): - If {selection} is a {Field}: - Let {responseKey} be the response key of {selection} (the alias if defined, otherwise the field name). - - Let {groupForResponseKey} be the list in {groupedFields} for + - Let {target} be {newTarget} if {newTarget} is defined; otherwise, let + {target} be {parentTarget}. + - Let {targetsForKey} be the list in {targetsByKey} for {responseKey}; if no + such list exists, create it as an empty set. + - Add {target} to {targetsForKey}. + - Let {fieldsForTarget} be the map in {fieldsByTarget} for {responseKey}; if + no such map exists, create it as an unordered map. + - Let {groupForResponseKey} be the list in {fieldsForTarget} for {responseKey}; if no such list exists, create it as an empty list. - Append {selection} to the {groupForResponseKey}. - If {selection} is a {FragmentSpread}: @@ -694,21 +948,32 @@ deferredGroupedFieldsList): - If {deferDirective} is defined: - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, deferredGroupedFieldsList)}. - - Append a record containing {label} and {deferredGroupedFields} to - {deferredGroupedFieldsList}. - - Continue with the next {selection} in {selectionSet}. - - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, deferredGroupedFieldsList)}. - - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - - Let {responseKey} be the response key shared by all fields in - {fragmentGroup}. - - Let {groupForResponseKey} be the list in {groupedFields} for - {responseKey}; if no such list exists, create it as an empty list. - - Append all items in {fragmentGroup} to {groupForResponseKey}. + - Let {ancestors} be an empty list. + - Append {parentTarget} to {ancestors}. + - If {parentTarget} is defined: + - Let {parentAncestors} be the {ancestor} entry on {parentTarget}. + - Append all items in {parentAncestors} to {ancestors}. + - Let {target} be a new Defer Usage record created from {label} and + {ancestors}. + - Append {target} to {newDeferUsages}. + - Otherwise: + - Let {target} be {newTarget}. + - Let {fragmentTargetsByKey}, {fragmentFieldsByTarget}, + {fragmentNewDeferUsages} be the result of calling + {AnalyzeSelectionSet(objectType, fragmentSelectionSet, variableValues, + visitedFragments, parentTarget, target)}. + - For each {target} and {fragmentMap} in {fragmentFieldsByTarget}: + - Let {mapForTarget} be the ordered map in {fieldsByTarget} for {target}; + if no such map exists, create it as an empty ordered map. + - For each {responseKey} and {fragmentList} in {fragmentMap}: + - Let {listForResponseKey} be the list in {fieldsByTarget} for + {responseKey}; if no such list exists, create it as an empty list. + - Append all items in {fragmentList} to {listForResponseKey}. + - For each {responseKey} and {targetSet} in {fragmentTargetsByKey}: + - Let {setForResponseKey} be the set in {targetsByKey} for {responseKey}; + if no such set exists, create it as the empty set. + - Add all items in {targetSet} to {setForResponseKey}. + - Append all items in {fragmentNewDeferUsages} to {newDeferUsages}. - If {selection} is an {InlineFragment}: - Let {fragmentType} be the type condition on {selection}. - If {fragmentType} is not {null} and {DoesFragmentTypeApply(objectType, @@ -724,24 +989,35 @@ deferredGroupedFieldsList): - If {deferDirective} is defined: - Let {label} be the value or the variable to {deferDirective}'s {label} argument. - - Let {deferredGroupedFields} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, deferredGroupedFieldsList)}. - - Append a record containing {label} and {deferredGroupedFields} to - {deferredGroupedFieldsList}. - - Continue with the next {selection} in {selectionSet}. - - Let {fragmentGroupedFieldSet} be the result of calling - {CollectFields(objectType, fragmentSelectionSet, variableValues, - visitedFragments, deferredGroupedFieldsList)}. - - For each {fragmentGroup} in {fragmentGroupedFieldSet}: - - Let {responseKey} be the response key shared by all fields in - {fragmentGroup}. - - Let {groupForResponseKey} be the list in {groupedFields} for - {responseKey}; if no such list exists, create it as an empty list. - - Append all items in {fragmentGroup} to {groupForResponseKey}. -- Return {groupedFields}, {deferredGroupedFieldsList} and {visitedFragments}. - -Note: The steps in {CollectFields()} evaluating the `@skip` and `@include` + - Let {ancestors} be an empty list. + - Append {parentTarget} to {ancestors}. + - If {parentTarget} is defined: + - Let {parentAncestors} be {ancestor} on {parentTarget}. + - Append all items in {parentAncestors} to {ancestors}. + - Let {target} be a new Defer Usage record created from {label} and + {ancestors}. + - Append {target} to {newDeferUsages}. + - Otherwise: + - Let {target} be {newTarget}. + - Let {fragmentTargetsByKey}, {fragmentFieldsByTarget}, + {fragmentNewDeferUsages} be the result of calling + {AnalyzeSelectionSet(objectType, fragmentSelectionSet, variableValues, + visitedFragments, parentTarget, target)}. + - For each {target} and {fragmentMap} in {fragmentFieldsByTarget}: + - Let {mapForTarget} be the ordered map in {fieldsByTarget} for {target}; + if no such map exists, create it as an empty ordered map. + - For each {responseKey} and {fragmentList} in {fragmentMap}: + - Let {listForResponseKey} be the list in {fieldsByTarget} for + {responseKey}; if no such list exists, create it as an empty list. + - Append all items in {fragmentList} to {listForResponseKey}. + - For each {responseKey} and {targetSet} in {fragmentTargetsByKey}: + - Let {setForResponseKey} be the set in {targetsByKey} for {responseKey}; + if no such set exists, create it as the empty set. + - Add all items in {targetSet} to {setForResponseKey}. + - Append all items in {fragmentNewDeferUsages} to {newDeferUsages}. +- Return {fieldsByTarget}, {targetsByKey}, and {newDeferUsages}. + +Note: The steps in {AnalyzeSelectionSet()} evaluating the `@skip` and `@include` directives may be applied in either order since they apply commutatively. DoesFragmentTypeApply(objectType, fragmentType): @@ -756,56 +1032,138 @@ DoesFragmentTypeApply(objectType, fragmentType): - if {objectType} is a possible type of {fragmentType}, return {true} otherwise return {false}. -#### Async Payload Record - -An Async Payload Record is either a Deferred Fragment Record or a Stream Record. -All Async Payload Records are structures containing: - -- {label}: value derived from the corresponding `@defer` or `@stream` directive. -- {path}: a list of field names and indices from root to the location of the - corresponding `@defer` or `@stream` directive. -- {iterator}: The underlying iterator if created from a `@stream` directive. -- {isCompletedIterator}: a boolean indicating the payload record was generated - from an iterator that has completed. -- {errors}: a list of field errors encountered during execution. -- {dataExecution}: A result that can notify when the corresponding execution has - completed. - -#### Execute Deferred Fragment - -ExecuteDeferredFragment(label, objectType, objectValue, groupedFieldSet, path, -variableValues, parentRecord, subsequentPayloads): - -- Let {deferRecord} be an async payload record created from {label} and {path}. -- Initialize {errors} on {deferRecord} to an empty list. -- Let {dataExecution} be the asynchronous future value of: - - Let {payload} be an unordered map. - - Initialize {resultMap} to an empty ordered map. - - For each {groupedFieldSet} as {responseKey} and {fields}: - - Let {fieldName} be the name of the first entry in {fields}. Note: This - value is unaffected if an alias is used. - - Let {fieldType} be the return type defined for the field {fieldName} of - {objectType}. - - If {fieldType} is defined: - - Let {responseValue} be {ExecuteField(objectType, objectValue, fieldType, - fields, variableValues, path, subsequentPayloads, asyncRecord)}. - - Set {responseValue} as the value for {responseKey} in {resultMap}. - - Append any encountered field errors to {errors}. - - If {parentRecord} is defined: - - Wait for the result of {dataExecution} on {parentRecord}. - - If {errors} is not empty: - - Add an entry to {payload} named `errors` with the value {errors}. - - If a field error was raised, causing a {null} to be propagated to - {responseValue}: - - Add an entry to {payload} named `data` with the value {null}. +BuildGroupedFieldSets(fieldsByTarget, targetsByKey, parentTargets) + +- If {parentTargets} is not provided, initialize it to a set containing the + value {undefined}. +- Initialize {groupedFieldSet} to an empty ordered map. +- Initialize {groupDetailsMap} to an empty unordered map. +- For each {responseKey} and {targets} in {targetsByKey}: + - If {IsSameSet(targets, parentTargets)} is {true}: + - Let {fieldGroup} be the Field Group record in {groupedFieldSet} for + {responseKey}; if no such record exists, create a new such record from the + empty list {fields} and the set of {parentTargets}. + - For each {target} in {targets}: + - Let {fields} be the entry in {fieldsByTarget} for {target}. + - Let {nodes} be the list in {fields} for {responseKey}. + - For each {node} of {nodes}: + - Let {fieldDetails} be a new Field Details record created from {node} + and {target}. + - Append {fieldDetails} to the {fields} entry on {fieldGroup}. +- Initialize {groupDetailsMap} to an empty unordered map. +- For each {maskingTargets} and {targetSetDetails} in {targetSetDetailsMap}: + - Initialize {newGroupedFieldSet} to an empty ordered map. + - Let {keys} be the corresponding entry on {targetSetDetails}. + - Let {orderedResponseKeys} be the result of + {GetOrderedResponseKeys(maskingTargets, remainingFieldsByTarget)}. + - For each {responseKey} in {orderedResponseKeys}: + - If {keys} does not contain {responseKey}, continue to the next member of + {orderedResponseKeys}. + - Let {fieldGroup} be the Field Group record in {newGroupedFieldSet} for + {responseKey}; if no such record exists, create a new such record from the + empty list {fields} and the set of {parentTargets}. + - Let {targets} be the entry in {targetsByKeys} for {responseKey}. + - For each {target} in {targets}: + - Let {remainingFieldsForTarget} be the entry in {remainingFieldsByTarget} + for {target}. + - Let {nodes} be the list in {remainingFieldsByTarget} for {responseKey}. + - Remove the entry for {responseKey} from {remainingFieldsByTarget}. + - For each {node} of {nodes}: + - Let {fieldDetails} be a new Field Details record created from {node} + and {target}. + - Append {fieldDetails} to the {fields} entry on {fieldGroup}. + - Let {shouldInitiateDefer} be the corresponding entry on {targetSetDetails}. + - Initialize {details} to an empty unordered map. + - Set the entry for {groupedFieldSet} in {details} to {newGroupedFieldSet}. + - Set the corresponding entry in {details} to {shouldInitiateDefer}. + - Set the entry for {maskingTargets} in {groupDetailsMap} to {details}. +- Return {groupedFieldSet} and {groupDetailsMap}. + +Note: entries are always added to Grouped Field Set records in the order in +which they appear for the first target. Field order for deferred grouped field +sets never alters the field order for the parent. + +GetTargetSetDetails(targetsByKey, parentTargets): + +- Initialize {keysWithParentTargets} to the empty set. +- Initialize {targetSetDetailsMap} to an empty unordered map. +- For each {responseKey} and {targets} in {targetsByKey}: + - Initialize {maskingTargets} to an empty set. + - For each {target} in {targets}: + - If {target} is not defined: + - Add {target} to {maskingTargets}. + - Continue to the next entry in {targets}. + - Let {ancestors} be the corresponding entry on {target}. + - For each {ancestor} of {ancestors}: + - If {targets} contains {ancestor}, continue to the next member of + {targets}. + - Add {target} to {maskingTargets}. + - If {IsSameSet(maskingTargets, parentTargets)} is {true}: + - Append {responseKey} to {keysWithParentTargets}. + - Continue to the next entry in {targetsByKey}. + - For each {key} in {targetSetDetailsMap}: + - If {IsSameSet(maskingTargets, key)} is {true}, let {targetSetDetails} be + the map in {targetSetDetailsMap} for {maskingTargets}. + - If {targetSetDetails} is defined: + - Let {keys} be the corresponding entry on {targetSetDetails}. + - Add {responseKey} to {keys}. - Otherwise: - - Add an entry to {payload} named `data` with the value {resultMap}. - - If {label} is defined: - - Add an entry to {payload} named `label` with the value {label}. - - Add an entry to {payload} named `path` with the value {path}. - - Return {payload}. -- Set {dataExecution} on {deferredFragmentRecord}. -- Append {deferRecord} to {subsequentPayloads}. + - Initialize {keys} to the empty set. + - Add {responseKey} to {keys}. + - Let {shouldInitiateDefer} be {false}. + - For each {target} in {maskingTargets}: + - If {parentTargets} does not contain {target}: + - Set {shouldInitiateDefer} equal to {true}. + - Create {newTargetSetDetails} as an map containing {keys} and + {shouldInitiateDefer}. + - Set the entry in {targetSetDetailsMap} for {maskingTargets} to + {newTargetSetDetails}. +- Return {keysWithParentTargets} and {targetSetDetailsMap}. + +IsSameSet(setA, setB): + +- If the size of setA is not equal to the size of setB: + - Return {false}. +- For each {item} in {setA}: + - If {setB} does not contain {item}: + - Return {false}. +- Return {true}. + +## Executing Deferred Grouped Field Sets + +ExecuteDeferredGroupedFieldSets(objectType, objectValue, variableValues, path, +deferredGroupedFieldSetDetails, deferMap) + +- Initialize {forDeferredFragments} to an empty list. +- Initialize {futures} to an empty list. +- For each {deferredGroupedFieldSetDetails} in {detailsList}, allowing for + parallelization: + - Let {path}, {deferredFragments}, {groupedFieldSet}, and + {shouldInitiateDefer} be the corresponding entries on + {deferredGroupedFieldSetDetails}. + - Let {future} represent the future execution of + {ExecuteDeferredGroupedFieldSet(groupedFieldSet, objectType, objectValue, + variableValues, path, deferredFragments, deferMap)}. + - If {shouldInitiateDefer} is {false}: + - Initiate {future}. + - Otherwise, if early execution of deferred fields is desired: + - Following any implementation specific deferral of further execution, + initiate {future}. + - Append all members of {deferredFragments} to {forDeferredFragments}. + - Append {future} to {futures}. +- Return {futures} and {forDeferredFragments}. + +ExecuteDeferredGroupedFieldSet(groupedFieldSet, objectType, objectValue, +variableValues, path, deferMap, deferredFragments): + +- Let {data}, {newIncrementalResults}, and {futures} be the result of + {ExecuteGroupedFieldSet(groupedFieldSet, objectType, objectValue, + variableValues, path, deferredFragments, deferMap)}. +- Let {errors} be the list of all _field error_ raised while executing the + {groupedFieldSet}. +- Let {deferredResult} be an unordered map containing {path}, + {deferredFragments}, {data}, {errors}, {newIncrementalResults}, and {futures}. +- Return {deferredResult}. ## Executing Fields @@ -815,19 +1173,19 @@ coerces any provided argument values, then resolves a value for the field, and finally completes that value either by recursively executing another selection set or coercing a scalar value. -ExecuteField(objectType, objectValue, fieldType, fields, variableValues, path, -subsequentPayloads, asyncRecord): +ExecuteField(objectType, objectValue, fieldType, fieldGroup, variableValues, +path, deferMap): -- Let {field} be the first entry in {fields}. -- Let {fieldName} be the field name of {field}. +- Let {fieldDetails} be the first entry in {fieldGroup}. +- Let {node} be the corresponding entry on {fieldDetails}. +- Let {fieldName} be the field name of {node}. - Append {fieldName} to {path}. - Let {argumentValues} be the result of {CoerceArgumentValues(objectType, field, variableValues)} - Let {resolvedValue} be {ResolveFieldValue(objectType, objectValue, fieldName, argumentValues)}. -- Let {result} be the result of calling {CompleteValue(fieldType, fields, - resolvedValue, variableValues, path, subsequentPayloads, asyncRecord)}. -- Return {result}. +- Return the result of {CompleteValue(fieldType, fields, resolvedValue, + variableValues, path, deferMap)}. ### Coercing Field Arguments @@ -929,59 +1287,49 @@ yielded items satisfies `initialCount` specified on the `@stream` directive. #### Execute Stream Field -ExecuteStreamField(label, iterator, index, fields, innerType, path, -parentRecord, variableValues, subsequentPayloads): +ExecuteStreamField(stream, iterator, fieldGroup, index, innerType, +variableValues): -- Let {streamRecord} be an async payload record created from {label}, {path}, - and {iterator}. -- Initialize {errors} on {streamRecord} to an empty list. +- Let {path} be the corresponding entry on {stream}. - Let {itemPath} be {path} with {index} appended. -- Let {dataExecution} be the asynchronous future value of: - - Wait for the next item from {iterator}. - - If an item is not retrieved because {iterator} has completed: - - Set {isCompletedIterator} to {true} on {streamRecord}. - - Return {null}. - - Let {payload} be an unordered map. - - If an item is not retrieved because of an error: - - Append the encountered error to {errors}. - - Add an entry to {payload} named `items` with the value {null}. - - Otherwise: - - Let {item} be the item retrieved from {iterator}. - - Let {data} be the result of calling {CompleteValue(innerType, fields, - item, variableValues, itemPath, subsequentPayloads, parentRecord)}. - - Append any encountered field errors to {errors}. - - Increment {index}. - - Call {ExecuteStreamField(label, iterator, index, fields, innerType, path, - streamRecord, variableValues, subsequentPayloads)}. - - If a field error was raised, causing a {null} to be propagated to {data}, - and {innerType} is a Non-Nullable type: - - Add an entry to {payload} named `items` with the value {null}. - - Otherwise: - - Add an entry to {payload} named `items` with a list containing the value - {data}. - - If {errors} is not empty: - - Add an entry to {payload} named `errors` with the value {errors}. - - If {label} is defined: - - Add an entry to {payload} named `label` with the value {label}. - - Add an entry to {payload} named `path` with the value {itemPath}. - - If {parentRecord} is defined: - - Wait for the result of {dataExecution} on {parentRecord}. - - Return {payload}. -- Set {dataExecution} on {streamRecord}. -- Append {streamRecord} to {subsequentPayloads}. - -CompleteValue(fieldType, fields, result, variableValues, path, -subsequentPayloads, asyncRecord): +- Let {newDeferMap} be an empty unordered map. +- Wait for the next item from {iterator}. +- If {iterator} is closed, return. +- Let {item} be the next item retrieved via {iterator}. +- Let {nextIndex} be {index} plus one. +- Let {completedItem}, {newIncrementalResults}, and {itemFutures} be the result + of {CompleteValue(innerType, fieldGroup, item, variableValues, itemPath, + newDeferMap)}. +- Initialize {completedItems} to an empty list. +- Append {completedItem} to {completedItems}. +- Let {future} represent the future execution of {ExecuteStreamItem(stream, + path, iterator, fieldGroup, nextIndex, innerType, variableValues)}. +- If early execution of streamed fields is desired: + - Following any implementation specific deferral of further execution, + initiate {execution}. +- Initialize {futures} to an empty list. +- Append {future} to {futures}. +- Append all members of {itemFutures} to {futures}. +- Let {errors} be the list of all _field error_ raised while completing the + item. +- Let {streamedItems} be an unordered map containing {stream}, {completedItems} + {errors}, {newIncrementalResults}, and {futures}. +- Return {streamedItem}. + +CompleteValue(fieldType, fieldGroup, result, variableValues, path, deferMap): - If the {fieldType} is a Non-Null type: - Let {innerType} be the inner type of {fieldType}. - - Let {completedResult} be the result of calling {CompleteValue(innerType, - fields, result, variableValues, path)}. + - Let {completedResult}, {newIncrementalResults}, {forDeferredFragments}, and + {futures} be the result of calling {CompleteValue(innerType, fieldGroup, + result, variableValues, path)}. - If {completedResult} is {null}, raise a _field error_. - Return {completedResult}. - If {result} is {null} (or another internal value similar to {null} such as {undefined}), return {null}. - If {fieldType} is a List type: + - Initialize {newIncrementalResults}, {forDeferredFragments}, and {futures} to + empty lists. - If {result} is not a collection of values, raise a _field error_. - Let {field} be the first entry in {fields}. - Let {innerType} be the inner type of {fieldType}. @@ -1002,20 +1350,36 @@ subsequentPayloads, asyncRecord): - While {result} is not closed: - If {streamDirective} is defined and {index} is greater than or equal to {initialCount}: - - Call {ExecuteStreamField(label, iterator, index, fields, innerType, - path, asyncRecord, subsequentPayloads)}. - - Return {items}. + - Let {stream} be an unordered map containing {label} and {path}. + - Append {stream} to {newIncrementalResults}. + - Let {streamFieldGroup} be the result of + {GetStreamFieldGroup(fieldGroup)}. + - Let {future} represent the future execution of + {ExecuteStreamField(stream, iterator, streamFieldGroup, index, + innerType, variableValues)}. + - If early execution of streamed fields is desired: + - Following any implementation specific deferral of further execution, + initiate {future}. + - Append {future} to {futures}. + - Return {items}, {newIncrementalResults}, {forDeferredFragments}, and + {futures}, - Otherwise: - Wait for the next item from {result} via the {iterator}. - If an item is not retrieved because of an error, raise a _field error_. - - Let {resultItem} be the item retrieved from {result}. + - Let {item} be the item retrieved from {result}. - Let {itemPath} be {path} with {index} appended. - - Let {resolvedItem} be the result of calling {CompleteValue(innerType, - fields, resultItem, variableValues, itemPath, subsequentPayloads, - asyncRecord)}. - - Append {resolvedItem} to {items}. + - Let {completedItem}, {itemNewIncrementalResults} and {itemFutures} be + the result of calling {CompleteValue(innerType, fields, item, + variableValues, itemPath, deferMap)}. + - Append {completedItem} to {items}. + - Append all members of {itemNewIncrementalResults} to + {newIncrementalResults}. + - Append all members of {itemForDeferredFragments} to + {forDeferredFragments}. + - Append all members of {itemFutures} to {futures}. - Increment {index}. - - Return {items}. + - Return {items}, {newIncrementalResults}, {forDeferredFragments}, and + {futures}. - If {fieldType} is a Scalar or Enum type: - Return the result of {CoerceResult(fieldType, result)}. - If {fieldType} is an Object, Interface, or Union type: @@ -1023,10 +1387,72 @@ subsequentPayloads, asyncRecord): - Let {objectType} be {fieldType}. - Otherwise if {fieldType} is an Interface or Union type. - Let {objectType} be {ResolveAbstractType(fieldType, result)}. - - Let {subSelectionSet} be the result of calling {MergeSelectionSets(fields)}. - - Return the result of evaluating {ExecuteSelectionSet(subSelectionSet, - objectType, result, variableValues, path, subsequentPayloads, asyncRecord)} - _normally_ (allowing for parallelization). + - Let {groupedFieldSet}, {groupDetailsMap}, and {deferUsages} be the result of + {ProcessSubSelectionSets(objectType, fieldGroup, variableValues)}. + - Let {newDeferMap} and {newIncrementalResults} be the result of + {GetNewDeferredFragments(newDeferUsages, deferMap, path)}. + - Let {detailsList} be the result of + {GetDeferredGroupedFieldSetDetails(groupDetailsMap, newDeferMap, path)}. + - Let {completed}, {nestedNewIncrementalResults}, + {nestedForDeferredFragments}, and {nestedFutures} be the result of + evaluating {ExecuteGroupedFieldSet(groupedFieldSet, objectType, result, + variableValues, path, newDeferMap)} _normally_ (allowing for + parallelization). + - In parallel, let {futures} and {forDeferredFragments} be the result of + {ExecuteDeferredGroupedFieldSets(queryType, initialValues, variableValues, + detailsList, newDeferMap)}. + - Append all members of {nestedNewIncrementalResults} to + {newIncrementalResults}. + - Append all members of {nestedForDeferredFragments} to + {forDeferredFragments}. + - Append all members of {nestedFutures} to {futures}. + - Return {completed}, {newIncrementalResults}, {forDeferredFragments}, and + {futures}. + +ProcessSubSelectionSets(objectType, fieldGroup, variableValues): + +- Initialize {targetsByKey} to an empty unordered map of sets. +- Initialize {fieldsByTarget} to an empty unordered map of ordered maps. +- Initialize {newDeferUsages} to an empty list. +- Let {fields} and {targets} be the corresponding entries on {fieldGroup}. +- For each {fieldDetails} within {fields}: + - Let {node} and {target} be the corresponding entries on {fieldDetails}. + - Let {fieldSelectionSet} be the selection set of {fieldNode}. + - If {fieldSelectionSet} is null or empty, continue to the next field. + - Let {subfieldsFieldsByTarget}, {subfieldTargetsByKey}, and + {subfieldNewDeferUsages} be the result of calling + {AnalyzeSelectionSet(objectType, fieldSelectionSet, variableValues, + visitedFragments, target)}. + - For each {target} and {subfieldMap} in {subfieldFieldsByTarget}: + - Let {mapForTarget} be the ordered map in {fieldsByTarget} for {target}; + if no such map exists, create it as an empty ordered map. + - For each {responseKey} and {subfieldList} in {subfieldMap}: + - Let {listForResponseKey} be the list in {fieldsByTarget} for + {responseKey}; if no such list exists, create it as an empty list. + - Append all items in {subfieldList} to {listForResponseKey}. + - For each {responseKey} and {targetSet} in {subfieldTargetsByKey}: + - Let {setForResponseKey} be the set in {targetsByKey} for {responseKey}; + if no such set exists, create it as the empty set. + - Add all items in {targetSet} to {setForResponseKey}. + - Append all items in {subfieldNewDeferUsages} to {newDeferUsages}. +- Let {parentTargets} be the corresponding entry on {fieldGroup}. +- Let {groupedFieldSet} and {newGroupedFieldSetDetails} be the result of calling + {BuildGroupedFieldSets(fieldsByTarget, targetsByKey, parentTargets)}. +- Return {groupedFieldSet}, {newGroupedFieldSetDetails}, and {newDeferUsages}. + +GetStreamFieldGroup(fieldGroup): + +- Let {streamFields} be an empty list. +- Let {fields} be the corresponding entry on {fieldGroup}. +- For each {fieldDetails} in {fields}: + - Let {node} be the corresponding entry on {fieldDetails}. + - Let {newFieldDetails} be a new Field Details record created from {node} and + {undefined}. + - Append {newFieldDetails} to {streamFields}. +- Let {targets} be a set containing the value {undefined}. +- Let {streamFieldGroup} be a new Field Group record created from {streamFields} + and {targets}. +- Return {streamFieldGroup}. **Coercing Results** @@ -1089,17 +1515,9 @@ sub-selections. } ``` -After resolving the value for `me`, the selection sets are merged together so -`firstName` and `lastName` can be resolved for one value. - -MergeSelectionSets(fields): - -- Let {selectionSet} be an empty list. -- For each {field} in {fields}: - - Let {fieldSelectionSet} be the selection set of {field}. - - If {fieldSelectionSet} is null or empty, continue to the next field. - - Append all selections in {fieldSelectionSet} to {selectionSet}. -- Return {selectionSet}. +After resolving the value for `me`, the selection sets are merged together by +calling {ProcessSubSelectionSets()} so `firstName` and `lastName` can be +resolved for one value. ### Handling Field Errors @@ -1134,15 +1552,17 @@ resolves to {null}, then the entire list must resolve to {null}. If the `List` type is also wrapped in a `Non-Null`, the field error continues to propagate upwards. -When a field error is raised inside `ExecuteDeferredFragment` or +When a field error is raised inside `ExecuteDeferredGroupedFieldSets` or `ExecuteStreamField`, the defer and stream payloads act as error boundaries. That is, the null resulting from a `Non-Null` type cannot propagate outside of the boundary of the defer or stream payload. If a field error is raised while executing the selection set of a fragment with the `defer` directive, causing a {null} to propagate to the object containing -this fragment, the {null} should not propagate any further. In this case, the -associated Defer Payload's `data` field must be set to {null}. +this fragment, the {null} should not be sent to the client, as this will +overwrite existing data. In this case, the associated Defer Payload's +`completed` entry must include the causative errors, whose presence indicated +the failure of the payload to be included within the final reconcilable object. For example, assume the `month` field is a `Non-Null` type that raises a field error: @@ -1165,21 +1585,24 @@ Response 1, the initial response is sent: ```json example { "data": { "birthday": {} }, + "pending": [ + { "path": ["birthday"], "label": "monthDefer" } + { "path": ["birthday"], "label": "yearDefer" } + ], "hasNext": true } ``` -Response 2, the defer payload for label "monthDefer" is sent. The {data} entry -has been set to {null}, as this {null} as propagated as high as the error -boundary will allow. +Response 2, the defer payload for label "monthDefer" is completed with errors. +Incremental data cannot be sent, as this would overwrite previously sent values. ```json example { - "incremental": [ + "completed": [ { "path": ["birthday"], "label": "monthDefer", - "data": null + "errors": [...] } ], "hasNext": false @@ -1194,21 +1617,27 @@ payload is unaffected by the previous null error. "incremental": [ { "path": ["birthday"], - "label": "yearDefer", "data": { "year": "2022" } } ], + "completed": [ + { + "path": ["birthday"], + "label": "yearDefer" + } + ], "hasNext": false } ``` If the `stream` directive is present on a list field with a Non-Nullable inner type, and a field error has caused a {null} to propagate to the list item, the -{null} should not propagate any further, and the associated Stream Payload's -`item` field must be set to {null}. - -For example, assume the `films` field is a `List` type with an `Non-Null` inner -type. In this case, the second list item raises a field error: +{null} similarly should not be sent to the client, as this will overwrite +existing data. In this case, the associated Stream's `completed` entry must +include the causative errors, whose presence indicated the failure of the stream +to complete successfully. For example, assume the `films` field is a `List` type +with an `Non-Null` inner type. In this case, the second list item raises a field +error: ```graphql example { @@ -1221,19 +1650,20 @@ Response 1, the initial response is sent: ```json example { "data": { "films": ["A New Hope"] }, + "pending": [{ "path": ["films"] }], "hasNext": true } ``` -Response 2, the first stream payload is sent. The {items} entry has been set to -{null}, as this {null} as propagated as high as the error boundary will allow. +Response 2, the stream is completed with errors. Incremental data cannot be +sent, as this would overwrite previously sent values. ```json example { - "incremental": [ + "completed": [ { - "path": ["films", 1], - "items": null + "path": ["films"], + "errors": [...], } ], "hasNext": false @@ -1259,19 +1689,20 @@ Response 1, the initial response is sent: } ``` -Response 2, the first stream payload is sent. The {items} entry has been set to -a list containing {null}, as this {null} has only propagated as high as the list -item. +Response 2, the first stream payload is sent; the stream is not completed. The +{items} entry has been set to a list containing {null}, as this {null} has only +propagated as high as the list item. ```json example { "incremental": [ { "path": ["films", 1], - "items": [null] + "items": [null], + "errors": [...], } ], - "hasNext": false + "hasNext": true } ``` diff --git a/spec/Section 7 -- Response.md b/spec/Section 7 -- Response.md index db50408fa..0bea49561 100644 --- a/spec/Section 7 -- Response.md +++ b/spec/Section 7 -- Response.md @@ -264,11 +264,19 @@ discouraged. } ``` -### Incremental +### Incremental Delivery -The `incremental` entry in the response is a non-empty list of Defer or Stream -payloads. If the response of the GraphQL operation is a response stream, this -field may appear on both the initial and subsequent values. +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. If errors are For example, a query containing both defer and stream: @@ -302,6 +310,10 @@ results. "films": [{ "title": "A New Hope" }] } }, + "pending": [ + { "path": ["person"], "label": "homeWorldDefer" }, + { "path": ["person", "films"], "label": "filmStream" } + ], "hasNext": true } ``` @@ -312,16 +324,15 @@ Response 2, contains the defer payload and the first stream payload. { "incremental": [ { - "label": "homeWorldDefer", "path": ["person"], "data": { "homeWorld": { "name": "Tatooine" } } }, { - "label": "filmsStream", - "path": ["person", "films", 1], + "path": ["person", "films"], "items": [{ "title": "The Empire Strikes Back" }] } ], + "completed": [{ "path": ["person"], "label": "homeWorldDefer" }], "hasNext": true } ``` @@ -335,8 +346,7 @@ would be the final response. { "incremental": [ { - "label": "filmsStream", - "path": ["person", "films", 2], + "path": ["person", "films"], "items": [{ "title": "Return of the Jedi" }] } ], @@ -350,39 +360,40 @@ iterator of the `films` field closes. ```json example { + "completed": [{ "path": ["person", "films"], "label": "filmStream" }], "hasNext": false } ``` -#### Stream payload +#### Streamed data -A stream payload is a map that may appear as an item in the `incremental` entry -of a response. A stream payload is the result of an associated `@stream` -directive in the operation. A stream payload must contain `items` and `path` -entries and may contain `label`, `errors`, and `extensions` entries. +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 `items` is set to `null`, -it indicates that an error has caused a `null` to bubble up to a field higher -than the list field with the associated `@stream` directive. +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. -#### Defer payload +#### Deferred data -A defer payload is a map that may appear as an item in the `incremental` entry -of a response. A defer payload is the result of an associated `@defer` directive -in the operation. A defer payload must contain `data` and `path` entries and may -contain `label`, `errors`, and `extensions` entries. +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 `data` is set to `null`, it indicates -that 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. +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 From 9b274dbd6a090a2f28debaacd40dea9e3ff29702 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 9 Nov 2023 14:07:49 +0200 Subject: [PATCH 64/64] deferred fields should not change ordering of non-deferred --- spec/Section 6 -- Execution.md | 49 +++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/spec/Section 6 -- Execution.md b/spec/Section 6 -- Execution.md index f60743648..313ad78ff 100644 --- a/spec/Section 6 -- Execution.md +++ b/spec/Section 6 -- Execution.md @@ -1076,6 +1076,44 @@ BuildGroupedFieldSets(fieldsByTarget, targetsByKey, parentTargets) - Initialize {details} to an empty unordered map. - Set the entry for {groupedFieldSet} in {details} to {newGroupedFieldSet}. - Set the corresponding entry in {details} to {shouldInitiateDefer}. + - Set the entry for {targets} in {groupDetailsMap} to {details}. +- Return {groupedFieldSet} and {groupDetailsMap}. + +Note: entries are always added to Grouped Field Set records in the order in +which they appear for the first target. Field order for deferred grouped field +sets never alters the field order for the parent. + +GetTargetSetDetails(targetsByKey, parentTargets): + +- Initialize {keysWithParentTargets} to the empty set. +- Initialize {targetSetDetailsMap} to an empty unordered map. +- For each {responseKey} and {targets} in {targetsByKey}: + - If {IsSameSet(targets, parentTargets)} is {true}: + - Append {responseKey} to {keysWithParentTargets}. + - Continue to the next entry in {targetsByKey}. + - For each {key} in {targetSetDetailsMap}: + - If {IsSameSet(targets, key)} is {true}, let {targetSetDetails} be the map + in {targetSetDetailsMap} for {targets}. + - If {targetSetDetails} is defined: + - Let {keys} be the corresponding entry on {targetSetDetails}. + - Add {responseKey} to {keys}. + - Otherwise: + - Initialize {keys} to the empty set. + - Add {responseKey} to {keys}. + - Let {shouldInitiateDefer} be {false}. + - For each {target} in {targets}: + - Let {remainingFieldsForTarget} be the entry in {remainingFieldsByTarget} + for {target}. + - Let {nodes} be the list in {remainingFieldsByTarget} for {responseKey}. + - Remove the entry for {responseKey} from {remainingFieldsByTarget}. + - For each {node} of {nodes}: + - Let {fieldDetails} be a new Field Details record created from {node} + and {target}. + - Append {fieldDetails} to the {fields} entry on {fieldGroup}. + - Let {shouldInitiateDefer} be the corresponding entry on {targetSetDetails}. + - Initialize {details} to an empty unordered map. + - Set the entry for {groupedFieldSet} in {details} to {newGroupedFieldSet}. + - Set the corresponding entry in {details} to {shouldInitiateDefer}. - Set the entry for {maskingTargets} in {groupDetailsMap} to {details}. - Return {groupedFieldSet} and {groupDetailsMap}. @@ -1116,7 +1154,7 @@ GetTargetSetDetails(targetsByKey, parentTargets): - Set {shouldInitiateDefer} equal to {true}. - Create {newTargetSetDetails} as an map containing {keys} and {shouldInitiateDefer}. - - Set the entry in {targetSetDetailsMap} for {maskingTargets} to + - Set the entry in {targetSetDetailsMap} for {targets} to {newTargetSetDetails}. - Return {keysWithParentTargets} and {targetSetDetailsMap}. @@ -1129,6 +1167,15 @@ IsSameSet(setA, setB): - Return {false}. - Return {true}. +GetOrderedResponseKeys(targets, fieldsByTarget): + +- Let {firstTarget} be the first entry in {targets}. +- Assert that {firstTarget} is defined. +- Let {firstFields} be the entry for {firstTarget} in {fieldsByTarget}. +- Assert that {firstFields} is defined. +- Let {responseKeys} be the keys of {firstFields}. +- Return {responseKeys}. + ## Executing Deferred Grouped Field Sets ExecuteDeferredGroupedFieldSets(objectType, objectValue, variableValues, path,