diff --git a/package-lock.json b/package-lock.json index df6969202d..1a71701a5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@svgr/webpack": "6.2.1", "@types/chai": "4.3.1", "@types/mocha": "9.1.0", - "@types/node": "17.0.24", + "@types/node": "^17.0.24", "@typescript-eslint/eslint-plugin": "5.19.0", "@typescript-eslint/parser": "5.19.0", "c8": "7.11.0", diff --git a/package.json b/package.json index c779267657..c411f0d2b2 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@svgr/webpack": "6.2.1", "@types/chai": "4.3.1", "@types/mocha": "9.1.0", - "@types/node": "17.0.24", + "@types/node": "^17.0.24", "@typescript-eslint/eslint-plugin": "5.19.0", "@typescript-eslint/parser": "5.19.0", "c8": "7.11.0", diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 55c22ea9de..6b36ae5819 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -665,14 +665,25 @@ function completeValue( // If field type is List, complete each item in the list with the inner type if (isListType(returnType)) { - return completeListValue( - exeContext, - returnType, - fieldNodes, - info, - path, - result, - ); + if (isListType(returnType)) { + return exeContext.operation.name?.value === 'IntrospectionQuery' + ? completeListValue( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ) + : completeListValueChunked( + exeContext, + returnType, + fieldNodes, + info, + path, + result, + ); + } } // If field type is a leaf type, Scalar or Enum, serialize to a valid value, @@ -786,6 +797,106 @@ function completeListValue( return containsPromise ? Promise.all(completedResults) : completedResults; } +const MAX_EVENTLOOP_BLOCK_TIME_IN_MS = 50; + +const waitForNextEventCycle = () => + new Promise((resolve) => setImmediate(resolve)); + +/** + * Complete a list value by completing each item in the list with the + * inner type + */ +async function completeListValueChunked( + exeContext: ExecutionContext, + returnType: GraphQLList, + fieldNodes: ReadonlyArray, + info: GraphQLResolveInfo, + path: Path, + result: unknown, +): Promise> { + if (!isIterableObject(result)) { + throw new GraphQLError( + `Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`, + ); + } + + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another Promise. + const itemType = returnType.ofType; + let containsPromise = false; + const completedResults = []; + const startTime = new Date().getTime(); + + for (const [index, item] of Array.from(result).entries()) { + // No need to modify the info object containing the path, + // since from here on it is not ever accessed by resolver functions. + const itemPath = addPath(path, index, undefined); + + const is20thElement = index % 20 === 0 && index > 0; + const currentTime = new Date().getTime(); + const deltaFromStartTime = currentTime - startTime; + + if (is20thElement && deltaFromStartTime > MAX_EVENTLOOP_BLOCK_TIME_IN_MS) { + console.warn( + 'GraphQLExecutor::EventLoopBlock - Exceeded max execution time per cycle', + { + operationName: exeContext.operation.name?.value ?? 'Unknown', + resolverName: info.fieldName ?? 'Unknown', + }, + ); + await waitForNextEventCycle(); + } + + try { + let completedItem; + if (isPromise(item)) { + completedItem = item.then((resolved) => + completeValue( + exeContext, + itemType, + fieldNodes, + info, + itemPath, + resolved, + ), + ); + } else { + completedItem = completeValue( + exeContext, + itemType, + fieldNodes, + info, + itemPath, + item, + ); + } + + if (isPromise(completedItem)) { + containsPromise = true; + // Note: we don't rely on a `catch` method, but we do expect "thenable" + // to take a second callback for the error case. + completedResults.push( + completedItem.then(undefined, (rawError) => { + const error = locatedError( + rawError, + fieldNodes, + pathToArray(itemPath), + ); + return handleFieldError(error, itemType, exeContext); + }), + ); + continue; + } + completedResults.push(completedItem); + } catch (rawError) { + const error = locatedError(rawError, fieldNodes, pathToArray(itemPath)); + completedResults.push(handleFieldError(error, itemType, exeContext)); + } + } + + return containsPromise ? Promise.all(completedResults) : completedResults; +} + /** * Complete a Scalar or Enum by serializing to a valid value, returning * null if serialization is not possible.