Liliana Matos, Director, Front-End Engineering @ 1stdibs Rob Richard, Senior Director, Front-End Engineering @ 1stdibs
Defer and Stream Directives in GraphQL
Improve Latency with Incremental Delivery
Defer and Stream Directives in GraphQL Improve Latency with - - PowerPoint PPT Presentation
Defer and Stream Directives in GraphQL Improve Latency with Incremental Delivery Liliana Matos, Director, Front-End Engineering @ 1stdibs Rob Richard, Senior Director, Front-End Engineering @ 1stdibs About Online marketplace for luxury
Liliana Matos, Director, Front-End Engineering @ 1stdibs Rob Richard, Senior Director, Front-End Engineering @ 1stdibs
Improve Latency with Incremental Delivery
across multiple verticals
React, and Relay
and Bangalore, India
GraphQL Specification to support incremental delivery for state-less queries
query TalksQuery { talks(first: 6) @stream ( label: "talkStream", initialCount: 3 ) { name ...TalkComments @defer(label: "talkCommentsDefer") } } fragment TalkComments on Talk { comments { body } }
pre-fetching, come with undesirable trade-offs
requested data to the server without undesirable trade-offs
separate query after initial query
fields
/** Original Query */ query SpeakerQuery($speakerId: String!) { speaker(speakerId: $speakerId) { name ...SpeakerPicture } } fragment SpeakerPicture on Speaker { picture { height width url } } /** Split Queries */ query SpeakerInitialQuery($speakerId: String!) { speaker(speakerId: $speakerId) { name } } query SpeakerFollowUpQuery($speakerId: String!) { speaker(speakerId: $speakerId) { ...SpeakerPicture } }
action
https://www.apollographql.com/blog/introducing-defer-in-apollo-server-f6797c4e9d6e/
spread.
defaults to true.
directives in an operation.
directive @defer(if: Boolean, label: String) on FRAGMENT_SPREAD
Example
query SpeakerQuery($speakerId: String!) { speaker(speakerId: $speakerId) { name ...SpeakerPicture @defer(label: “speakerPictureDefer”) } } fragment SpeakerPicture on Speaker { picture { height width url } } // Response Payloads // Payload 1 { "data": { "speaker": { "name": "Jesse Rosenberger" } }, "hasNext": true } // Payload 2 { "label": "speakerPictureDefer", "path": ["speaker"], "data": { "picture": { "height": 200, "width": 200, "url": "jesse-headshot.jpg" } }, "hasNext": false }
initial response.
directive @stream(if: Boolean, label: String, initialCount: Int) on FIELD
Example
query SpeakersQuery { speakers(first: 3) @stream(label: “speakerStream", initialCount: 1) { name } } // Response Payloads // Payload 1 { "data": { "speakers": [ { "name": "Jesse Rosenberger" } ] }, "hasNext": true } // Payload 2 { "label": "speakerStream", “path": ["speakers", 1], "data": { "name": "Liliana Matos" }, "hasNext": true } // Payload 3 { "label": "speakerStream", "path": ["speakers", 2], "data": { "name": "Rob Richard" }, "hasNext": false }
Overview
execution will return multiple payloads
present in this payload
Details
directive that corresponds to this results
sent for this operation.
set
Example
query SpeakersQuery { speakers(first: 2) @stream(label: "speakerStream", initialCount: 1) { name ...SpeakerPicture @defer(label: "speakerPictureDefer") } } fragment SpeakerPicture on Speaker { picture { url } }
// Response Payloads // Payload 1 { "data": { "speakers": [ { "name": "Jesse Rosenberger" } ] }, "hasNext": true } // Payload 2 { "label": "SpeakerPictureDefer", "path": ["speakers", 0, "picture"], "data": { "url": "jesse-headshot.jpg" }, "hasNext": true } // Payload 3 { "label": "SpeakerStream", "path": ["speakers", 1], "data": { "name": "Liliana Matos" }, "hasNext": true } // Payload 4 { "label": "SpeakerPictureDefer", "path": ["speakers", 1, "picture"], "data": { "url": "liliana-headshot.jpg" }, "hasNext": false }
fragment SpeakerPicture on Speaker { picture { height width url } } { "data": { "speaker": { "name": "Jesse Rosenberger" } }, "hasNext": true } query SpeakerQuery($speakerId: String!) { speaker(speakerId: $speakerId) { name ...SpeakerPicture @defer(label: “speakerPictureDefer”) } } { "label": "speakerPictureDefer", "path": ["speaker"], "data": { "picture": { "height": 200, "width": 200, "url": "jesse-headshot.jpg" } }, "hasNext": false }
Fork execution to dispatcher Initial Payload Subsequent Payload
const resolvers = { Query: { items: async function (_, { filters }): Promise<Array<Item>> { const items = await api.getFilteredItems({ filters }); return items; }, }, };
const resolvers = { Query: { items: async function (_, { filters }): Array<Promise<<Item>> { const itemIds = await api.filterItems({ filters }); return itemIds.map(async itemId => await api.getItemById(itemId)); }, }, };
const resolvers = { Query: { users: async function* (): AsyncIterable<User> { const db = new Database(); while (true) { // select one document const result = await db.getNext(); // end iteration if there are no documents returned if (!result) { break; } yield result; } return; }, }, };
Content-Length: 590 { "data": { "talks": [{ "title": "Opening Keynote", "time": "10:30-11:00", "speaker": { "name": "Jesse Rosenberger" }, }, ...] } }
Content-Length: 140 { "path": ["talks", 0, "comments"], "data": [{ "body": "Loved this!" }] }
query ConferenceQuery { talks { title time speaker { name } ...commentsFragment @defer } } fragment commentsFragment on Talk { body }
be served over HTTP
encoding and multipart responses
function sendPartialResponse( response: $Response, result: AsyncExecutionResult, ): void { const json = JSON.stringify(result, null, 2); const chunk = Buffer.from(json, 'utf8'); const data = [ '', '---', 'Content-Type: application/json; charset=utf-8', 'Content-Length: ' + String(chunk.length), '', chunk, '', ].join('\r\n'); response.write(data); } // Close connection response.end();
const response = await fetch(url, { method, headers, body }) // Don't call response.json()! // That waits for the whole response to finish loading. // const json = await response.json() const reader = response.body.getReader(); while (true) { const { value, done } = await reader.read(); if (done) return; handleChunk(value); }
// This works in Internet Explorer 7! const xhr = new XMLHttpRequest(); let index = 0; xhr.open(method, url); xhr.addEventListener("readystatechange", function onReadyStateChange() { const chunk = xhr.response.substr(index); handleChunk(chunk); index = xhr.responseText.length; }); xhr.send(body);
fetch-multipart-graphql
DeferStream.md