Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1a2c04d
Add array merge strategy to deep merger
jerelmiller Sep 12, 2025
92078c6
Default DeepMerger generic argument
jerelmiller Sep 12, 2025
308702a
WIP truncate merge arrays
jerelmiller Sep 12, 2025
67161f6
Check length before truncating
jerelmiller Sep 15, 2025
16bf7bc
Add another test for truncate merge
jerelmiller Sep 15, 2025
8442c84
Add past copies instead of copying twice
jerelmiller Sep 15, 2025
2f00a39
Inline new merger
jerelmiller Sep 15, 2025
05f9085
Move type to namespace
jerelmiller Sep 15, 2025
07567ef
Use dynamic array merge strategies
jerelmiller Sep 15, 2025
21a1928
Use property type for array merge
jerelmiller Sep 15, 2025
296dc9a
Truncate arrays in defer20220824 handler
jerelmiller Sep 15, 2025
156fa22
Update useBackgroundQuery tests to reflect updated nature on lists
jerelmiller Sep 15, 2025
58b5269
Combine array items if merging defer arrays
jerelmiller Sep 15, 2025
ede1476
Make graphql17Alpha9 more like Defer20220824 when determining arrayMe…
jerelmiller Sep 15, 2025
64d8536
Update useSuspenseQuery stream tests with updated behavior of list me…
jerelmiller Sep 15, 2025
5d7626a
Update spyOnConsole statement
jerelmiller Sep 15, 2025
32ad13a
Remove todo in useBackgroundQuery tests
jerelmiller Sep 15, 2025
ac33578
Update useQuery stream tests
jerelmiller Sep 15, 2025
3715a17
Remove unneeded arg
jerelmiller Sep 15, 2025
f9e176b
Add test to ensure custom merge function can be used to combine cache…
jerelmiller Sep 15, 2025
2aa31c7
Add changeset
jerelmiller Sep 15, 2025
6dddd33
Update size limits
jerelmiller Sep 15, 2025
1780c72
Add tests for refetches with defer arrays
jerelmiller Sep 15, 2025
f63c2b2
Fix changeset version type
jerelmiller Sep 15, 2025
dc712e7
Print object as array in warning
jerelmiller Sep 15, 2025
01cace0
Add changeset
jerelmiller Sep 15, 2025
31e405e
Update api report
jerelmiller Sep 15, 2025
c496039
Fix tag mix in api report
jerelmiller Sep 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .api-reports/api-report-utilities_internal.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,20 @@ export type DecoratedPromise<TValue> = PendingPromise<TValue> | FulfilledPromise
export function decoratePromise<TValue>(promise: Promise<TValue>): DecoratedPromise<TValue>;

// @internal @deprecated (undocumented)
export class DeepMerger<TContextArgs extends any[]> {
export namespace DeepMerger {
// (undocumented)
export type ArrayMergeStrategy = "truncate" | "combine";
// (undocumented)
export interface Options {
// (undocumented)
arrayMerge?: DeepMerger.ArrayMergeStrategy;
}
}

// @internal @deprecated (undocumented)
export class DeepMerger<TContextArgs extends any[] = any[]> {
// Warning: (ae-forgotten-export) The symbol "ReconcilerFunction" needs to be exported by the entry point index.d.ts
constructor(reconciler?: ReconcilerFunction<TContextArgs>);
constructor(reconciler?: ReconcilerFunction<TContextArgs>, options?: DeepMerger.Options);
// (undocumented)
isObject: typeof isNonNullObject;
// (undocumented)
Expand Down
5 changes: 5 additions & 0 deletions .changeset/cold-kiwis-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": minor
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm marking this as a minor change. While it does fix the array merging behavior, it's a big enough difference to warrant a minor.

---

Fix an issue where deferred payloads that reteurned arrays with fewer items than the original cached array would retain items from the cached array. This change includes `@stream` arrays where stream arrays replace the cached arrays.
5 changes: 5 additions & 0 deletions .changeset/neat-lemons-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Improve the cache data loss warning message when `existing` or `incoming` is an array.
8 changes: 4 additions & 4 deletions .size-limits.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44194,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39041,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33526,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27519
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44386,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39203,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33554,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27582
}
4 changes: 2 additions & 2 deletions src/cache/inmemory/writeToStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -894,8 +894,8 @@ For more information about these options, please refer to the documentation:
" have an ID or a custom merge function, or "
: "",
typeDotName,
{ ...existing },
{ ...incoming }
Array.isArray(existing) ? [...existing] : { ...existing },
Array.isArray(incoming) ? [...incoming] : { ...incoming }
);
}

Expand Down
184 changes: 184 additions & 0 deletions src/core/__tests__/client.watchQuery/defer20220824.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { InMemoryCache } from "@apollo/client/cache";
import { Defer20220824Handler } from "@apollo/client/incremental";
import { ApolloLink } from "@apollo/client/link";
import {
markAsStreaming,
mockDefer20220824,
ObservableStream,
} from "@apollo/client/testing/internal";
Expand Down Expand Up @@ -163,3 +164,186 @@ test("deduplicates queries as long as a query still has deferred chunks", async
// expect(query5).not.toEmitAnything();
expect(outgoingRequestSpy).toHaveBeenCalledTimes(2);
});

it.each([["cache-first"], ["no-cache"]] as const)(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were added from #11374 (with some tweaks)

"correctly merges deleted rows when receiving a deferred payload",
async (fetchPolicy) => {
const query = gql`
query Characters {
characters {
id
uppercase
... @defer {
lowercase
}
}
}
`;

const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } =
mockDefer20220824();
const client = new ApolloClient({
cache: new InMemoryCache(),
link: httpLink,
incrementalHandler: new Defer20220824Handler(),
});

const observable = client.watchQuery({ query, fetchPolicy });
const stream = new ObservableStream(observable);

await expect(stream).toEmitTypedValue({
data: undefined,
dataState: "empty",
loading: true,
networkStatus: NetworkStatus.loading,
partial: true,
});

enqueueInitialChunk({
data: {
characters: [
{ __typename: "Character", id: 1, uppercase: "A" },
{ __typename: "Character", id: 2, uppercase: "B" },
{ __typename: "Character", id: 3, uppercase: "C" },
],
},
hasNext: true,
});

await expect(stream).toEmitTypedValue({
data: markAsStreaming({
characters: [
{ __typename: "Character", id: 1, uppercase: "A" },
{ __typename: "Character", id: 2, uppercase: "B" },
{ __typename: "Character", id: 3, uppercase: "C" },
],
}),
dataState: "streaming",
loading: true,
networkStatus: NetworkStatus.streaming,
partial: true,
});

enqueueSubsequentChunk({
incremental: [{ data: { lowercase: "a" }, path: ["characters", 0] }],
hasNext: true,
});

await expect(stream).toEmitTypedValue({
data: markAsStreaming({
characters: [
{ __typename: "Character", id: 1, uppercase: "A", lowercase: "a" },
{ __typename: "Character", id: 2, uppercase: "B" },
{ __typename: "Character", id: 3, uppercase: "C" },
],
}),
dataState: "streaming",
loading: true,
networkStatus: NetworkStatus.streaming,
partial: true,
});

enqueueSubsequentChunk({
incremental: [
{ data: { lowercase: "b" }, path: ["characters", 1] },
{ data: { lowercase: "c" }, path: ["characters", 2] },
],
hasNext: false,
});

await expect(stream).toEmitTypedValue({
data: {
characters: [
{ __typename: "Character", id: 1, uppercase: "A", lowercase: "a" },
{ __typename: "Character", id: 2, uppercase: "B", lowercase: "b" },
{ __typename: "Character", id: 3, uppercase: "C", lowercase: "c" },
],
},
dataState: "complete",
loading: false,
networkStatus: NetworkStatus.ready,
partial: false,
});

void observable.refetch();

await expect(stream).toEmitTypedValue({
data: {
characters: [
{ __typename: "Character", id: 1, uppercase: "A", lowercase: "a" },
{ __typename: "Character", id: 2, uppercase: "B", lowercase: "b" },
{ __typename: "Character", id: 3, uppercase: "C", lowercase: "c" },
],
},
dataState: "complete",
loading: true,
networkStatus: NetworkStatus.refetch,
partial: false,
});

// on refetch, the list is shorter
enqueueInitialChunk({
data: {
characters: [
{ __typename: "Character", id: 1, uppercase: "A" },
{ __typename: "Character", id: 2, uppercase: "B" },
],
},
hasNext: true,
});

await expect(stream).toEmitTypedValue({
data: markAsStreaming({
characters:
// no-cache fetch policy doesn't merge with existing cache data, so
// the lowercase field is not added to each item
fetchPolicy === "no-cache" ?
[
{ __typename: "Character", id: 1, uppercase: "A" },
{ __typename: "Character", id: 2, uppercase: "B" },
]
: [
{
__typename: "Character",
id: 1,
uppercase: "A",
lowercase: "a",
},
{
__typename: "Character",
id: 2,
uppercase: "B",
lowercase: "b",
},
],
}),
dataState: "streaming",
loading: true,
networkStatus: NetworkStatus.streaming,
partial: true,
});

enqueueSubsequentChunk({
incremental: [
{ data: { lowercase: "a" }, path: ["characters", 0] },
{ data: { lowercase: "b" }, path: ["characters", 1] },
],
hasNext: false,
});

await expect(stream).toEmitTypedValue({
data: {
characters: [
{ __typename: "Character", id: 1, uppercase: "A", lowercase: "a" },
{ __typename: "Character", id: 2, uppercase: "B", lowercase: "b" },
],
},
dataState: "complete",
loading: false,
networkStatus: NetworkStatus.ready,
partial: false,
});

await expect(stream).not.toEmitAnything();
}
);
Loading