Skip to content

Commit d48f4ef

Browse files
implement correct merging of incremental respones (@defer/@stream) (#3580)
* implement correct merging of incremental respones (@defer/@stream) * fix lint error * fix error for real
1 parent 9d42a25 commit d48f4ef

File tree

2 files changed

+71
-36
lines changed

2 files changed

+71
-36
lines changed

.changeset/clean-pets-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphiql/react': minor
3+
---
4+
5+
Implement correct merging of incremental responses (@defer/@stream)

packages/graphiql-react/src/execution.tsx

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import {
22
Fetcher,
3-
FetcherResultPayload,
43
formatError,
54
formatResult,
65
isAsyncIterable,
76
isObservable,
87
Unsubscribable,
98
} from '@graphiql/toolkit';
10-
import { ExecutionResult, FragmentDefinitionNode, print } from 'graphql';
9+
import {
10+
ExecutionResult,
11+
FragmentDefinitionNode,
12+
GraphQLError,
13+
print,
14+
} from 'graphql';
1115
import { getFragmentDependenciesForAST } from 'graphql-language-service';
1216
import { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
1317
import setValue from 'set-value';
@@ -183,7 +187,7 @@ export function ExecutionContextProvider({
183187
});
184188

185189
try {
186-
let fullResponse: FetcherResultPayload = { data: {} };
190+
const fullResponse: ExecutionResult = {};
187191
const handleResponse = (result: ExecutionResult) => {
188192
// A different query was dispatched in the meantime, so don't
189193
// show the results of this one.
@@ -202,40 +206,8 @@ export function ExecutionContextProvider({
202206
}
203207

204208
if (maybeMultipart) {
205-
const payload: FetcherResultPayload = {
206-
data: fullResponse.data,
207-
};
208-
const maybeErrors = [
209-
...(fullResponse?.errors || []),
210-
...maybeMultipart.flatMap(i => i.errors).filter(Boolean),
211-
];
212-
213-
if (maybeErrors.length) {
214-
payload.errors = maybeErrors;
215-
}
216-
217209
for (const part of maybeMultipart) {
218-
// We pull out errors here, so we dont include it later
219-
const { path, data, errors, ...rest } = part;
220-
if (path) {
221-
if (!data) {
222-
throw new Error(
223-
`Expected part to contain a data property, but got ${part}`,
224-
);
225-
}
226-
227-
setValue(payload.data, path, data, { merge: true });
228-
} else if (data) {
229-
// If there is no path, we don't know what to do with the payload,
230-
// so we just set it.
231-
payload.data = data;
232-
}
233-
234-
// Ensures we also bring extensions and alike along for the ride
235-
fullResponse = {
236-
...payload,
237-
...rest,
238-
};
210+
mergeIncrementalResult(fullResponse, part);
239211
}
240212

241213
setIsFetching(false);
@@ -361,3 +333,61 @@ function tryParseJsonObject({
361333
}
362334
return parsed;
363335
}
336+
337+
type IncrementalResult = {
338+
data?: Record<string, unknown> | null;
339+
errors?: ReadonlyArray<GraphQLError>;
340+
extensions?: Record<string, unknown>;
341+
hasNext?: boolean;
342+
path?: ReadonlyArray<string | number>;
343+
incremental?: ReadonlyArray<IncrementalResult>;
344+
label?: string;
345+
items?: ReadonlyArray<Record<string, unknown>> | null;
346+
};
347+
348+
/**
349+
* @param executionResult The complete execution result object which will be
350+
* mutated by merging the contents of the incremental result.
351+
* @param incrementalResult The incremental result that will be merged into the
352+
* complete execution result.
353+
*/
354+
function mergeIncrementalResult(
355+
executionResult: ExecutionResult,
356+
incrementalResult: IncrementalResult,
357+
): void {
358+
const path = ['data', ...(incrementalResult.path ?? [])];
359+
360+
if (incrementalResult.items) {
361+
for (const item of incrementalResult.items) {
362+
setValue(executionResult, path.join('.'), item);
363+
// Increment the last path segment (the array index) to merge the next item at the next index
364+
// eslint-disable-next-line unicorn/prefer-at -- cannot mutate the array using Array.at()
365+
(path[path.length - 1] as number)++;
366+
}
367+
}
368+
369+
if (incrementalResult.data) {
370+
setValue(executionResult, path.join('.'), incrementalResult.data, {
371+
merge: true,
372+
});
373+
}
374+
375+
if (incrementalResult.errors) {
376+
executionResult.errors ||= [];
377+
(executionResult.errors as GraphQLError[]).push(
378+
...incrementalResult.errors,
379+
);
380+
}
381+
382+
if (incrementalResult.extensions) {
383+
setValue(executionResult, 'extensions', incrementalResult.extensions, {
384+
merge: true,
385+
});
386+
}
387+
388+
if (incrementalResult.incremental) {
389+
for (const incrementalSubResult of incrementalResult.incremental) {
390+
mergeIncrementalResult(executionResult, incrementalSubResult);
391+
}
392+
}
393+
}

0 commit comments

Comments
 (0)