Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes support for defer in mutations #10368

Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/tough-ghosts-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@apollo/client": patch
---

Fix: unblocks support for defer in mutations

If the `@defer` directive is present in the document passed to `mutate`, the Promise will resolve with the final merged data after the last multipart chunk has arrived in the response.
2 changes: 1 addition & 1 deletion config/bundlesize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { join } from "path";
import { gzipSync } from "zlib";
import bytes from "bytes";

const gzipBundleByteLengthLimit = bytes("31.87KB");
const gzipBundleByteLengthLimit = bytes("32KB");
const minFile = join("dist", "apollo-client.min.cjs");
const minPath = join(__dirname, "..", minFile);
const gzipByteLen = gzipSync(readFileSync(minPath)).byteLength;
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/exports.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ Array [
"getFragmentDefinitions",
"getFragmentFromSelection",
"getFragmentQueryDocument",
"getGraphQLErrorsFromResult",
"getInclusionDirectives",
"getMainDefinition",
"getOperationDefinition",
Expand Down
17 changes: 2 additions & 15 deletions src/core/QueryInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { equal } from "@wry/equality";

import { Cache, ApolloCache } from '../cache';
import { DeepMerger } from "../utilities"
import { mergeIncrementalData } from '../utilities/common/incrementalResult';
import { WatchQueryOptions, ErrorPolicy } from './watchQueryOptions';
import { ObservableQuery, reobserveCacheFirst } from './ObservableQuery';
import { QueryListener } from './types';
Expand Down Expand Up @@ -373,21 +374,7 @@ export class QueryInfo {
this.reset();

if ('incremental' in result && isNonEmptyArray(result.incremental)) {
let mergedData = this.getDiff().result;

result.incremental.forEach(({ data, path, errors }) => {
for (let i = path.length - 1; i >= 0; --i) {
const key = path[i];
const isNumericKey = !isNaN(+key);
const parent: Record<string | number, any> = isNumericKey ? [] : {};
parent[key] = data;
data = parent as typeof data;
}
if (errors) {
graphQLErrors.push(...errors);
}
mergedData = merger.merge(mergedData, data);
});
const mergedData = mergeIncrementalData(this.getDiff().result, result);
alessbell marked this conversation as resolved.
Show resolved Hide resolved
result.data = mergedData;

// Detect the first chunk of a deferred query and merge it with existing
Expand Down
104 changes: 75 additions & 29 deletions src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ type OperationTypeNode = any;
import { equal } from '@wry/equality';

import { ApolloLink, execute, FetchResult } from '../link/core';
import { isExecutionPatchIncrementalResult } from '../utilities/common/incrementalResult';
import {
isExecutionPatchIncrementalResult,
isExecutionPatchResult,
} from '../utilities/common/incrementalResult';
import { Cache, ApolloCache, canonicalStringify } from '../cache';

import {
Expand All @@ -15,6 +18,7 @@ import {
getOperationName,
hasClientExports,
graphQLResultHasError,
getGraphQLErrorsFromResult,
removeConnectionDirectiveFromDocument,
canUseWeakMap,
ObservableSubscription,
Expand All @@ -27,6 +31,7 @@ import {
isDocumentNode,
isNonNullObject,
} from '../utilities';
import { mergeIncrementalData } from '../utilities/common/incrementalResult';
import { ApolloError, isApolloError } from '../errors';
import {
QueryOptions,
Expand Down Expand Up @@ -248,7 +253,7 @@ export class QueryManager<TStore> {
(result: FetchResult<TData>) => {
if (graphQLResultHasError(result) && errorPolicy === 'none') {
throw new ApolloError({
graphQLErrors: result.errors,
graphQLErrors: getGraphQLErrorsFromResult(result),
});
}

Expand Down Expand Up @@ -301,7 +306,9 @@ export class QueryManager<TStore> {
// multiple FetchResult payloads from the ApolloLink chain, so we will
// probably need to collect those results in this next method and call
// resolve only later, in an observer.complete function.
alessbell marked this conversation as resolved.
Show resolved Hide resolved
resolve(storeResult);
if (!('hasNext' in storeResult) || storeResult.hasNext === false) {
resolve(storeResult);
}
},

error(err: Error) {
Expand Down Expand Up @@ -355,12 +362,38 @@ export class QueryManager<TStore> {
const skipCache = mutation.fetchPolicy === "no-cache";

if (!skipCache && shouldWriteResult(result, mutation.errorPolicy)) {
cacheWrites.push({
result: result.data,
dataId: 'ROOT_MUTATION',
query: mutation.document,
variables: mutation.variables,
});
if (!isExecutionPatchIncrementalResult(result)) {
cacheWrites.push({
result: result.data,
dataId: 'ROOT_MUTATION',
query: mutation.document,
variables: mutation.variables,
});
}
if (isExecutionPatchIncrementalResult(result) && isNonEmptyArray(result.incremental)) {
const diff = cache.diff<TData>({
id: "ROOT_MUTATION",
// The cache complains if passed a mutation where it expects a
// query, so we transform mutations and subscriptions to queries
// (only once, thanks to this.transformCache).
query: this.transform(mutation.document).asQuery,
variables: mutation.variables,
optimistic: false,
returnPartialData: true,
});
const mergedData = mergeIncrementalData(diff.result, result);
if (typeof mergedData !== 'undefined') {
// cast the ExecutionPatchResult to FetchResult here since
// ExecutionPatchResult never has `data` when returned from the server
(result as FetchResult).data = mergedData;
cacheWrites.push({
result: mergedData,
dataId: 'ROOT_MUTATION',
query: mutation.document,
variables: mutation.variables,
})
}
}

const { updateQueries } = mutation;
if (updateQueries) {
Expand Down Expand Up @@ -421,6 +454,12 @@ export class QueryManager<TStore> {
// apply those writes to the store by running this reducer again with
// a write action.
const { update } = mutation;
// Determine whether result is a SingleExecutionResult,
// or the final ExecutionPatchResult.
const isFinalResult =
!isExecutionPatchResult(result) ||
(isExecutionPatchIncrementalResult(result) && !result.hasNext);

if (update) {
if (!skipCache) {
// Re-read the ROOT_MUTATION data we just wrote into the cache
Expand All @@ -438,20 +477,38 @@ export class QueryManager<TStore> {
returnPartialData: true,
});

if (diff.complete && !(isExecutionPatchIncrementalResult(result))) {
result = { ...result, data: diff.result };
if (diff.complete) {
result = { ...result as FetchResult, data: diff.result };
if ('incremental' in result) {
delete result.incremental;
}
if ('hasNext' in result) {
delete result.hasNext;
}
}
}

update(cache, result, {
context: mutation.context,
variables: mutation.variables,
});
// If we've received the whole response,
// either a SingleExecutionResult or the final ExecutionPatchResult,
// call the update function.
if (isFinalResult) {
update(cache, result, {
context: mutation.context,
variables: mutation.variables,
});
}
}

// TODO Do this with cache.evict({ id: 'ROOT_MUTATION' }) but make it
// shallow to allow rolling back optimistic evictions.
if (!skipCache && !mutation.keepRootFields) {
if (
!skipCache &&
!mutation.keepRootFields &&
(
!isExecutionPatchResult(result) ||
(isExecutionPatchIncrementalResult(result) && !result.hasNext)
alessbell marked this conversation as resolved.
Show resolved Hide resolved
alessbell marked this conversation as resolved.
Show resolved Hide resolved
)
) {
cache.modify({
id: 'ROOT_MUTATION',
fields(value, { fieldName, DELETE }) {
Expand Down Expand Up @@ -1053,19 +1110,8 @@ export class QueryManager<TStore> {
),

result => {
const graphQLErrors = isNonEmptyArray(result.errors)
? result.errors.slice(0)
: [];

if ('incremental' in result && isNonEmptyArray(result.incremental)) {
result.incremental.forEach(incrementalResult => {
if (incrementalResult.errors) {
graphQLErrors.push(...incrementalResult.errors);
}
});
}

const hasErrors = isNonEmptyArray(graphQLErrors);
const graphQLErrors = getGraphQLErrorsFromResult(result);
const hasErrors = graphQLErrors.length > 0;

// If we interrupted this request by calling getResultsFromLink again
// with the same QueryInfo object, we ignore the old results.
Expand Down
Loading