Skip to content
This repository has been archived by the owner on Mar 20, 2023. It is now read-only.

Support for experimental @defer & @stream #583

Merged
merged 3 commits into from
Nov 20, 2020

Conversation

robrichard
Copy link
Collaborator

Support for streaming multi-part http responses as implemented by

graphql/graphql-js#2319
https://github.com/relay-tools/fetch-multipart-graphql

@acao acao added the PR: feature 🚀 requires increase of "minor" version number label Feb 22, 2020
@acao
Copy link
Member

acao commented Feb 22, 2020

looks great, thanks @robrichard! once we get one of the graphql-js proposals merged for 15.0.0 we can revisit the tests here. saw there are two proposal PRs for graphql-js

src/index.ts Outdated
if (isAsyncIterable(executeResult)) {
response.setHeader('Content-Type', 'multipart/mixed; boundary="-"');
sendPartialResponse(pretty, response, formattedResult);
for await (let payload of executeResult) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@robrichard What happens when the request is terminated by the client (i.e. I navigate away from the page or close the browser)? Would it make sense to add an event listener to response for the close event and call the AsyncIterator's return method inside?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I think that makes sense. I will implement that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added a handler for the close event to call return on the underlying async iterable.

@IvanGoncharov this required disabling some lint rules (see d883ae7#diff-a2a171449d862fe29692ce031981047d7ab755ae7f84c707aef80701b3ea0c80R419). Do you think this is ok or do you have a better way to implement this?

@robrichard robrichard force-pushed the defer-stream branch 3 times, most recently from 7c2e919 to 06f9ded Compare October 28, 2020 15:21
@n1ru4l
Copy link

n1ru4l commented Oct 28, 2020

I got a bit too excited so I created a playground making it easier for people to test this new stuff! https://github.com/n1ru4l/graphql-bleeding-edge-playground

Awesome work!!!

@acao
Copy link
Member

acao commented Oct 28, 2020

@n1ru4l awesome! it looks great in graphiql as well. we are going to archive playground soon though :(

@n1ru4l
Copy link

n1ru4l commented Oct 28, 2020

ah wait it is graphiql 😂 playground ist just the name of the repo it uses graphiql :D

Please check out the GraphiQL fetchers 😇

@acao
Copy link
Member

acao commented Oct 28, 2020

@n1ru4l oh awesome I see, thanks for that! did you see this? graphql/graphiql#1700 (comment)

@acao
Copy link
Member

acao commented Oct 28, 2020

@n1ru4l are you interested in helping me create this PR in graphiql monorepo for the new fetcher patterns? Otherwise I might not have time until next weekend :/

@n1ru4l
Copy link

n1ru4l commented Oct 29, 2020

@robrichard Would it be possible for you to do single commits instead of force-pushing, I am having a hard time following the changes 😅 I also updated the playground to the latest commit.

@robrichard
Copy link
Collaborator Author

I've been force pushing because it's harder to keep rebasing when there are a lot of commits and this PR has been open for a long time.

src/index.ts Outdated
@@ -344,16 +372,18 @@ export function graphqlHTTP(options: Options): Middleware {
});
}

// Collect and apply any metadata extensions if a function was provided.
// https://graphql.github.io/graphql-spec/#sec-Response-Format
if (isAsyncIterable(executeResult)) {
Copy link

Choose a reason for hiding this comment

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

Since executeFn is typed to return an AsyncIterableIterator, it would make sense to instead use a isAsyncIterableIterator function. That way it is also possible to call executeResult.return?.() in case the request is aborted without adding ts-ignores.

Copy link

@n1ru4l n1ru4l Nov 1, 2020

Choose a reason for hiding this comment

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

Nevermind, if you use the isAsyncIterable function the type checker already is aware of the actual type (AsyncIterableIterator), making it easy to call .return()

@n1ru4l
Copy link

n1ru4l commented Oct 30, 2020

It would also make sense to adjust the renderGraphiQL function to provide a multipart compatible fetcher

src/index.ts Outdated
// Get first payload from AsyncIterator. http status will reflect status
// of this payload.
const asyncIterator = getAsyncIterator<ExecutionResult>(executeResult);
const { value } = await asyncIterator.next();
Copy link
Member

Choose a reason for hiding this comment

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

await can throw and should be inside try 🔼

Copy link
Member

Choose a reason for hiding this comment

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

test for that would be great. You can do that by using custom executeFn that return an async iterator.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@IvanGoncharov do you think it makes sense for the try/catch that is currently wrapping await executeFn({...}) to be expanded to also wrap this?

Copy link
Member

Choose a reason for hiding this comment

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

@robrichard Yes, I think it would work 👍

@robrichard robrichard force-pushed the defer-stream branch 3 times, most recently from df074c9 to aa62e24 Compare November 11, 2020 22:45
result: FormattedExecutionResult | FormattedExecutionPatchResult,
): void {
const json = JSON.stringify(result, null, pretty ? 2 : 0);
const chunk = Buffer.from(json, 'utf8');
Copy link

Choose a reason for hiding this comment

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

will this always be utf8?

Copy link
Member

@acao acao Nov 14, 2020

Choose a reason for hiding this comment

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

this has me wanting to bring up protocolbuffers, messagepack and other binary formats, as well as yaml, however that seems way out of scope for this PR, and should probably begin as an HTTP transport RFC proposal. GraphiQL users have on multiple occasions requested support for these formats, especially for server-to-server transmissions and iot

back to this discussion, though, would it work to use a binary encoding format latin1 with JSON? I've never tried that myself. if someone was returning a binary format like BSON for example, then we should be returning content type of application/bson, right? Is there a case where someone would be sending application/json and not using utf8 encoding?

if there are indeed more ways to mix and match charset and content type with JSON than I'm aware of, maybe this should be controlled by the request headers then, and fall back to utf8 as default?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link

@maraisr maraisr Nov 14, 2020

Choose a reason for hiding this comment

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

meros also at this time uses UTF8. It's a force of habit. But there might be people out there needing some out of the ordinary encodings.

Maybe it's a cross that bridge when we get there?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

express-graphql already only sends utf8 so I don't think there's any change in behavior here: https://github.com/graphql/express-graphql/blob/master/src/index.ts#L531

As far as the spec, I'm not sure if it's worth specifying what encodings are required. FWIW it seems like JSON requires either UTF-8, UTF-16, or UTF-32 but recommends UTF8 for maximum interoperablity.

https://tools.ietf.org/html/rfc7159#section-8.1

@maraisr
Copy link

maraisr commented Nov 14, 2020

Wish github had stacked PR's 😔 robrichard#1

@robrichard robrichard force-pushed the defer-stream branch 3 times, most recently from 78fdee3 to e303a2c Compare November 19, 2020 15:41
@IvanGoncharov IvanGoncharov changed the base branch from master to defer-stream November 20, 2020 12:12
@IvanGoncharov IvanGoncharov merged commit 95e7324 into graphql:defer-stream Nov 20, 2020
robrichard added a commit that referenced this pull request Dec 3, 2020
robrichard added a commit that referenced this pull request Feb 9, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
PR: feature 🚀 requires increase of "minor" version number
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants