Skip to content

Instantly share code, notes, and snippets.

@robrichard
Last active June 2, 2023 14:47
Show Gist options
  • Save robrichard/309687cc1046428169277ac6473c18fa to your computer and use it in GitHub Desktop.
Save robrichard/309687cc1046428169277ac6473c18fa to your computer and use it in GitHub Desktop.
defer-stream proposal 2023-04-14

Summary of the latest "no-overlapping-branches" proposal by @benjie & @robrichard

Features

  • No overlapping branching.
  • No duplicate delivery of any fields
  • Reduces the risk of response amplification
  • No complex WeakMap/similar caching shenanigans.
  • No query rewriting.
  • No look-ahead.
  • Consistent delivery of fragments.
  • Each deferred fragment can be delivered independently
  • Labels are entirely optional - only exist for providing information to the client in case it wants to correlate pendings with the original directives.
  • Labels do not affect the grouping of defers.
  • Deferred fragment data will not be delivered if:
    • A null bubbled above the highest field in the deferred fragment
    • A null bubbles to a field that is shared with another deferred fragment

The incremental array is the actual data to be applied to the response, while the pending and completed arrays return "metadata" about the execution. They are used to inform clients that defers are being executed and when all fields for that defer have been delivered.

Defer Examples

Example A

Overlapping fields from initial payload are not sent in subsequent payloads. Fragment consistency is preserved. Even if "MyFragment" is ready earlier, it is not sent until "j" is also ready.

query ExampleA {    
  f2 { a b c { d e f { h i } } }
  ... @defer {
    MyFragment: __typename
    f2 { a b c { d e f { h j } } }
  }
}

Example Result:

[
  {
    "data": {
      "f2": {
        "a": "a",
        "b": "b",
        "c": {
          "d": "d",
          "e": "e",
          "f": { "h": "h", "i": "i" }
        }
      }
    },
    // `pending`'s `path` field is always the location of `@defer` directive.
    "pending": [{ "path": [], "id": "0" }],
    "hasNext": true
  },
  {
    "incremental": [
      { "id": "0", "data": { "MyFragment": "Query" } }
      // `subPath` in `incremental` object is a sub-path of the `path` sent in the `pending` object.
      { "id": "0", "subPath": ["f2", "c", "f"], "data": { "j": "j" } }
    ],
    "completed": [{ "id": "0" }],
    "hasNext": false
  }
]

Example A2

Overlapping fields from parent defers are also not duplicated

query Example A2 {    
  f2 { a b c { d e f { h i } } }
  ... @defer(label: "D1") {
    f2 { a b c { d e f {
      h i j k
      ...@defer(label: "D2") {
        h i j k l m
      }
    } } }
  }
}

Example Result:

[
  {
    "data": {"f2": {"a": "A", "b": "B", "c": {
      "d": "D", "e": "E", "f": {
        "h": "H", "i": "I"
      }
    }}},
    "pending": [{"id": "0", "path": [], "label": "D1"}],
    "hasNext": true
  },
  {
    "incremental": [
      {"id": "0", "subPath": ["f2", "c", "f"] "data": {"j": "J", "k": "K"}}
    ],
    "pending": [{"id": "1", "path": ["f2", "c", "f"], "label": "D2"}],
    "completed": [
      {"id": "0"}
    ],
    "hasNext": true
  },
  {
    "incremental": [
      {"id": "1", "data": {"l": "L", "m": "M"}}
    ],
    "completed": [
      {"id": "1"}
    ],
    "hasNext": false
  }
]

Example B

Overlapping fields e & f are sent with whichever fragment completes first, and not sent in subsequent payloads.

{
  a {
    b {
      c {
        d
      }
      ... @defer(label: "Red") {
        e {
          f
        }
        potentiallySlowFieldA
      }
    }
  }
  ... @defer(label: "Blue") {
    a {
      b {
        e {
          f
        }
      }
    }
    g {
      h
    }
    potentiallySlowFieldB
  }
}

