Skip to content

Commit 67c16c9

Browse files
authoredFeb 6, 2025··
keep deferred inFlightLinkObservables until the response is finished (#12338)
1 parent 80a68aa commit 67c16c9

File tree

5 files changed

+150
-6
lines changed

5 files changed

+150
-6
lines changed
 

‎.changeset/quiet-apricots-reply.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
In case of a multipart response (e.g. with `@defer`), query deduplication will
6+
now keep going until the final chunk has been received.

‎.size-limits.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 42196,
3-
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34405
2+
"dist/apollo-client.min.cjs": 42225,
3+
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34432
44
}

‎src/config/jest/setup.ts

+3
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ if (!Symbol.asyncDispose) {
3636

3737
// @ts-ignore
3838
expect.addEqualityTesters([areApolloErrorsEqual, areGraphQLErrorsEqual]);
39+
40+
// not available in JSDOM 🙄
41+
global.structuredClone = (val) => JSON.parse(JSON.stringify(val));

‎src/core/QueryManager.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1182,8 +1182,12 @@ export class QueryManager<TStore> {
11821182
]);
11831183
observable = entry.observable = concast;
11841184

1185-
concast.beforeNext(() => {
1186-
inFlightLinkObservables.remove(printedServerQuery, varJson);
1185+
concast.beforeNext(function cb(method, arg: FetchResult) {
1186+
if (method === "next" && "hasNext" in arg && arg.hasNext) {
1187+
concast.beforeNext(cb);
1188+
} else {
1189+
inFlightLinkObservables.remove(printedServerQuery, varJson);
1190+
}
11871191
});
11881192
}
11891193
} else {

‎src/core/__tests__/ApolloClient/general.test.ts

+133-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
Observable,
1010
Observer,
1111
} from "../../../utilities/observables/Observable";
12-
import { ApolloLink, FetchResult } from "../../../link/core";
12+
import {
13+
ApolloLink,
14+
FetchResult,
15+
type RequestHandler,
16+
} from "../../../link/core";
1317
import { InMemoryCache } from "../../../cache";
1418

1519
// mocks
@@ -31,7 +35,11 @@ import { wait } from "../../../testing/core";
3135
import { ApolloClient } from "../../../core";
3236
import { mockFetchQuery } from "../ObservableQuery";
3337
import { Concast, print } from "../../../utilities";
34-
import { ObservableStream, spyOnConsole } from "../../../testing/internal";
38+
import {
39+
mockDeferStream,
40+
ObservableStream,
41+
spyOnConsole,
42+
} from "../../../testing/internal";
3543

3644
describe("ApolloClient", () => {
3745
const getObservableStream = ({
@@ -6522,6 +6530,129 @@ describe("ApolloClient", () => {
65226530
)
65236531
).toBeUndefined();
65246532
});
6533+
6534+
it("deduplicates queries as long as a query still has deferred chunks", async () => {
6535+
const query = gql`
6536+
query LazyLoadLuke {
6537+
people(id: 1) {
6538+
id
6539+
name
6540+
friends {
6541+
id
6542+
... @defer {
6543+
name
6544+
}
6545+
}
6546+
}
6547+
}
6548+
`;
6549+
6550+
const outgoingRequestSpy = jest.fn(((operation, forward) =>
6551+
forward(operation)) satisfies RequestHandler);
6552+
const defer = mockDeferStream();
6553+
const client = new ApolloClient({
6554+
cache: new InMemoryCache({}),
6555+
link: new ApolloLink(outgoingRequestSpy).concat(defer.httpLink),
6556+
});
6557+
6558+
const query1 = new ObservableStream(
6559+
client.watchQuery({ query, fetchPolicy: "network-only" })
6560+
);
6561+
const query2 = new ObservableStream(
6562+
client.watchQuery({ query, fetchPolicy: "network-only" })
6563+
);
6564+
expect(outgoingRequestSpy).toHaveBeenCalledTimes(1);
6565+
6566+
const initialData = {
6567+
people: {
6568+
__typename: "Person",
6569+
id: 1,
6570+
name: "Luke",
6571+
friends: [
6572+
{
6573+
__typename: "Person",
6574+
id: 5,
6575+
} as { __typename: "Person"; id: number; name?: string },
6576+
{
6577+
__typename: "Person",
6578+
id: 8,
6579+
} as { __typename: "Person"; id: number; name?: string },
6580+
],
6581+
},
6582+
};
6583+
const initialResult = {
6584+
data: initialData,
6585+
loading: false,
6586+
networkStatus: 7,
6587+
};
6588+
6589+
defer.enqueueInitialChunk({
6590+
data: initialData,
6591+
hasNext: true,
6592+
});
6593+
6594+
await expect(query1).toEmitFetchResult(initialResult);
6595+
await expect(query2).toEmitFetchResult(initialResult);
6596+
6597+
const query3 = new ObservableStream(
6598+
client.watchQuery({ query, fetchPolicy: "network-only" })
6599+
);
6600+
await expect(query3).toEmitFetchResult(initialResult);
6601+
expect(outgoingRequestSpy).toHaveBeenCalledTimes(1);
6602+
6603+
const firstChunk = {
6604+
incremental: [
6605+
{
6606+
data: {
6607+
name: "Leia",
6608+
},
6609+
path: ["people", "friends", 0],
6610+
},
6611+
],
6612+
hasNext: true,
6613+
};
6614+
const resultAfterFirstChunk = structuredClone(initialResult);
6615+
resultAfterFirstChunk.data.people.friends[0].name = "Leia";
6616+
6617+
defer.enqueueSubsequentChunk(firstChunk);
6618+
6619+
await expect(query1).toEmitFetchResult(resultAfterFirstChunk);
6620+
await expect(query2).toEmitFetchResult(resultAfterFirstChunk);
6621+
await expect(query3).toEmitFetchResult(resultAfterFirstChunk);
6622+
6623+
const query4 = new ObservableStream(
6624+
client.watchQuery({ query, fetchPolicy: "network-only" })
6625+
);
6626+
expect(query4).toEmitFetchResult(resultAfterFirstChunk);
6627+
expect(outgoingRequestSpy).toHaveBeenCalledTimes(1);
6628+
6629+
const secondChunk = {
6630+
incremental: [
6631+
{
6632+
data: {
6633+
name: "Han Solo",
6634+
},
6635+
path: ["people", "friends", 1],
6636+
},
6637+
],
6638+
hasNext: false,
6639+
};
6640+
const resultAfterSecondChunk = structuredClone(resultAfterFirstChunk);
6641+
resultAfterSecondChunk.data.people.friends[1].name = "Han Solo";
6642+
6643+
defer.enqueueSubsequentChunk(secondChunk);
6644+
6645+
await expect(query1).toEmitFetchResult(resultAfterSecondChunk);
6646+
await expect(query2).toEmitFetchResult(resultAfterSecondChunk);
6647+
await expect(query3).toEmitFetchResult(resultAfterSecondChunk);
6648+
await expect(query4).toEmitFetchResult(resultAfterSecondChunk);
6649+
6650+
const query5 = new ObservableStream(
6651+
client.watchQuery({ query, fetchPolicy: "network-only" })
6652+
);
6653+
expect(query5).not.toEmitAnything();
6654+
expect(outgoingRequestSpy).toHaveBeenCalledTimes(2);
6655+
});
65256656
});
65266657

65276658
describe("missing cache field warnings", () => {

0 commit comments

Comments
 (0)
Please sign in to comment.