Skip to content

Commit 492337d

Browse files
committed
Fix no-cache stream/defer case where streaming was incorrectly reported
1 parent 7a8684e commit 492337d

2 files changed

Lines changed: 120 additions & 18 deletions

File tree

src/core/QueryInfo.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -271,27 +271,35 @@ export class QueryInfo<
271271

272272
let result: MarkQueryResult<any, ExtensionsWithStreamInfo> = {
273273
...incrementalResult,
274-
dataState:
275-
incrementalResult.data == null ? "empty"
276-
// TODO: This is too naive. For stream arrays, this might be complete
277-
: this.hasNext ? "streaming"
278-
: "complete",
274+
dataState: incrementalResult.data == null ? "empty" : "complete",
279275
};
280276

281277
if (skipCache) {
282-
return {
283-
...result,
284-
dataState:
285-
result.data == null ? "empty"
286-
// Ww can simplify the checks for no-cache queries because the
287-
// result is purely server driven which means we can assume a
288-
// well-formed GraphQL response. In this case, `streaming` only
289-
// makes sense if we are using the `@defer` directive and there are
290-
// more chunks still streaming (i.e. this.hasNext is true). Purely
291-
// `@stream` queries should always be complete.
292-
: this.hasNext && hasDirectives(["defer"], query) ? "streaming"
293-
: "complete",
294-
};
278+
const hasPendingDefer = this.incremental?.pending?.some(
279+
(pending) => this.incremental?.getPendingType?.(pending.id) === "defer"
280+
);
281+
282+
if (
283+
hasPendingDefer ||
284+
// The Defer20220824Handler cannot track pending/completed incremental
285+
// chunks due to its data format so we naively set dataState to
286+
// streaming if we are still processing chunks. The only case where
287+
// streaming is incorrect and should actually be complete is when
288+
// both a @defer and @stream boundary is present and the @defer chunk
289+
// has completed before the `@stream` array.
290+
//
291+
// Assigning the naive "streaming" value avoids a much more expensive
292+
// pass over `result.data` that would otherwise need to traverse the
293+
// selection sets and evaluate the data object at each defer boundary
294+
// to see if it fulfills the selection set. For such a narrow case where
295+
// its incorrect on a format that is now outdated is not worth the
296+
// fix so we are ok with reporting a `streaming` here.
297+
(!this.incremental?.pending && this.hasNext)
298+
) {
299+
result.dataState = "streaming";
300+
}
301+
302+
return result;
295303
}
296304

297305
if (shouldWriteResult(result, errorPolicy)) {

src/core/__tests__/client.watchQuery/streamGraphQL17Alpha9.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,100 @@ test("handles @defer inside @stream", async () => {
896896
await expect(observableStream).not.toEmitAnything();
897897
});
898898

899+
test('reports "complete" once a sibling @defer boundary resolves while a @stream is still streaming with a "no-cache" fetch policy', async () => {
900+
const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } =
901+
mockDeferStreamGraphQL17Alpha9();
902+
const client = new ApolloClient({
903+
link: httpLink,
904+
cache: new InMemoryCache(),
905+
incrementalHandler: new GraphQL17Alpha9Handler(),
906+
});
907+
908+
const query = gql`
909+
query {
910+
friendList @stream(initialCount: 1) {
911+
id
912+
name
913+
}
914+
... @defer {
915+
someScalar
916+
}
917+
}
918+
`;
919+
920+
const observableStream = new ObservableStream(
921+
client.watchQuery({ query, fetchPolicy: "no-cache" })
922+
);
923+
924+
await expect(observableStream).toEmitTypedValue({
925+
data: undefined,
926+
dataState: "empty",
927+
loading: true,
928+
networkStatus: NetworkStatus.loading,
929+
partial: true,
930+
});
931+
932+
enqueueInitialChunk({
933+
data: { friendList: [{ __typename: "Friend", id: "1", name: "Luke" }] },
934+
pending: [
935+
{ id: "0", path: [] },
936+
{ id: "1", path: ["friendList"] },
937+
],
938+
hasNext: true,
939+
} as any);
940+
941+
await expect(observableStream).toEmitTypedValue({
942+
data: markAsStreaming({
943+
friendList: [{ __typename: "Friend", id: "1", name: "Luke" }],
944+
}),
945+
dataState: "streaming",
946+
loading: true,
947+
networkStatus: NetworkStatus.streaming,
948+
partial: true,
949+
});
950+
951+
enqueueSubsequentChunk({
952+
incremental: [{ id: "0", data: { someScalar: "resolved" } }],
953+
completed: [{ id: "0" }],
954+
hasNext: true,
955+
} as any);
956+
957+
await expect(observableStream).toEmitTypedValue({
958+
data: {
959+
friendList: [{ __typename: "Friend", id: "1", name: "Luke" }],
960+
someScalar: "resolved",
961+
},
962+
dataState: "complete",
963+
loading: true,
964+
networkStatus: NetworkStatus.streaming,
965+
partial: false,
966+
});
967+
968+
enqueueSubsequentChunk({
969+
incremental: [
970+
{ id: "1", items: [{ __typename: "Friend", id: "2", name: "Han" }] },
971+
],
972+
completed: [{ id: "1" }],
973+
hasNext: false,
974+
} as any);
975+
976+
await expect(observableStream).toEmitTypedValue({
977+
data: {
978+
friendList: [
979+
{ __typename: "Friend", id: "1", name: "Luke" },
980+
{ __typename: "Friend", id: "2", name: "Han" },
981+
],
982+
someScalar: "resolved",
983+
},
984+
dataState: "complete",
985+
loading: false,
986+
networkStatus: NetworkStatus.ready,
987+
partial: false,
988+
});
989+
990+
await expect(observableStream).not.toEmitAnything();
991+
});
992+
899993
test("can use custom merge function to combine cached and streamed lists", async () => {
900994
const cache = new InMemoryCache({
901995
typePolicies: {

0 commit comments

Comments
 (0)