Example Result when potentiallySlowFieldA completes first:

[
  {
    "data": {
      "a": { "b": { "c": { "d": "d" } } }
    },
    "pending": [
      { "path": [], "id": "0", "label": "Blue" },
      { "path": ["a", "b"], "id": "1", "label": "Red" }
    ],
    "hasNext": true
  },
  {
    "incremental": [
      { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } },
      // e is returned in a separate incremental data because there is overlap with other defer paths
      { "id": "1", "data": { "e": { "f": "f" } } }
    ],
    "completed": [{ "id": "1" }]
    "hasNext": true
  },
  {
    "incremental": [
      // `g` & `potentiallySlowFieldB` are returned in the same incremental data because they both belong to the same set of defers
      { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } }
    ],
    "completed": [{ "id": "0" }]
    "hasNext": false
  }
]

Example Result when potentiallySlowFieldB completes first:

[
  {
    "data": {
      "a": { "b": { "c": { "d": "d" } } }
    },
    "pending": [
      { "path": [], "id": "0", "label": "Blue" },
      { "path": ["a", "b"], "id": "1", "label": "Red" }
    ],
    "hasNext": true
  },
  {
    "incremental": [
      { "id": "0", "data": { "g": { "h": "h" }, "potentiallySlowFieldB": "potentiallySlowFieldB" } },
      // e is returned in a separate incremental data because there is overlap with other defer paths
      // id with longer path is preferred
      { "id": "1", "data": { "e": { "f": "f" } } }
    ],
    "completed": [{ "id": "0" }]
    "hasNext": true
  },
  {
    "incremental": [
      { "id": "1", "data": { "potentiallySlowFieldA": "potentiallySlowFieldA" } }
    ],
   "completed": [{ "id": "1" }]
    "hasNext": false
  }
]

Example D

Non-overlapping fields in child selections of overlapping fields are sent in separate incremental objects.

query ExampleD {
  me {
    ... @defer {
      list {
        item {
          id
        }
      }
    }
  ...@defer {
    me {
      list {
        item {
          value
        }
      }
    }
  }
}

Example Result

[
  {
    "data": { "me": {} },
    "pending": [
      { "path": [], "id":" 0" },
      { "path": ["me"], "id": "1" }
    ],
    "hasNext": true
  },
  {
    "incremental": [
      {
        "id": "1",
        "data": { "list": [{ "item": {} }, { "item": {} }, { "item": {} }] }
      },
      // in this case "id" resolves before value
      { "id": "1", "subPath": ["list", "item", 0], "data": { "id": "1" } },
      { "id": "1", "subPath": ["list", "item", 1], "data": { "id": 2 } },
      { "id": "1", "subPath": ["list", "item", 2], "data": { "id": 3 } }
    ],
    "completed": [{ "id": "1" }],
    "hasNext": true
  },
  {
    "incremental": [
      { "id": "0", "subPath": ["me", "list", "item", 0], "data": { "value": "Foo" } },
      { "id": "0", "subPath": ["me", "list", "item", 1], "data": { "value": "Bar" } },
      { "id": "0", "subPath": ["me", "list", "item", 2], "data": { "value": "Baz" } }
    ],
    "completed": [{ "id":" 0" }],
    "hasNext": false
  }
]

Example F

Defer fragments with no fields are skipped entirely

query ExampleF {
  me {
    ...@defer(label: "A") {
      ...@defer(label: "B") {
        a b
      }
    }
  }
}

Example Result

[
  {
    "data": {
      "me": {}
    },
    "pending": [
      {"id": "0", "path": ["me"], "label": "B"}
    ],
    "hasNext": true
  },
  {
    "incremental": [
      {"id":0 , "data": {"a": "A", "b": "B"}}
    ],
    "completed": [
      {"id": "0"}
    ],
    "hasNext": false
  }
]

Example G

Deferred fragments at the same path are delivered independently

query ExampleG {
  me {
    ...Projects
    ...Billing @defer(label: "Billing")
  }
}

fragment Projects on User { 
  id
  avatarUrl
  projects {
    name
  }
}

fragment Billing on User {
  tier
  renewalDate
  latestInvoiceTotal
  ...PreviousInvoices @defer(label:"Prev")
}


fragment PreviousInvoices on User {
  previousInvoices { name }
}

Example Result

[
  {
    "data": {
      "me": {
        "id": 1,
        "avatarUrl": "http://…",
        "projects": [{ "name": "My Project" }]
      }
    },
    "pending": [
      { "id": 0, "path": ["me"], "label": "Billing" },
      { "id": 1, "path": ["me"], "label": "Prev" }]
    ],
    "hasNext": true
  },
  {
    "incremental": [
      {
        "id": 0,
        "data": {
          "tier": "BRONZE",
          "renewalDate": "2023-03-20",
          "latestInvoiceTotal": "$12.34"
        }
      }
    ],
    "completed": [{ "id": 0 }],
    "hasNext": true
  },
  {
    "incremental": [
      {
        "id": 1,
        "data": { "previousInvoices": [{ "name": "My Invoice" }] }
      }
    ],
    "completed": [{ "id": 1 }],
    "hasNext": false
  }
]

