Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
7072d7e
Update to failing tests for expected partial stream change behavior
jerelmiller Jul 8, 2026
b8e4763
Add failing edge case tests with defer and partial data
jerelmiller Jul 9, 2026
940caa5
Return dataState in markQueryResult
jerelmiller Jul 9, 2026
682a183
Add additiional test for skip inside defer
jerelmiller Jul 9, 2026
84b61aa
Suppress expected console warning
jerelmiller Jul 9, 2026
67aa90e
Add another test with multi-defer
jerelmiller Jul 9, 2026
9ab5140
Add test for defer with named fragment
jerelmiller Jul 9, 2026
a47192f
Move new defer tests to client.watchQuery test suite
jerelmiller Jul 9, 2026
90a85e0
Adjust assertion for each test
jerelmiller Jul 9, 2026
d0863cd
Update test description to distinguish fetch policies
jerelmiller Jul 9, 2026
09196a7
Remove unneeded fetch policy
jerelmiller Jul 9, 2026
97202d2
More accurately report partial dataState for most cases
jerelmiller Jul 9, 2026
1574ca6
Add isTypenameField helper
jerelmiller Jul 9, 2026
cc14922
More tests to catch more edge cases
jerelmiller Jul 10, 2026
2702395
Bump test target to match our tsconfig
jerelmiller Jul 10, 2026
a3ab130
Some of the cases working
jerelmiller Jul 10, 2026
24362aa
Add more comments
jerelmiller Jul 10, 2026
f3b8bb6
Handle fragments when determining partial data
jerelmiller Jul 10, 2026
fdc9f6f
Fix more cases dealing with non-overlapped fields
jerelmiller Jul 10, 2026
64fd7e8
Better handle overlapping fields
jerelmiller Jul 10, 2026
3ead820
Collapse conditionals
jerelmiller Jul 10, 2026
e1e468b
Remove outdated comment
jerelmiller Jul 10, 2026
33c83d3
Handle exlusive __typename as child
jerelmiller Jul 10, 2026
6957054
Add isDeferredFragment helper
jerelmiller Jul 10, 2026
790cb6b
Fix incorrectly setup test
jerelmiller Jul 10, 2026
2299cc6
More accurate handling if determining deferred fragment
jerelmiller Jul 10, 2026
1aa4509
Add case that forces check for defer variables
jerelmiller Jul 10, 2026
b2e346f
Use isDeferredFragment to correctly evaluate vars
jerelmiller Jul 10, 2026
84b008f
Move collectNonDeferredFields into own utility file
jerelmiller Jul 10, 2026
5459246
Use context object
jerelmiller Jul 10, 2026
6642e16
Handle overlapping fields in defer boundaries
jerelmiller Jul 10, 2026
912819b
Inline check for empty data state
jerelmiller Jul 10, 2026
5ba2fca
Update stream tests to use complete dataState
jerelmiller Jul 10, 2026
d48fa15
Remove streaming marker in defer tests
jerelmiller Jul 10, 2026
a02571d
Make returnPartialData optional
jerelmiller Jul 10, 2026
1032876
Key off networkStatus instead of dataState with mid-stream errors
jerelmiller Jul 10, 2026
dbb6eb1
Ensure the partial flag is set correctly when dataState is complete
jerelmiller Jul 10, 2026
37f0d22
Suppress warnings in tests where expected to avoid noise
jerelmiller Jul 10, 2026
89c25db
Remove unused import
jerelmiller Jul 10, 2026
29ed4d9
Update api report
jerelmiller Jul 10, 2026
e697b9d
Add more tests for client.watchQuery defer from useSuspenseQuery tests
jerelmiller Jul 10, 2026
b7fddc6
Fix dataState in useSuspenseQuery/defer tests
jerelmiller Jul 10, 2026
1a381e0
Handle partial array items with stream
jerelmiller Jul 10, 2026
aeb6770
Remove check for array indices already handled
jerelmiller Jul 10, 2026
f47daca
WIP changeset
jerelmiller Jul 10, 2026
5b4e8f3
Ensure fetchMore sets the right dataState
jerelmiller Jul 10, 2026
8f90464
Update all incremental delivery tests to use updated dataState
jerelmiller Jul 10, 2026
f49257a
Add changeset
jerelmiller Jul 10, 2026
7d36475
Remove more unnecessary markAsStreaming
jerelmiller Jul 10, 2026
4326f78
Set visitedFragments to ensure the value is cached
jerelmiller Jul 10, 2026
3f52708
Use an object instead of Map for field collection
jerelmiller Jul 10, 2026
87b69d5
Flatten conditionals
jerelmiller Jul 10, 2026
8255ae6
Fix type errors
jerelmiller Jul 10, 2026
d099dd5
Fix no-cache streaming dataState for incremental chunks
jerelmiller Jul 10, 2026
0a64029
Update API report
jerelmiller Jul 10, 2026
4da892e
Update exports snapshot
jerelmiller Jul 10, 2026
39da11d
Add additional changeset
jerelmiller Jul 10, 2026
43cd803
Formatting
jerelmiller Jul 10, 2026
447cc7a
Try upgrading node version
jerelmiller Jul 10, 2026
79ea5c6
Revert "Try upgrading node version"
jerelmiller Jul 10, 2026
e55b8a7
Revert change to tsconfig
jerelmiller Jul 10, 2026
7f0858f
Add failing test
jerelmiller Jul 13, 2026
e7c380b
Copy over tests added from #13333
jerelmiller Jul 13, 2026
14498bd
Ensure we apply diff result when data is only missing at defer boundary
jerelmiller Jul 13, 2026
2b76f86
Clean up Prettier, Size-limit, and Api-Extractor
jerelmiller Jul 13, 2026
2966b16
Add changeset
jerelmiller Jul 13, 2026
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
27 changes: 27 additions & 0 deletions .api-reports/api-report-utilities_internal.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ErrorLike } from '@apollo/client';
import type { FieldNode } from 'graphql';
import type { FormattedExecutionResult } from 'graphql';
import type { FragmentDefinitionNode } from 'graphql';
import type { FragmentSpreadNode } from 'graphql';
import type { GraphQLFormattedError } from 'graphql';
import type { HKT } from '@apollo/client/utilities';
import type { Incremental } from '@apollo/client/incremental';
Expand Down Expand Up @@ -85,6 +86,21 @@ export type ClassicSignature = SignatureStyle extends "classic" ? unknown : neve
// @internal @deprecated
export function cloneDeep<T>(value: T): T;

// @public (undocumented)
interface CollectionContext {
// (undocumented)
exclude: SelectionNode;
// Warning: (ae-incompatible-release-tags) The symbol "fragmentMap" is marked as @public, but its signature references "FragmentMap" which is marked as @internal
//
// (undocumented)
fragmentMap: FragmentMap;
}

// Warning: (ae-forgotten-export) The symbol "CollectionContext" needs to be exported by the entry point index.d.ts
//
// @internal @deprecated
export function collectSiblingFields(selectionSet: SelectionSetNode, context: CollectionContext, visitedFragments?: Map<string, FieldMap>): FieldMap;

// @public
export function combineLatestBatched<T>(observables: Array<Observable<T> & {
dirty?: boolean;
Expand Down Expand Up @@ -221,6 +237,11 @@ export interface ExtensionsWithStreamInfo extends Record<string, unknown> {
};
}

// @public (undocumented)
export type FieldMap = {
[fieldName: string]: FieldMap | true;
};

// @public (undocumented)
export function filterMap<T, R>(fn: (value: T, context: undefined) => R | undefined): OperatorFunction<T, R>;

Expand Down Expand Up @@ -368,6 +389,9 @@ export type IsAny<T> = 0 extends 1 & T ? true : false;
// @internal @deprecated
export const isArray: (a: any) => a is any[] | readonly any[];

// @public (undocumented)
export function isDeferredFragment(fragmentSelection: InlineFragmentNode | FragmentSpreadNode, variables: OperationVariables): boolean;

// @internal @deprecated (undocumented)
export function isDocumentNode(value: unknown): value is DocumentNode;

Expand All @@ -390,6 +414,9 @@ export function isNonNullObject(obj: unknown): obj is Record<string | number, an
// @internal @deprecated (undocumented)
export function isPlainObject(obj: unknown): obj is Record<string | number, any>;

// @public (undocumented)
export function isTypenameField(field: FieldNode): boolean;

// @public
export type LazyType<T> = T & {
[K in "" as never]: LazyType<never>;
Expand Down
97 changes: 97 additions & 0 deletions .changeset/angry-baboons-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
"@apollo/client": minor
---

Fix the accuracy of `dataState` in complex incremental streaming scenarios, especially when combined with `returnPartialData: true`.

Prior to this change, all intermediate chunks used for both `@defer` and `@stream` directives returned a `dataState` of `streaming`, regardless of whether the actual data shape fit the definition of the `streaming` data state. The `streaming` data state represents an incomplete incremental response where the only holes in the data occur at `@defer` boundaries.

Let's use the following example of where the previous `dataState` fell down when combined with `returnPartialData`.

```gql
query GreetingQuery {
greeting {
message
... @defer {
recipient {
name
email
}
}
}
}
```

1. Scenario 1: partial data inside a `@defer` boundary written to the cache

Let's say the cache contained the following partial data:

```ts
{
greeting: {
__typename: "Greeting",
recipient: {
__typename: "Person",
name: "John Doe",
},
},
};
```

After the first chunk arrives from the server, the data looks like the following:

```ts
{
greeting: {
__typename: "Greeting",
message: "Hello, John",
recipient: {
__typename: "Person",
name: "John Doe",
},
},
};
```

This data is not `complete` because `recipient.email` is missing. This data is also not `streaming` because the data requirements in the `@defer` boundary are partially fulfilled due to the existence of `recipient`. This could lead to runtime crashes on `recipient.email` if you use the existence of `recipient` to detect whether data in the `@defer` boundary has streamed in or not. This change now accurately reports this as `partial` to ensure the field is marked as a partial field in `recipient`.

2. Scenario 2: partial data written to the cache that fulfills the data requirements of the `@defer` boundary

Let's say the cache contained the following partial data:

```ts
{
greeting: {
__typename: "Greeting",
recipient: {
__typename: "Person",
name: "John Doe",
email: "john@example.com",
},
},
};
```

After the first chunk arrives from the server, the data looks like the following:

```ts
{
greeting: {
__typename: "Greeting",
message: "Hello, John",
recipient: {
__typename: "Person",
name: "John Doe",
email: "john@example.com",
},
},
};
```

In this case, the combination of the first chunk and the partial data in the cache now fulfills the data requirements of the query. Even though the server is still streaming data (`NetworkStatus.streaming`), we can report this as `dataState: "complete"` since it is safe to access data on all fields.

This change also means `@stream` queries by definition fulfill the data requirements of the query after the first chunk arrives since `@stream` operates on lists and contains no data holes. `@stream` queries now accurately report `dataState` as `complete` or `partial`, depending on whether the list mixes partial data with streamed list items.

As a result of this change, some cases where you'd previously see `dataState` reported as `"streaming"` are now reported as `partial` or `complete`.

If you use `dataState` to determine whether an incremental request is still in-flight, please use `networkStatus` instead to check for `NetworkStatus.streaming`. `dataState` is type narrowing feature and not intended to report the network status.
34 changes: 34 additions & 0 deletions .changeset/fast-geckos-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@apollo/client": patch
---

Fix an issue where field `read` functions were not applied to intermediate results while streaming `@defer` responses. `cache.diff` ran the `read` functions, but the transformed values were only applied to the emitted result when the updated cache result was considered complete. Intermediate chunks whose only holes were at `@defer` boundaries now correctly return the result of field `read` functions.

```ts
new InMemoryCache({
typePolicies: {
Greeting: {
fields: {
message: {
read: (message) => message.toUpperCase(),
},
},
},
},
});

// query GreetingQuery {
// greeting {
// message
// ... @defer {
// recipient { name }
// }
// }
// }

// First chunk previously returned:
// { greeting: { message: "Hello world" } }
//
// Now correctly returns while still streaming:
// { greeting: { message: "HELLO WORLD" } }
```
7 changes: 7 additions & 0 deletions .changeset/serious-bugs-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@apollo/client": patch
---

Fix an issue with `@stream` queries when using `returnPartialData: true` where the streamed list was truncated after the first incremental chunk when the list contained partial cache data. The list is no longer truncated and partial list items are now retained as incremental chunks arrive. The `dataState` is now reported as `partial` until the server has streamed enough of the list so that each list item fully satisfies the query.

This change also updates `@stream` queries so that they reported with `dataState: "complete` instead of `"streaming"` since it is safe to access all fields in the response.
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)": 48863,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 43151,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 36730,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 30145
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 49697,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 43842,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 37288,
"import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 30735
}
3 changes: 3 additions & 0 deletions src/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ Array [
"canonicalStringify",
"checkDocument",
"cloneDeep",
"collectSiblingFields",
"combineLatestBatched",
"compact",
"createFragmentMap",
Expand Down Expand Up @@ -474,11 +475,13 @@ Array [
"hasDirectives",
"hasForcedResolvers",
"isArray",
"isDeferredFragment",
"isDocumentNode",
"isField",
"isNonEmptyArray",
"isNonNullObject",
"isPlainObject",
"isTypenameField",
"makeReference",
"makeUniqueId",
"mapObservableFragmentMemoized",
Expand Down
5 changes: 1 addition & 4 deletions src/core/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,10 +1009,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`,
// will be overwritten anyways, just here for types sake
loading: false,
data: diff.result,
dataState:
fetchMoreResult.dataState === "streaming" ?
"streaming"
: "complete",
dataState: diff.complete ? "complete" : "streaming",
},
});
}
Expand Down
Loading
Loading