Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0d96e4b
Add array merge strategy to deep merger
jerelmiller Sep 12, 2025
ee7da21
Default DeepMerger generic argument
jerelmiller Sep 12, 2025
6da0221
WIP truncate merge arrays
jerelmiller Sep 12, 2025
f9b8772
Check length before truncating
jerelmiller Sep 15, 2025
108acd2
Add another test for truncate merge
jerelmiller Sep 15, 2025
bd60f05
Add past copies instead of copying twice
jerelmiller Sep 15, 2025
b156bef
Inline new merger
jerelmiller Sep 15, 2025
138a110
Move type to namespace
jerelmiller Sep 15, 2025
2c8180f
Use dynamic array merge strategies
jerelmiller Sep 15, 2025
b62fe24
Use property type for array merge
jerelmiller Sep 15, 2025
6a39f32
Truncate arrays in defer20220824 handler
jerelmiller Sep 15, 2025
43791bc
Update useBackgroundQuery tests to reflect updated nature on lists
jerelmiller Sep 15, 2025
2cb8e98
Combine array items if merging defer arrays
jerelmiller Sep 15, 2025
4bc8dd9
Make graphql17Alpha9 more like Defer20220824 when determining arrayMe…
jerelmiller Sep 15, 2025
121a646
Update useSuspenseQuery stream tests with updated behavior of list me…
jerelmiller Sep 15, 2025
e5cb816
Update spyOnConsole statement
jerelmiller Sep 15, 2025
0857af7
Remove todo in useBackgroundQuery tests
jerelmiller Sep 15, 2025
cc80296
Update useQuery stream tests
jerelmiller Sep 15, 2025
37979b9
Remove unneeded arg
jerelmiller Sep 15, 2025
34a29d2
Add test to ensure custom merge function can be used to combine cache…
jerelmiller Sep 15, 2025
c826bc3
Add changeset
jerelmiller Sep 15, 2025
a506a89
Update size limits
jerelmiller Sep 15, 2025
ff289b7
Add tests for refetches with defer arrays
jerelmiller Sep 15, 2025
c558fe5
Fix changeset version type
jerelmiller Sep 15, 2025
04dbe02
Print object as array in warning
jerelmiller Sep 15, 2025
1becedf
Add changeset
jerelmiller Sep 15, 2025
0c156f1
Update api report
jerelmiller Sep 15, 2025
dd5eccb
Fix tag mix in api report
jerelmiller Sep 15, 2025
059426e
Fix typo
jerelmiller Nov 14, 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 returned 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
Loading