Example H

If a field in a subsequent defer nulls a previously sent field due to null bubbling, the entire fragment will not be delivered. Clients should treat this fragment similar to a fragment that is @skip(if: true).

Assume baz resolves before qux and qux is non-nullable and returns null.

query ExampleH {
  ... @defer(label: "A") {
    me {
      foo {
        bar {
          baz
        }
      }
    } 
  }
  me {
    ... @defer(label: "B") {
      anotherField
      foo {
        bar {
          qux
        }
      }   
    }
  }
}

Example Result

[
  {
    "data": {
      "me": {}
    },
    "pending": [
      {"id": "0", "path": [], "label": "A"},
      {"id": "1", "path": ["me"], "label": "B"}
    ],
    "hasNext": true
  },
  {
    "incremental": [
      {
        "id": "0", 
        "subPath": ["me"], 
        "data": {
          "foo": { 
            "bar": {
              "baz": "BAZ"
            }
          }
        }
      }
    ],
    "completed": [
      {"id": "0"}
    ],
    "hasNext": true
  },
    {
    // No "incremental" objects are sent. "completed" object contains the 
    // errors that triggered the conflict due to null bubbling. 
    "completed": [
      {
        "id": "1", 
        "errors": [
          {
            "message": "Cannot return null for non-nullable field Bar.qux.",
            "locations": [...],
            "path": ["foo", "bar", "qux"]
          }
        ]
      }
    ],
    "hasNext": false
  }
]

Stream Examples

Example I

query ExampleI {
  person(id: "1") {
    ...HomeWorldFragment @defer(label: "homeWorldDefer")
    name
    films @stream(initialCount: 2, label: "filmsStream") {
      title
    }
  }
}
fragment HomeWorldFragment on Person {
  homeworld {
    name
  }
}
[
  {
    "data": {
      "person": {
        "name": "Luke Skywalker",
        "films": [
          { "title": "A New Hope" },
          { "title": "The Empire Strikes Back" }
        ]
      }
    },
    "pending": [
      {"id": "0", "path": ["person"], "label": "homeWorldDefer"},
      // `pending` path is location of stream directive
      // one `pending` object is sent for the entire stream
      {"id": "1", "path": ["person", "films"], "label": "filmsStream"}
    ],
    "hasNext": true
  },
  {
    "incremental": [
      // No subpath or index, items must be returned in order. 
      // Multiple items can be returned in array
      { "id": "1", "items": [{"title": "Return of the Jedi"}] },
    ],    
    "hasNext": true
  },
  {
    // `completed` object may be sent after final list item, in cases where the server cannot know the list has ended synchronously 
    "completed": [{"id": "1"}],
    "hasNext": true
  }.
  {
    "incremental": [
      {"id": "0", "data": {"homeworld": {"name": "Tatooine"}}},
    ],
    "completed": [{"id": "0"}],
    "hasNext": false
  }
]

Example J

If after some list fields are streamed:

  • either the underlying datasource errors
  • or a null bubbles up to the list field

A "completed" object is sent for the stream with the errors and no more streamed results will be sent

query ExampleJ {
  person(id: "1") {
    films @stream(initialCount: 1, label: "filmsStream") {
      title
    }
  }
}
[
  {
    "data": {
      "person": {
        "films": [{ "title": "A New Hope" }]
      }
    },
    "pending": [
      {"id": "1", "path": ["person", "films"], "label": "filmsStream"}
    ],
    "hasNext": true
  },
  {
    "incremental": [
      // one streamed list item is delivered successfully
      { "id": "1", "items": [{"title": "The Empire Strikes Back"}] }
    ],    
    "hasNext": true
  },
    {
    // An error is encountered while loading the next list item
    "completed": [
      {
        "id": "1", 
        "errors": [
          {
            "message": "Cannot return null for non-nullable field Person.films.",
            "locations": [...],
            "path": ["person", "filmns"]
          }
        ]
      }
    ],    
    "hasNext": false
  }
]

Solution Criteria Evaluation

[A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [K] [L] [M] [N] [O] [P] [Q]
✅️ ✅️ ✅️ ✅️ ✅️ ✅️ ✅️ ✅️ ✅️ ✅️ 🚫 🚫

FAQ's

Why not done: true on incremental instead of completed?

It's possible for a single incremental object result to "complete" more than one deferred fragment. In this example assume baz takes longer to resolve than the other fields.

{
  me {
    ... @defer(label: "A") {
      foo
      bar
    }
    ... @defer(label: "B") {
      bar
      baz
    }
    ... @defer(label: "C") {
      foo
      baz
    }
  }
}

Example Response

{
  data: { me: {} },
  pending: [
    {id: 0, label: "A", path: ["me"]},
    {id: 1, label: "B", path: ["me"]},
    {id: 2, label: "C", path: ["me"]}
  ]
}
{
  incremental: [
    {id: 0, data: {foo: "foo"}},
    {id: 0, data: {bar: "bar"}}
  ],
  completed: [
    {id: 0}
  ]
}
{
  incremental: [
    {id: 1, data: {baz: "baz"}}
  ],
  completed: [
    {id: 1},
    {id: 2}
  ]
}

Should incremental[i].id be an array, since data is shared by multiple ids?

No, because then it would introduce confusion on which id is the prefix of the subpath. This single id does not signify that this is the only fragment being delivered as multiple fragments can be delivered together. We will prefer the id with the longest path in pending to minimize the size of the subpath.

@Keweiqu
Copy link

Keweiqu commented May 4, 2023

Hey Rob, thanks for this detailed write up and so many interesting examples!!
Regarding example H, I'm not sure why "anotherField" need to suffer the casualty and not be delivered? Maybe you can specify the schema's nullability?

@mjmahone
Copy link

mjmahone commented May 4, 2023

For the discussion we had in the WG, let me see if I can describe what I want to have on the client, when I talk about broken/linked sub-trees.

For example B:

{
  a {
    b {
      c {
        d
      }
      ... @defer(label: "A") {
        e {
          f
        }
        potentiallySlowFieldA
      }
    }
  }
  ... @defer(label: "B") {
    a {
      b {
        e {
          f
        }
      }
    }
    g {
      h
    }
    potentiallySlowFieldB
  }
}

I could see the response looking something like:

[
  {
    "data": {
      "__defer:B": "0",
      "a": { 
        "b": { 
          "__defer:A": "1",
          "c": { "d": "d" } 
        }
      }
    },
    "pending": {
      "0": { "path": [], "label": "B" },
      "1": { "path": ["a", "b"], "label": "A" }
    },
    "hasNext": true
  },
  {
    "incremental": {
      "1": {
        "data": {
          "__defer:A": "2",
          "potentiallySlowFieldA": "potentiallySlowFieldA" }
      },
      "2": {
        "data": {
          "e": { "f": "f" }
        }
      }
    },
    "completed": ["1", "2"],
    "hasNext": true
  },
  {
    "incremental": {
      "0": {
        "data": {
          "a": {
            "b": {
              "__defer:B": "2"
            }
          },
          "g": { "h": "h" },
          "potentiallySlowFieldB": "potentiallySlowFieldB"
        }
      }
    },
    "completed": ["0"],
    "hasNext": false
  }
]

Basically with this structure, on the client I have some choices:

  • whenever I see a __defer: field, merge the data we're pointing to in at this point, if it's available (and if the data is already merged, just drop the __defer: field). This is equivalent to the merging algorithms we discussed where the client holds a mutable tree to merge and update.
  • when rendering a response (react-like, so from the root response callback), walk the tree like normal. When you encounter a __defer:, find the defer payload by ID by looping through all incremental responses and finding the ID (there will only be one), and then continue traversing like normal
    • For this algorithm, you may be referring to previously provided defer-identifiers, and everything will still work as expected. But you need to have a 1:1 mapping between incremental payloads and IDs.
  • Other more clever options, like merging into a flattened set of responses so that my components that do deferring can hold onto a subscription and get a callback exactly when the id they're waiting on comes back.

@mjmahone
Copy link

mjmahone commented May 4, 2023

^There's almost certainly improvements that we'd need to the above example (for instance, maybe the meta-field is more like Label: __defer: { id: "0" } or __defer: { labels: ["A"], id: "0" }, maybe there's a way to de-duplicate all redundant paths, etc.)

@robrichard
Copy link
Author

@Keweiqu I think if we delivered the other fields in the fragments it could put clients into a bad state where they understand that all the fields from defer "B" have been delivered, but it has an object for "bar" and no result for "qux". Since the schema has a non-null constraint on "qux" this is a state that should be impossible.

We discussed options where bar is resent as null. Maybe there's some advanced clients that can simultaneously render both states if they occur in unrelated UI components, but if you are constructing the final reconciled object you end up either dropping the whole fragment, rendering the fragment in the above described invalid state, or remove data that has already been displayed to the user.

This should only happen when non-null fields are shared across sibling defers. We were thinking this situation could potentially be addressed in the future by the client controlled nullability proposal.

@yaacovCR
Copy link

yaacovCR commented May 8, 2023

EDIT: See below for my current proposal. Original comment available in revisions.

@mjmahone
Copy link

mjmahone commented May 8, 2023

Let's see if we can get the pointer structure without __<somehting> inline pointers:

[
  {
    "data": {
      "a": { 
        "b": {
          "c": { "d": "d" } 
        }
      }
    },
    "defers": {
      "0": { "path": [], "labels": ["B"] },
      "1": { "path": ["a", "b"], "labels": ["A", "B"] }
    },
    "hasNext": true
  },
  {
    "incremental": {
      "1": {
        "data": {
          "potentiallySlowFieldA": "potentiallySlowFieldA"
        },
        "defers": {
          "2": { "path": [], labels: ["A", "B"] },
        }
      },
      "2": {
        "data": {
          "e": { "f": "f" }
        }
      }
    },
    "completed": ["1", "2"],
    "hasNext": true
  },
  {
    "incremental": {
      "0": {
        "data": {
          "a": {
            "b": {}
          },
          "g": { "h": "h" },
          "potentiallySlowFieldB": "potentiallySlowFieldB"
        }
      },
      "defers": {
          "2": { "path": ["a", "b"], labels: ["A", "B"] },
        }
    },
    "completed": ["0"],
    "hasNext": false
  }
]

@yaacovCR
Copy link

yaacovCR commented May 10, 2023

EDIT: See below for my current proposal. Original comment available in revisions.

@yaacovCR
Copy link

yaacovCR commented May 24, 2023

My current proposal for meeting @mjmahone 's goals (as I understand them) of (1) making it easy for clients to build up the deferred fragments from parent + its grouped field sets without a single "fully reconcilable object" acting as a mutable cache and (2) providing tree like structures for updates rather than lists:

(1) Instead of embedding pending/incremental data in two different tree-like structures, I send only one tree-like structure containing entries for both types of metadata., i.e. the pending/incremental IDs.
(2) I removed the ability of previously sent result IDs to shorten the path.

TLDR: In my opinion, this significantly increases readability, which was the main concern in earlier iterations:

Worked Examples:

Example A:

query ExampleA {
  f2 {
    a
    b
    c {
      d
      e
      f {
        h
        i
      }
    }
  }
  ... @defer(label: "Defer") {
    MyFragment: __typename
    f2 {
      a
      b
      c {
        d
        e
        f {
          h
          j
        }
      }
    }
  }
}

Example Result:

[
  {
    "data": {
      "f2": {
        "a": "a",
        "b": "b",
        "c": {
          "d": "d",
          "e": "e",
          "f": {
            "h": "h",
            "i": "i"
          }
        }
      }
    },
    "metadata": {
      "--pending--": ["R0"]
    },
    "pending": {
      "R0": [{ "label": "Defer" }]
    },
    "hasNext": true
  },
  {
    "metadata": {
      "++incremental++": ["I0"],
      "f2": {
        "c": {
          "f": {
            "++incremental++": ["I1"]
          }
        }
      }
    },
    "incremental": {
      "I0": {
        "data": {
          "MyFragment": "Query"
        },
        "resultIds": ["R0"]
      },
      "I1": { "resultIds": ["R0"], "data": { "j": "j" } }
    },
    "completed": {
      // the algorithm for generating immutable data on completion is to take the data from the initial result
      // and insert the incremental payloads at the locations indicated by "+"
      "R0": { "insert": ["I0", "I1"] }
    },
    "hasNext": false
  }
]

Example A2:

query Example A2 {
  f2 { a b c { d e f { h i } } }
  ... @defer(label: "D1") {
    f2 { a b c { d e f {
      h i j k
      ...@defer(label: "D2") {
        h i j k l m
      }
    } } }
  }
}
[
  {
    "data": {
      "f2": {
        "a": "A",
        "b": "B",
        "c": {
          "d": "D",
          "e": "E",
          "f": {
            "h": "H",
            "i": "I"
          }
        }
      }
    },
    "metadata": {
      "--pending--": ["R0"]
    },
    "pending": {
      "R0": [{ "label": "D1" }]
    },
    "hasNext": true
  },
  {
    "metadata": {
      "++incremental++": ["I0"],
      "f2": {
        "c": {
          "f": {
            "--pending--": ["R1"]
          }
        }
      }
    },
    "pending": {
      "R1": { "parentId": "R0" }
    },
    "incremental": {
      "I0": {
        "data": {
          "j": "J",
          "k": "K"
        },
        "resultIds": ["R0", "R1"]
      }
    },
    "completed": {
      "R0": { "insert": ["I0"] }
    },
    "hasNext": true
  },
  {
    "metadata": {
      "f2": {
        "c": {
          "f": {
            "++incremental++": ["I1"]
          }
        }
      }
    },
    "incremental": {
      "I1": {
        "data": {
          "l": "L",
          "m": "M"
        },
        "resultIds": ["R1"]
      }
    },
    "completed": {
      "R1": { "insert": ["I0", "I1"], "on": "R0" }
    },
    "hasNext": false
  }
]

Example B:

{
  a {
    b {
      c {
        d
      }
      ... @defer(label: "Red") {
        e {
          f
        }
        potentiallySlowFieldA
      }
    }
  }
  ... @defer(label: "Blue") {
    a {
      b {
        e {
          f
        }
      }
    }
    g {
      h
    }
    potentiallySlowFieldB
  }
}

If potentiallySlowFieldA returns first:

[
  {
    "data": {
      "a": { "b": { "c": { "d": "d" } } }
    },
    "metadata": {
      "--pending--": ["R0"],
      "a": {
        "b": {
          "--pending": ["R1"]
        }
      }
    },
    "pending": {
      "R0": [{ "label": "Blue" }],
      "R1": [{ "label": "Red" }]
    },
    "hasNext": true
  },
  {
    "metadata": {
      "a": {
        "b": {
          "++incremental++": ["I0", "I1"]
        }
      }
    },
    "incremental": {
      "I0": {
        "data": {
          "potentiallySlowFieldA": "potentiallySlowFieldA"
        },
        "resultIds": ["R1"]
      },
      // e is returned in a separate incremental data because there is overlap with other defer paths
      "I1": {
        "data": {
          "e": { "f": "f" }
        },
        "resultIds": ["R0", "R1"]
      }
    },
    "completed": {
      "R1": { "insert": ["I0", "I1"] }
    },
    "hasNext": true
  },
  {
    "metadata": {
      "++incremental++": ["I2"]
    },
    "incremental": {
      // `g` & `potentiallySlowFieldB` are returned in the same incremental data because they both belong to the same set of defers
      "I2": {
        "data": {
          "g": { "h": "h" },
          "potentiallySlowFieldB": "potentiallySlowFieldB"
        },
        "resultIds": ["R0"]
      }
    },
    "completed": {
      "R0": { "insert": ["I1", "I2"] }
    },
    "hasNext": false
  }
]

If potentiallySlowFieldB returns first:

[
  {
    "data": {
      "a": { "b": { "c": { "d": "d" } } }
    },
    "metadata": {
      "--pending--": ["R0"],
      "a": {
        "b": {
          "--pending": ["R1"]
        }
      }
    },
    "hasNext": true
  },
  {
    "metadata": {
      "++incremental++": ["I2"],
      "a": {
        "b": {
          "++incremental++": ["I1"]
        }
      }
    },
    "incremental": {
      "I2": {
        // `g` & `potentiallySlowFieldB` are returned in the same incremental data because they both belong to the same set of defers
        "data": {
          "g": { "h": "h" },
          "potentiallySlowFieldB": "potentiallySlowFieldB"
        },
        "resultIds": ["R0"]
      },
      // e is returned in a separate incremental data because there is overlap with other defer paths
      // the result id with the longer path is preferred to make the tree shorter, so we use R1
      // even though we are currently delivering R0. For readability, we could change this,
      // although it would make the trees larger
      "I1": {
        "data": {
          "e": { "f": "f" }
        },
        "resultIds": ["R0", "R1"]
      },
    },
    "completed": {
      "R0": { "insert": ["I1", "I2"] }
    },
    "hasNext": true
  },
  {
    "metadata": {
      "++incremental++": ["I2"],
      "a": {
        "b": {
          "++incremental++": ["I1"]
        }
      }
    },
    "incremental": {
      "I0": {
        "data": {
          "potentiallySlowFieldA": "potentiallySlowFieldA"
        },
        "resultIds": ["R1"]
      },
      // e is returned in a separate incremental data because there is overlap with other defer paths
      "I1": {
        "data": {
          "e": { "f": "f" }
        },
        "resultIds": ["R0", "R1"],
      }
    },
    "completed": {
      "R1": { "insert": ["I0", "I1"] }
    },
    "hasNext": false
  }
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment