Clients can't reliably distinguish if defer/stream was inlined or not #52
Replies: 12 comments 46 replies
-
I like that fix but I'd like to go even stronger and not have the choice between absent and ie, the proposal would be, either there is a single map that has no |
Beta Was this translation helpful? Give feedback.
-
I'm concerned that this may lead to response explosion. Take for example this {
me {
friends {
id
friends {
id
... @defer(label: "1") { name bio }
... @defer(label: "2") { age bio }
... @defer(label: "3") { dob bio }
... @defer(label: "4") { avatarUrl bio }
... @defer(label: "5") { id bio }
... @defer(label: "6") { joinedAt bio }
... @defer(label: "7") { lastSeen bio }
... @defer(label: "8") { location bio }
}
}
}
} In a non-deferred query, we'd end up with effectively this query: {
me {
friends {
id
friends {
id
name
bio
age
dob
avatarUrl
joinedAt
lastSeen
location
}
}
}
} (Lets assume that the friends field always returns N records.) {data:{me:{
friends:[
N x {id:..., friends: [
N x {id:..., name:..., bio:..., age:..., dob:..., avatarUrl: ..., joinedAt:..., lastSeen:..., location:...}
] }
]
}}} Most likely we already have all these fields thanks to In a forced-incremental context we'd get something more like: {
data:{me:{friends:[
N x {id: ..., friends: [
N x {id: ...}
]}
]}},
incremental:[
N x N x {label:"1",path:["me","friends",n,"friends",m],data:{name:..., bio:...}},
N x N x {label:"2",path:["me","friends",n,"friends",m],data:{age:..., bio:...}},
N x N x {label:"3",path:["me","friends",n,"friends",m],data:{dob:..., bio:...}},
N x N x {label:"4",path:["me","friends",n,"friends",m],data:{avatarUrl:..., bio:...}},
N x N x {label:"5",path:["me","friends",n,"friends",m],data:{id:..., bio:...}},
N x N x {label:"6",path:["me","friends",n,"friends",m],data:{joinedAt:..., bio:...}},
N x N x {label:"7",path:["me","friends",n,"friends",m],data:{lastSeen:..., bio:...}},
N x N x {label:"8",path:["me","friends",n,"friends",m],data:{location:..., bio:...}},
]
} (And this is without using fragments to expand the amount of data needed in each Imagine that the bio is a fairly standard Twitter-length bio of 160 bytes, and This is highly redundant data and will compress well on the wire via |
Beta Was this translation helpful? Give feedback.
-
See #58 which suggests to require fragment aliasing |
Beta Was this translation helpful? Give feedback.
-
For defer, see graphql/graphql-spec#998 which alternatively suggests soft aliases or references that don't wrap the fragment's fields, but rather add another "reference" alongside them to indicate the state of the fragment. |
Beta Was this translation helpful? Give feedback.
-
For stream, mentioned at the 11/17/2022 graphql-wg meeting, we could suggest the following response: {
"data": {
"stream1": [],
"stream2": [],
},
"incremental": [{
"streamHasStarted": true,
"path": ["stream1"],
"label": "Stream1",
}]
} |
Beta Was this translation helpful? Give feedback.
-
Adding this idea as a jumping off point for discussion. This would be very straightforward to implement, and I think it could cover the all of the currently ambiguous cases. We add a new array field Example query {
person(id: "1") {
...HomeWorldFragment @defer(label: "homeWorldDefer")
name
films @stream(initialCount: 0, label: "filmsStream") {
title
}
}
}
fragment HomeWorldFragment on Person {
homeWorld {
name
}
} Response 1: {
"data": {
"person": {
"name": "Luke Skywalker",
"films": []
}
},
"pendingPayloads": [
{ "label": "filmsStream", "path": ["person", "films", 0] },
{ "label": "homeWorldDefer", "path": ["person"] }
],
"hasNext": true
} Response 2: {
"incremental": [{
"label": "filmsStream",
"path": ["person", "films", 0],
"items": [
{ "title": "A New Hope" }
]
}],
"pendingPayloads": [
{ "label": "filmsStream", "path": ["person", "films", 1] },
{ "label": "homeWorldDefer", "path": ["person"] }
],
"hasNext": true
} Response 3: {
"incremental": [{
"label": "homeWorldDefer",
"path": ["person"],
"data": {
"homeWorld": { "name": "Tatooine" }
}
}],
"pendingPayloads": [
{ "label": "filmsStream", "path": ["person", "films", 1] }
],
"hasNext": true
} Caveats
|
Beta Was this translation helpful? Give feedback.
-
Variation of @robrichard suggestion #52 (comment) Instead of adding new top-level Response 1: {
"data": {
"person": {
"name": "Luke Skywalker",
"films": []
}
},
"incremental": [
{ "label": "filmsStream", "path": ["person", "films", 0], "hasNext": true },
{ "label": "homeWorldDefer", "path": ["person"], "hasNext": true }
],
"hasNext": true
} Response 2: {
"incremental": [
{
"label": "filmsStream",
"path": ["person", "films", 0],
"hasNext": false,
"items": [
{ "title": "A New Hope" }
]
},
{ "label": "filmsStream", "path": ["person", "films", 1], "hasNext": true }
],
"hasNext": true
} Response 3: {
"incremental": [
{
"label": "homeWorldDefer",
"path": ["person"],
"data": {
"homeWorld": { "name": "Tatooine" }
}
},
],
"hasNext": true
}
It also solves this issue you just send: {
"incremental": [
{
"label": "homeWorldDefer",
"path": ["person"],
"data": {
"homeWorld": { "name": "Tatooine" }
}
},
{ "label": "filmsStream", "path": ["person", "films", 1], "hasNext": false }
],
"hasNext": true
} |
Beta Was this translation helpful? Give feedback.
-
Expanding on all of the previous discussions (thank you to @robrichard, @IvanGoncharov, @yaacovCR and everyone else who has helped iterate on these solutions), I've proposed an alternative on Discord which I'll reflect here. My proposal (after todays on-call iteration - thanks to everyone's input) is to introduce the new No label on @stream:All fields with {
me { ...UserFrag ...UserDetailFrag }
}
fragment UserFrag on User {
friends @stream { ... }
}
fragment UserDetailFrag on User {
friends @stream { ... }
} The Label on
|
Beta Was this translation helpful? Give feedback.
-
One of the concerns with the If we wanted to make things as easy as possible for clients, having an inline handle to point to future incremental responses would completely eliminate the need for a field path. Inline Pending PointersSimilar to above, I'd propose using a @defer exampleQuery:
Responses might look like:
then
and finally:
@stream examplesQuery:
Response option 1: pointer is last element of list
Response option 2: pointer lives parallel to streamed field
|
Beta Was this translation helpful? Give feedback.
-
I appreciate the advantages that the pending approach give us => there is a clear translation between what is happening on the server and what is communicated to the client. It is easy to reason about (if not necessarily easy for the server to decide what to do!) I do wonder whether it is so GraphQL-esque. The promise of GraphQL is "ask for what you need, get exactly what you want." Flowing from this principle, we should provide for the client what the client is interested in, rather than what we have available. The client wants to know which portions of the result tree have been delivered; not a description of what is pending. So, in general, I favor metadata that labels what we have already, whether we call this a metafield, fragment aliases, fragment references, etc. But can we push this further? Working off of @mjmahone 's example above: {
me {
userId
... @defer(label: "1") {
something
... @defer(label: "2") { innerMerged }
... on User @defer(label: "3") { innerUnique }
}
... @defer(label: "4") { cheapField }
}
} Could yield:
and then
and finally:
How would stream work? Probably differently, more along the lines of the pending approach. |
Beta Was this translation helpful? Give feedback.
-
Adding a new proposal that further iterates on @benjie and @IvanGoncharov's suggestions Proposal
Caveats
Example A: Multiple defers on the same object are mergedquery {
friendList {
... @defer {
...FriendFrag
... @defer {
...FriendFrag
... @defer {
...FriendFrag
... @defer {
...FriendFrag
}
}
}
}
}
}
fragment FriendFrag on Friend {
id
name
} // Responses
[
{
"data": { "friendList": [{}, {}, {}] },
"pending": [
{ "path": ["friendList", 0] },
{ "path": ["friendList", 1] },
{ "path": ["friendList", 2] }
],
"hasNext": true
},
{
"hasNext": false,
"incremental": [
{ "data": { "id": "1", "name": "Luke" }, "path": ["friendList", 0] },
{ "data": { "id": "2", "name": "Han" }, "path": ["friendList", 1] },
{ "data": { "id": "3", "name": "Leia" }, "path": ["friendList", 2] }
]
}
] Example B: Defer inside of stream, stream closes synchronouslyAlthough the incremental payloads for defer and stream have the same path, they are not ambiguous because the defer payload contains query {
friendList @stream {
id
... @defer {
name
}
}
} // Responses
[
{
"data": {
"friendList": []
},
"pending": [{ "path": ["friendList"] }], // stream is pending
"hasNext": true
},
{
"hasNext": true,
"pending": [{ "path": ["friendList", 0] }], // defer on first list item is pending
"incremental": [{ "path": ["friendList", 0], "items": [{ "id": "1" }] }]
},
{
"hasNext": true,
"pending": [{ "path": ["friendList", 1] }],
"incremental": [{ "path": ["friendList", 0], "data": { "name": "Luke" } }]
},
{
"hasNext": true,
"incremental": [{ "path": ["friendList", 1], "items": [{ "id": "2" }, "completed": true] }] // stream is complete
},
{
"hasNext": false,
"incremental": [{ "path": ["friendList", 1], "data": { "name": "Han" } }]
}
] Example C: Stream inside of defer, stream closes asynchronouslyquery {
... @defer {
friendList @stream {
id
}
}
}
// Responses
[
{
"data": {},
"pending": [{ "path": [] }], // defer is pending
"hasNext": true
},
{
"hasNext": true,
"pending": [{ "path": ["friendList"] }], // stream is pending
"incremental": [{ "path": [], "data": { "friendList": [] } }]
},
{
"hasNext": true,
"incremental": [{ "path": ["friendList", 0], "items": [{ "id": "1" }] }]
},
{
"hasNext": true,
"incremental": [{ "path": ["friendList", 1], "items": [{ "id": "2" }] }]
},
{
"hasNext": true,
"incremental": [{ "path": ["friendList", 2], "items": [{ "id": "3" }] }]
},
{
"hasNext": false,
"incremental": [{ "path": ["friendList"], "completed": true }] // stream is completed
}
] Open question How are fields merged that are part of both a deferred and non-deferred selection set? CollectFields currently only analyzes and merges the selection set at the current object level. Would CollectFields need to deeply analyze the entire selection set before it can decide which fields are deferred or not? If we do remove fields out of deferred fragments, we can relax the validation rule preventing streamed fields from merging. Example D: Nested objects with overlapping deferred and non-deferred fields{
f2 { a b c { d e f { h i } } }
... @defer {
f2 { a b c { d e f { h j } } }
}
} Can we make this equivalent to this? {
f2 { a b c { d e f { h i } } }
... @defer {
f2 { c { f { j } } }
}
} Example E: With a naive implementation there is still the opportunity for multiple defers pointing to the same pathquery {
nestedObject {
... @defer {
foo
}
}
... @defer {
nestedObject {
... @defer {
bar
}
}
}
} Results from naive implementation: Note that there are two objects each in [
{
"data": { "nestedObject": {} },
"pending": [
{ "path": [] },
{ "path": ["nestedObject"] }
],
"hasNext": true
},
{
"pending": [
{ "path": ["nestedObject"] }
],
"incremental": [
{
"path": [],
"data": { "nestedObject": {} }
}
],
"hasNext": true
},
{
"incremental": [
{
"path": ["nestedObject"],
"data": { "foo": "foo" }
}
],
"hasNext": true
},
{
"incremental": [
{
"path": ["nestedObject"],
"data": { "bar": "bar" }
}
],
"hasNext": false
}
] Improved implementation An improved merging algorithm, as proposed in graphql/graphql-js#3820 can give a result that does not do any deduplication, but guarantees all payloads have unique paths: [
{
"data": { "nestedObject": {} },
"hasNext": true,
"pending": [
{"path": []},
{"path": ["nestedObject"]}
]
},
{
"incremental": [
{
"data": { "nestedObject": {} },
"path": []
},
{
"data": { "foo": "foo", "bar": "foo" },
"path": ["nestedObject"]
}
],
"hasNext": false
}
] Example F: Optimally deduplicating fields could depend on the order which data is resolvedIf the first defer is resolved first, a.b.e.f can be removed from the second defer. If the second defer is resolved first, it cannot be removed, and the second defer can be removed entirely. {
a {
b {
c {
d
}
... @defer {
e {
f
}
}
}
}
... @defer {
a {
b {
e {
f
}
}
}
g {
h
}
}
} |
Beta Was this translation helpful? Give feedback.
-
Discussion around this issue is being continued in these three related discussions: |
Beta Was this translation helpful? Give feedback.
-
The current proposal allows the inlining of the result of
@defer
directly intodata
of the initial response.This makes it impossible to figure out if certain fields have already been delivered or not.
For example, with the below query:
and below response:
It's impossible to know if deferred "deferLabel" is inlined or not.
So client code that waits for this particular label is locked until
hasNext: false
.Stream Example:
and below response:
It's impossible to know if
myList
has additional items being prepared, or ifmyList
is actually an empty array.Proposed fix
Allow implementations to fully ignore
@stream/defer
at once (noincremental
andhasNext
either absent orfalse
).In all other cases, steam/defer results can be batched into
incremental
of the initial response.Beta Was this translation helpful? Give feedback.
All reactions