Skip to content

Spec edits for incremental delivery, Section 3 only #1132

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 10 commits into
base: incremental-integration
Choose a base branch
from

Conversation

robrichard
Copy link
Contributor

@robrichard robrichard commented Jan 7, 2025

Extracted from the full PR (#1110) and targeting an integration branch to aid in review.

Helpful reference material:

Response format examples: graphql/defer-stream-wg#69
Glossary: graphql/defer-stream-wg#106
GraphQL Conf talk: https://www.youtube.com/watch?v=LEyDeNoobT0

Response types

/** Currently defined types */
type RequestErrorResult = {
  errors: Error[] // request errors only
  extensions?: object
}
type ExecutionResult = {
  data: {} | null
  errors?: Error[] // execution errors only
  extensions?: object
}
type ResponseStream = ExecutionResult[]

/** New types that are yielded by an Incremental Stream */
interface InitialExecutionResult extends ExecutionResult {
  incremental: IncrementalResult[]
  pending?: Pending[]
  completed?: CompletedResult[],
  hasNext?: boolean;
}

type SubsequentExecutionResult = {
  incremental?: IncrementalResult[]
  pending?: Pending[]
  completed?: CompletedResult[],
  hasNext?: boolean;
}

type IncrementalStream = [InitialExecutionResult, ...SubsequentExecutionResult[]];

/** Response is updated to include IncrementalStream as a new member */
type Response = RequestErrorResult | ExecutionResult | ResponseStream | IncrementalStream;

type IncrementalResult = IncrementalObjectResult | IncrementalListResult

yaacovCR pushed a commit to graphql/graphql-js that referenced this pull request Jan 12, 2025
Updated to reflect spec draft
graphql/graphql-spec#1132

Also changed the argument order to match the spec draft
@robrichard robrichard requested a review from benjie January 30, 2025 19:56
Copy link
Member

@benjie benjie left a comment

Choose a reason for hiding this comment

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

Lots of nit-picky comments but I think we're pretty close! Do we have a glossary somewhere? I think we need to be really crisp on terms like "result", "response", "payload" and the like.

Comment on lines 2188 to 2189
responses: the initial response containing all non-deferred data, while
subsequent responses include deferred data.
Copy link
Member

Choose a reason for hiding this comment

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

I believe that we've refined the terminology around this: there's one response. The response for stream/defer is a stream consisting of an initial result payload followed by a number of incremental payloads. I couldn't find where we discussed this, so please correct as appropriate.

Suggested change
responses: the initial response containing all non-deferred data, while
subsequent responses include deferred data.
payloads: the initial payload containing all non-deferred data, while subsequent
payloads include deferred data.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@benjie after the last discussion we had, I decided to drop payload. The response section now says:

The result of a GraphQL request must be either a single initial response or an
incremental stream. The response will be an incremental stream when the GraphQL
service has deferred or streamed data as a result of the @defer or @stream
directives. When the result of the GraphQL operation is an incremental stream,
the first value will be an initial response, followed by one or more subsequent
responses.

Comment on lines 2255 to 2257
- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related
note below). When `false`, the field must not be streamed and all list items
must be initially included. Defaults to `true` when omitted.
Copy link
Member

Choose a reason for hiding this comment

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

The "and" here implies this is an additional behaviour

Suggested change
- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related
note below). When `false`, the field must not be streamed and all list items
must be initially included. Defaults to `true` when omitted.
- `if: Boolean! = true` - When `true`, field _should_ be streamed (see related
note below). When `false`, the field must behave as if the `@stream` directive
is not present—it must not be streamed and all of the list items must be
included. Defaults to `true` when omitted.

@robrichard robrichard force-pushed the incremental-integration-type-system branch from d5322ae to 32785b8 Compare February 7, 2025 20:33
@robrichard
Copy link
Contributor Author

@benjie I added a rough glossary here: graphql/defer-stream-wg#106, I'll keep refining it as we go

@robrichard robrichard requested a review from benjie February 24, 2025 19:38
@robrichard robrichard force-pushed the incremental-integration-type-system branch 2 times, most recently from 176172f to 7c0ba73 Compare February 24, 2025 19:42
Copy link
Contributor

@mjmahone mjmahone left a comment

Choose a reason for hiding this comment

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

These changes look correct to me, I don't think the definition of the @stream/@defer directives have changed in a very long time, so this feels good.

@robrichard robrichard force-pushed the incremental-integration-type-system branch from cf072a4 to 47f362c Compare March 7, 2025 17:07
@robrichard robrichard force-pushed the incremental-integration-type-system branch from 0640179 to b3187e0 Compare April 3, 2025 11:47
@robrichard robrichard changed the base branch from incremental-integration to main April 11, 2025 16:06
@robrichard robrichard changed the base branch from main to incremental-integration April 11, 2025 16:06
@robrichard robrichard force-pushed the incremental-integration-type-system branch from b3187e0 to 3b8799e Compare July 1, 2025 16:06
@robrichard robrichard requested a review from Keweiqu July 1, 2025 16:07
@robrichard robrichard force-pushed the incremental-integration-type-system branch from 3b8799e to 64dc38f Compare July 1, 2025 16:08
Copy link
Member

@benjie benjie left a comment

Choose a reason for hiding this comment

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

This is looking really good; all my suggestions are minor except the last one which I think warrants some extra work.

Comment on lines +2215 to +2216
consisting of an _initial execution result_ containing all non-deferred data,
followed by one or more _subsequent execution result_ including the deferred
Copy link
Member

Choose a reason for hiding this comment

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

Terminology:

  • initial execution result - confirms to execution result (just adds a couple more fields) 👍
  • subsequent execution result - sounds like it should conform to execution result, but doesn't ({hasNext: false} very obviously fails this) 👎

If we continue with this naming, we should revisit the definition of execution result.

Did we already discuss changing to subsequent incremental execution result? To my ear the word incremental1 makes it much clearer that the payload won't necessarily conform to execution result (a bit like "partial" - suddenly all the fields are no longer required).

Footnotes

  1. You could think of each additional payload in a subscription as being a "subsequent execution result", but the term "incremental execution result" would not fit for subscriptions - it's clearly a different beast.

Copy link
Contributor Author

@robrichard robrichard Jul 25, 2025

Choose a reason for hiding this comment

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

@benjie I decided to go with subsequent execution result instead of incremental execution result because we also need a name for the object in the incremental entry. As of the last discussion we are calling it an incremental result, (along with the other objects completed result, pending result).

The argument about subscriptions is compelling so I'm open to changing it, but I'm concerned having both an Incremental Execution Result and an Incremental Result will be confusing.

Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we can use some other term that indicates this result augments the previous/is partial rather than being a standalone but subsequent execution result. I’m thinking it should also make sense for live or other future things we want to include in the stream.

  • modifier (execution) result
  • partial execution result
  • layer execution result
  • Deferred execution result (don’t like this one so much, it suffers the same issue as subsequent, though not as strongly)
  • Patch result
  • additive result
  • execution update result

That last one feels right, because it breaks the phrase “execution result” so you don’t expect conformance to those specifications. It also makes sense for hasNext: false.

consisting of an initial execution result containing all non-deferred data,
followed by one or more execution update result including the deferred

query myQuery($shouldDefer: Boolean! = true) {
user {
name
...someFragment @defer(label: "someLabel", if: $shouldDefer)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
...someFragment @defer(label: "someLabel", if: $shouldDefer)
...someFragment @defer(if: $shouldDefer, label: "someLabel")

query myQuery($shouldStream: Boolean! = true) {
user {
friends(first: 10)
@stream(label: "friendsStream", initialCount: 5, if: $shouldStream) {
Copy link
Member

@benjie benjie Jul 24, 2025

Choose a reason for hiding this comment

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

Suggested change
@stream(label: "friendsStream", initialCount: 5, if: $shouldStream) {
@stream(if: $shouldStream, label: "friendsStream", initialCount: 5) {

Comment on lines +2238 to +2239
- `if: Boolean! = true` - When `true`, fragment _should_ be deferred (see
related note below). When `false`, fragment must not be deferred. Defaults to
Copy link
Member

Choose a reason for hiding this comment

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

Minor, but we would expect this note to be in the same section. Having it under @stream's heading is unexpected without further signposting.

`@defer` and/or `@stream` directives. This also applies to the `initialCount`
argument on the `@stream` directive. Clients must be able to process a streamed
field result that contains more initial list items than what was specified in
the `initialCount` argument.
Copy link
Member

Choose a reason for hiding this comment

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

This feels like it warrants its own header, similar to Supporting Subscriptions at Scale (beautiful bit of alliteration there!), that can be referenced by both blocks.

Also, this feels like it should be normative. Putting "must" in a non-normative note feels wrong.

We should also be much clearer that this is on a case by case basis, so for example:

{
  list @stream {
    field1
    ...@defer {
      field2
    }
  }
}

In this case, different entries in the streamed list may or may not defer field2 - the @defer is not ignored "wholesale" but on an execution-position by execution-position basis.

Also, I think we should more strongly empower schema authors to opt out when they see fit, because there are many situations where streaming may add complexity without reducing latency for the client. For example: reverse cursor pagination...

query Things($cursor: String) {
  things(first: 1000, after: $cursor) {
    nodes @stream(initialCount: 2) { id name }
  }
}

^ for this, it can clearly stream from the datasource; however:

query ThingsReverse($cursor: String) {
  things(last: 1000, before: $cursor) {
    nodes @stream(initialCount: 2) { id name }
  }
}

^ in this case, you can't necessarily stream from the datasource, because the last record to come from the datasource (assuming you're fetching in reverse, discovering each next row as you go) is actually the first record you should return to the user.

Similarly:

query ThingsAwkward($cursor: String) {
  things(first: 1000, last: 100, after: $cursor) {
    nodes @stream(initialCount: 2) { id name }
  }
}

^ in this case we may not know which the last 100 will be until we've fetched them all. (This is of course a pathological query.)

Of course streaming the underlying data from your data source and streaming the result of the field's execution are two different things (and happen at different positions in the waterfall), so YMMV.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants