Skip to content

Commit

Permalink
Fixes support for defer in mutations (#10368)
Browse files Browse the repository at this point in the history
  • Loading branch information
alessbell committed Dec 15, 2022
1 parent 52d5af2 commit 46b58e9
Show file tree
Hide file tree
Showing 8 changed files with 405 additions and 58 deletions.
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);
result.data = mergedData;

// Detect the first chunk of a deferred query and merge it with existing
Expand Down
108 changes: 73 additions & 35 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 @@ -295,13 +300,14 @@ export class QueryManager<TStore> {
next(storeResult) {
self.broadcastQueries();

// At the moment, a mutation can have only one result, so we can
// immediately resolve upon receiving the first result. In the future,
// mutations containing @defer or @stream directives might receive
// 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.
resolve(storeResult);
// Since mutations might receive multiple payloads from the
// ApolloLink chain (e.g. when used with @defer),
// we resolve with a SingleExecutionResult or after the final
// ExecutionPatchResult has arrived and we have assembled the
// multipart response into a single result.
if (!('hasNext' in storeResult) || storeResult.hasNext === false) {
resolve(storeResult);
}
},

error(err: Error) {
Expand Down Expand Up @@ -355,12 +361,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 +453,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 +476,31 @@ 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 && isFinalResult) {
cache.modify({
id: 'ROOT_MUTATION',
fields(value, { fieldName, DELETE }) {
Expand Down Expand Up @@ -1053,19 +1102,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

0 comments on commit 46b58e9

Please sign in to comment.