From 189b50243849c574721535b2a5077e61c1ebd20f Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sun, 7 Apr 2024 06:18:12 +0300 Subject: [PATCH 1/7] perf: allow skipping of field plan generation for the non-deferred case. --- src/execution/buildFieldPlan.ts | 79 +--- src/execution/collectFields.ts | 16 +- src/execution/execute.ts | 402 +++++++++++------- .../rules/SingleFieldSubscriptionsRule.ts | 16 +- 4 files changed, 296 insertions(+), 217 deletions(-) diff --git a/src/execution/buildFieldPlan.ts b/src/execution/buildFieldPlan.ts index 970b8d5c46..d29ae94cde 100644 --- a/src/execution/buildFieldPlan.ts +++ b/src/execution/buildFieldPlan.ts @@ -1,55 +1,39 @@ import { getBySet } from '../jsutils/getBySet.js'; import { isSameSet } from '../jsutils/isSameSet.js'; -import type { DeferUsage, FieldDetails } from './collectFields.js'; +import type { + DeferUsage, + FieldGroup, + GroupedFieldSet, +} from './collectFields.js'; export type DeferUsageSet = ReadonlySet; -export interface FieldGroup { - fields: ReadonlyArray; - deferUsages?: DeferUsageSet | undefined; +export interface FieldPlan { + groupedFieldSet: GroupedFieldSet; + newGroupedFieldSets: Map; } -export type GroupedFieldSet = Map; - export function buildFieldPlan( - fields: Map>, + originalGroupedFieldSet: GroupedFieldSet, parentDeferUsages: DeferUsageSet = new Set(), -): { - groupedFieldSet: GroupedFieldSet; - newGroupedFieldSets: Map; -} { - const groupedFieldSet = new Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - } - >(); +): FieldPlan { + const groupedFieldSet = new Map(); - const newGroupedFieldSets = new Map< - DeferUsageSet, - Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - } - > - >(); + const newGroupedFieldSets = new Map>(); const map = new Map< string, { deferUsageSet: DeferUsageSet; - fieldDetailsList: ReadonlyArray; + fieldGroup: FieldGroup; } >(); - for (const [responseKey, fieldDetailsList] of fields) { + for (const [responseKey, fieldGroup] of originalGroupedFieldSet) { const deferUsageSet = new Set(); let inOriginalResult = false; - for (const fieldDetails of fieldDetailsList) { + for (const fieldDetails of fieldGroup) { const deferUsage = fieldDetails.deferUsage; if (deferUsage === undefined) { inOriginalResult = true; @@ -69,44 +53,21 @@ export function buildFieldPlan( } }); } - map.set(responseKey, { deferUsageSet, fieldDetailsList }); + map.set(responseKey, { deferUsageSet, fieldGroup }); } - for (const [responseKey, { deferUsageSet, fieldDetailsList }] of map) { + for (const [responseKey, { deferUsageSet, fieldGroup }] of map) { if (isSameSet(deferUsageSet, parentDeferUsages)) { - let fieldGroup = groupedFieldSet.get(responseKey); - if (fieldGroup === undefined) { - fieldGroup = { - fields: [], - deferUsages: deferUsageSet, - }; - groupedFieldSet.set(responseKey, fieldGroup); - } - fieldGroup.fields.push(...fieldDetailsList); + groupedFieldSet.set(responseKey, fieldGroup); continue; } let newGroupedFieldSet = getBySet(newGroupedFieldSets, deferUsageSet); if (newGroupedFieldSet === undefined) { - newGroupedFieldSet = new Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - knownDeferUsages: DeferUsageSet; - } - >(); + newGroupedFieldSet = new Map(); newGroupedFieldSets.set(deferUsageSet, newGroupedFieldSet); } - let fieldGroup = newGroupedFieldSet.get(responseKey); - if (fieldGroup === undefined) { - fieldGroup = { - fields: [], - deferUsages: deferUsageSet, - }; - newGroupedFieldSet.set(responseKey, fieldGroup); - } - fieldGroup.fields.push(...fieldDetailsList); + newGroupedFieldSet.set(responseKey, fieldGroup); } return { diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 03ba5efde6..d411ff3f77 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -36,6 +36,10 @@ export interface FieldDetails { deferUsage: DeferUsage | undefined; } +export type FieldGroup = ReadonlyArray; + +export type GroupedFieldSet = ReadonlyMap; + interface CollectFieldsContext { schema: GraphQLSchema; fragments: ObjMap; @@ -61,7 +65,7 @@ export function collectFields( runtimeType: GraphQLObjectType, operation: OperationDefinitionNode, ): { - fields: Map>; + groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; } { const groupedFieldSet = new AccumulatorMap(); @@ -81,7 +85,7 @@ export function collectFields( groupedFieldSet, newDeferUsages, ); - return { fields: groupedFieldSet, newDeferUsages }; + return { groupedFieldSet, newDeferUsages }; } /** @@ -101,9 +105,9 @@ export function collectSubfields( variableValues: { [variable: string]: unknown }, operation: OperationDefinitionNode, returnType: GraphQLObjectType, - fieldDetails: ReadonlyArray, + fieldGroup: FieldGroup, ): { - fields: Map>; + groupedFieldSet: GroupedFieldSet; newDeferUsages: ReadonlyArray; } { const context: CollectFieldsContext = { @@ -117,7 +121,7 @@ export function collectSubfields( const subGroupedFieldSet = new AccumulatorMap(); const newDeferUsages: Array = []; - for (const fieldDetail of fieldDetails) { + for (const fieldDetail of fieldGroup) { const node = fieldDetail.node; if (node.selectionSet) { collectFieldsImpl( @@ -131,7 +135,7 @@ export function collectSubfields( } return { - fields: subGroupedFieldSet, + groupedFieldSet: subGroupedFieldSet, newDeferUsages, }; } diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 68037516e1..163d2fdfb0 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -47,14 +47,17 @@ import { GraphQLStreamDirective } from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; import { assertValidSchema } from '../type/validate.js'; +import type { DeferUsageSet, FieldPlan } from './buildFieldPlan.js'; +import { buildFieldPlan } from './buildFieldPlan.js'; import type { - DeferUsageSet, + DeferUsage, FieldGroup, GroupedFieldSet, -} from './buildFieldPlan.js'; -import { buildFieldPlan } from './buildFieldPlan.js'; -import type { DeferUsage, FieldDetails } from './collectFields.js'; -import { collectFields, collectSubfields } from './collectFields.js'; +} from './collectFields.js'; +import { + collectFields, + collectSubfields as _collectSubfields, +} from './collectFields.js'; import type { CancellableStreamRecord, DeferredGroupedFieldSetRecord, @@ -83,29 +86,24 @@ import { // so just disable it for entire file. /** - * A memoized function for building subfield plans with regard to the return - * type. Memoizing ensures the subfield plans are not repeatedly calculated, which + * A memoized collection of relevant subfields with regard to the return + * type. Memoizing ensures the subfields are not repeatedly calculated, which * saves overhead when resolving lists of values. */ -const buildSubFieldPlan = memoize3( +const collectSubfields = memoize3( ( exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldGroup: FieldGroup, - ) => { - const { fields: subFields, newDeferUsages } = collectSubfields( + ) => + _collectSubfields( exeContext.schema, exeContext.fragments, exeContext.variableValues, exeContext.operation, returnType, - fieldGroup.fields, - ); - return { - ...buildFieldPlan(subFields, fieldGroup.deferUsages), - newDeferUsages, - }; - }, + fieldGroup, + ), ); /** @@ -144,9 +142,15 @@ export interface ExecutionContext { fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; + errors: Array; cancellableStreams: Set; } +interface IncrementalContext { + errors: Array; + deferUsageSet?: DeferUsageSet | undefined; +} + export interface ExecutionArgs { schema: GraphQLSchema; document: DocumentNode; @@ -259,7 +263,6 @@ export function experimentalExecuteIncrementally( function executeOperation( exeContext: ExecutionContext, ): PromiseOrValue { - const errors: Array = []; try { const { operation, schema, fragments, variableValues, rootValue } = exeContext; @@ -271,59 +274,70 @@ function executeOperation( ); } - const { fields, newDeferUsages } = collectFields( - schema, - fragments, - variableValues, - rootType, - operation, - ); - const { groupedFieldSet, newGroupedFieldSets } = buildFieldPlan(fields); - - const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); + const { groupedFieldSet: nonPartitionedGroupedFieldSet, newDeferUsages } = + collectFields(schema, fragments, variableValues, rootType, operation); + let graphqlWrappedResult: PromiseOrValue< + GraphQLWrappedResult> + >; + if (newDeferUsages.length === 0) { + graphqlWrappedResult = executeRootGroupedFieldSet( + exeContext, + operation.operation, + rootType, + rootValue, + nonPartitionedGroupedFieldSet, + undefined, + ); + } else { + const { groupedFieldSet, newGroupedFieldSets } = buildFieldPlan( + nonPartitionedGroupedFieldSet, + ); - let graphqlWrappedResult = executeRootGroupedFieldSet( - exeContext, - operation.operation, - rootType, - rootValue, - groupedFieldSet, - errors, - newDeferMap, - ); + const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); - const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( - exeContext, - rootType, - rootValue, - undefined, - undefined, - newGroupedFieldSets, - newDeferMap, - ); + graphqlWrappedResult = executeRootGroupedFieldSet( + exeContext, + operation.operation, + rootType, + rootValue, + groupedFieldSet, + newDeferMap, + ); - graphqlWrappedResult = withNewDeferredGroupedFieldSets( - graphqlWrappedResult, - newDeferredGroupedFieldSetRecords, - ); + if (newGroupedFieldSets.size > 0) { + const newDeferredGroupedFieldSetRecords = + executeDeferredGroupedFieldSets( + exeContext, + rootType, + rootValue, + undefined, + undefined, + newGroupedFieldSets, + newDeferMap, + ); + + graphqlWrappedResult = withNewDeferredGroupedFieldSets( + graphqlWrappedResult, + newDeferredGroupedFieldSetRecords, + ); + } + } if (isPromise(graphqlWrappedResult)) { return graphqlWrappedResult.then( - (resolved) => - buildDataResponse(exeContext, resolved[0], errors, resolved[1]), + (resolved) => buildDataResponse(exeContext, resolved[0], resolved[1]), (error) => ({ data: null, - errors: withError(errors, error), + errors: withError(exeContext.errors, error), }), ); } return buildDataResponse( exeContext, graphqlWrappedResult[0], - errors, graphqlWrappedResult[1], ); } catch (error) { - return { data: null, errors: withError(errors, error) }; + return { data: null, errors: withError(exeContext.errors, error) }; } } @@ -352,9 +366,9 @@ function withError( function buildDataResponse( exeContext: ExecutionContext, data: ObjMap, - errors: ReadonlyArray, incrementalDataRecords: ReadonlyArray, ): ExecutionResult | ExperimentalIncrementalExecutionResults { + const errors = exeContext.errors; if (incrementalDataRecords.length === 0) { return errors.length > 0 ? { errors, data } : { data }; } @@ -468,6 +482,7 @@ export function buildExecutionContext( fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, + errors: [], cancellableStreams: new Set(), }; } @@ -478,6 +493,7 @@ function buildPerEventExecutionContext( ): ExecutionContext { return { ...exeContext, + errors: [], rootValue: payload, }; } @@ -488,7 +504,6 @@ function executeRootGroupedFieldSet( rootType: GraphQLObjectType, rootValue: unknown, groupedFieldSet: GroupedFieldSet, - errors: Array, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { switch (operation) { @@ -499,7 +514,7 @@ function executeRootGroupedFieldSet( rootValue, undefined, groupedFieldSet, - errors, + undefined, deferMap, ); case OperationTypeNode.MUTATION: @@ -509,7 +524,7 @@ function executeRootGroupedFieldSet( rootValue, undefined, groupedFieldSet, - errors, + undefined, deferMap, ); case OperationTypeNode.SUBSCRIPTION: @@ -521,7 +536,7 @@ function executeRootGroupedFieldSet( rootValue, undefined, groupedFieldSet, - errors, + undefined, deferMap, ); } @@ -537,7 +552,7 @@ function executeFieldsSerially( sourceValue: unknown, path: Path | undefined, groupedFieldSet: GroupedFieldSet, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { return promiseReduce( @@ -550,7 +565,7 @@ function executeFieldsSerially( sourceValue, fieldGroup, fieldPath, - errors, + incrementalContext, deferMap, ); if (result === undefined) { @@ -581,7 +596,7 @@ function executeFields( sourceValue: unknown, path: Path | undefined, groupedFieldSet: GroupedFieldSet, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const results = Object.create(null); @@ -600,7 +615,7 @@ function executeFields( sourceValue, fieldGroup, fieldPath, - errors, + incrementalContext, deferMap, ); @@ -644,7 +659,7 @@ function executeFields( } function toNodes(fieldGroup: FieldGroup): ReadonlyArray { - return fieldGroup.fields.map((fieldDetails) => fieldDetails.node); + return fieldGroup.map((fieldDetails) => fieldDetails.node); } /** @@ -659,10 +674,10 @@ function executeField( source: unknown, fieldGroup: FieldGroup, path: Path, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue> | undefined { - const fieldName = fieldGroup.fields[0].node.name.value; + const fieldName = fieldGroup[0].node.name.value; const fieldDef = exeContext.schema.getField(parentType, fieldName); if (!fieldDef) { return; @@ -686,7 +701,7 @@ function executeField( // TODO: find a way to memoize, in case this field is within a List type. const args = getArgumentValues( fieldDef, - fieldGroup.fields[0].node, + fieldGroup[0].node, exeContext.variableValues, ); @@ -705,7 +720,7 @@ function executeField( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -717,7 +732,7 @@ function executeField( info, path, result, - errors, + incrementalContext, deferMap, ); @@ -725,12 +740,14 @@ function executeField( // Note: we don't rely on a `catch` method, but we do expect "thenable" // to take a second callback for the error case. return completed.then(undefined, (rawError) => { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, returnType, fieldGroup, path, errors); return [null, []]; }); } return completed; } catch (rawError) { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, returnType, fieldGroup, path, errors); return [null, []]; } @@ -811,7 +828,7 @@ function completeValue( info: GraphQLResolveInfo, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue> { // If result is an Error, throw a located error. @@ -829,7 +846,7 @@ function completeValue( info, path, result, - errors, + incrementalContext, deferMap, ); if ((completed as GraphQLWrappedResult)[0] === null) { @@ -854,7 +871,7 @@ function completeValue( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -875,7 +892,7 @@ function completeValue( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -889,7 +906,7 @@ function completeValue( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -908,7 +925,7 @@ async function completePromisedValue( info: GraphQLResolveInfo, path: Path, result: Promise, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): Promise> { try { @@ -920,7 +937,7 @@ async function completePromisedValue( info, path, resolved, - errors, + incrementalContext, deferMap, ); @@ -929,6 +946,7 @@ async function completePromisedValue( } return completed; } catch (rawError) { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, returnType, fieldGroup, path, errors); return [null, []]; } @@ -963,7 +981,7 @@ function getStreamUsage( // safe to only check the first fieldNode for the stream directive const stream = getDirectiveValues( GraphQLStreamDirective, - fieldGroup.fields[0].node, + fieldGroup[0].node, exeContext.variableValues, ); @@ -990,12 +1008,10 @@ function getStreamUsage( '`@stream` directive not supported on subscription operations. Disable `@stream` by setting the `if` argument to `false`.', ); - const streamedFieldGroup: FieldGroup = { - fields: fieldGroup.fields.map((fieldDetails) => ({ - node: fieldDetails.node, - deferUsage: undefined, - })), - }; + const streamedFieldGroup: FieldGroup = fieldGroup.map((fieldDetails) => ({ + node: fieldDetails.node, + deferUsage: undefined, + })); const streamUsage = { initialCount: stream.initialCount, @@ -1020,7 +1036,7 @@ async function completeAsyncIteratorValue( info: GraphQLResolveInfo, path: Path, asyncIterator: AsyncIterator, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): Promise>> { let containsPromise = false; @@ -1092,7 +1108,7 @@ async function completeAsyncIteratorValue( info, itemPath, item, - errors, + incrementalContext, deferMap, ).then((resolved) => { graphqlWrappedResult[1].push(...resolved[1]); @@ -1111,7 +1127,7 @@ async function completeAsyncIteratorValue( fieldGroup, info, itemPath, - errors, + incrementalContext, deferMap, ) // TODO: add tests for stream backed by asyncIterator that completes to a promise @@ -1142,7 +1158,7 @@ function completeListValue( info: GraphQLResolveInfo, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const itemType = returnType.ofType; @@ -1157,7 +1173,7 @@ function completeListValue( info, path, asyncIterator, - errors, + incrementalContext, deferMap, ); } @@ -1217,7 +1233,7 @@ function completeListValue( info, itemPath, item, - errors, + incrementalContext, deferMap, ).then((resolved) => { graphqlWrappedResult[1].push(...resolved[1]); @@ -1235,7 +1251,7 @@ function completeListValue( fieldGroup, info, itemPath, - errors, + incrementalContext, deferMap, ) ) { @@ -1268,7 +1284,7 @@ function completeListItemValue( fieldGroup: FieldGroup, info: GraphQLResolveInfo, itemPath: Path, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): boolean { try { @@ -1279,7 +1295,7 @@ function completeListItemValue( info, itemPath, item, - errors, + incrementalContext, deferMap, ); @@ -1293,6 +1309,7 @@ function completeListItemValue( return resolved[0]; }, (rawError) => { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, itemType, fieldGroup, itemPath, errors); return null; }, @@ -1304,6 +1321,7 @@ function completeListItemValue( completedResults.push(completedItem[0]); parent[1].push(...completedItem[1]); } catch (rawError) { + const errors = (incrementalContext ?? exeContext).errors; handleFieldError(rawError, itemType, fieldGroup, itemPath, errors); completedResults.push(null); } @@ -1339,7 +1357,7 @@ function completeAbstractValue( info: GraphQLResolveInfo, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; @@ -1362,7 +1380,7 @@ function completeAbstractValue( info, path, result, - errors, + incrementalContext, deferMap, ), ); @@ -1382,7 +1400,7 @@ function completeAbstractValue( info, path, result, - errors, + incrementalContext, deferMap, ); } @@ -1452,7 +1470,7 @@ function completeObjectValue( info: GraphQLResolveInfo, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // If there is an isTypeOf predicate function, call it with the @@ -1472,7 +1490,7 @@ function completeObjectValue( fieldGroup, path, result, - errors, + incrementalContext, deferMap, ); }); @@ -1489,7 +1507,7 @@ function completeObjectValue( fieldGroup, path, result, - errors, + incrementalContext, deferMap, ); } @@ -1564,12 +1582,64 @@ function collectAndExecuteSubfields( fieldGroup: FieldGroup, path: Path, result: unknown, - errors: Array, + incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // Collect sub-fields to execute to complete this value. - const { groupedFieldSet, newGroupedFieldSets, newDeferUsages } = - buildSubFieldPlan(exeContext, returnType, fieldGroup); + const { groupedFieldSet: nonPartitionedGroupedFieldSet, newDeferUsages } = + collectSubfields(exeContext, returnType, fieldGroup); + + if (newDeferUsages.length === 0) { + if (deferMap === undefined) { + return executeFields( + exeContext, + returnType, + result, + path, + nonPartitionedGroupedFieldSet, + incrementalContext, + undefined, + ); + } + + const { groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( + nonPartitionedGroupedFieldSet, + incrementalContext?.deferUsageSet, + ); + + const subFields = executeFields( + exeContext, + returnType, + result, + path, + groupedFieldSet, + incrementalContext, + deferMap, + ); + + if (newGroupedFieldSets.size > 0) { + const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( + exeContext, + returnType, + result, + path, + incrementalContext?.deferUsageSet, + newGroupedFieldSets, + deferMap, + ); + + return withNewDeferredGroupedFieldSets( + subFields, + newDeferredGroupedFieldSetRecords, + ); + } + return subFields; + } + + const { groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( + nonPartitionedGroupedFieldSet, + incrementalContext?.deferUsageSet, + ); const newDeferMap = addNewDeferredFragments( newDeferUsages, @@ -1583,24 +1653,43 @@ function collectAndExecuteSubfields( result, path, groupedFieldSet, - errors, + incrementalContext, newDeferMap, ); - const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( - exeContext, - returnType, - result, - path, - fieldGroup.deferUsages, - newGroupedFieldSets, - newDeferMap, - ); + if (newGroupedFieldSets.size > 0) { + const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( + exeContext, + returnType, + result, + path, + incrementalContext?.deferUsageSet, + newGroupedFieldSets, + newDeferMap, + ); - return withNewDeferredGroupedFieldSets( - subFields, - newDeferredGroupedFieldSetRecords, - ); + return withNewDeferredGroupedFieldSets( + subFields, + newDeferredGroupedFieldSetRecords, + ); + } + return subFields; +} + +function buildSubFieldPlan( + originalGroupedFieldSet: GroupedFieldSet, + deferUsageSet: DeferUsageSet | undefined, +): FieldPlan { + let fieldPlan = ( + originalGroupedFieldSet as unknown as { _fieldPlan: FieldPlan } + )._fieldPlan; + if (fieldPlan !== undefined) { + return fieldPlan; + } + fieldPlan = buildFieldPlan(originalGroupedFieldSet, deferUsageSet); + (originalGroupedFieldSet as unknown as { _fieldPlan: FieldPlan })._fieldPlan = + fieldPlan; + return fieldPlan; } /** @@ -1816,7 +1905,7 @@ function executeSubscription( ); } - const { fields } = collectFields( + const { groupedFieldSet } = collectFields( schema, fragments, variableValues, @@ -1824,15 +1913,15 @@ function executeSubscription( operation, ); - const firstRootField = fields.entries().next().value as [ + const firstRootField = groupedFieldSet.entries().next().value as [ string, - ReadonlyArray, + FieldGroup, ]; - const [responseName, fieldDetailsList] = firstRootField; - const fieldName = fieldDetailsList[0].node.name.value; + const [responseName, fieldGroup] = firstRootField; + const fieldName = fieldGroup[0].node.name.value; const fieldDef = schema.getField(rootType, fieldName); - const fieldNodes = fieldDetailsList.map((fieldDetails) => fieldDetails.node); + const fieldNodes = fieldGroup.map((fieldDetails) => fieldDetails.node); if (!fieldDef) { throw new GraphQLError( `The subscription field "${fieldName}" is not defined.`, @@ -1921,7 +2010,10 @@ function executeDeferredGroupedFieldSets( sourceValue, path, groupedFieldSet, - [], + { + errors: [], + deferUsageSet, + }, deferMap, ); @@ -1961,7 +2053,7 @@ function executeDeferredGroupedFieldSet( sourceValue: unknown, path: Path | undefined, groupedFieldSet: GroupedFieldSet, - errors: Array, + incrementalContext: IncrementalContext, deferMap: ReadonlyMap, ): PromiseOrValue { let result; @@ -1972,14 +2064,14 @@ function executeDeferredGroupedFieldSet( sourceValue, path, groupedFieldSet, - errors, + incrementalContext, deferMap, ); } catch (error) { return { deferredFragmentRecords, path: pathToArray(path), - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }; } @@ -1987,7 +2079,7 @@ function executeDeferredGroupedFieldSet( return result.then( (resolved) => buildDeferredGroupedFieldSetResult( - errors, + incrementalContext.errors, deferredFragmentRecords, path, resolved, @@ -1995,13 +2087,13 @@ function executeDeferredGroupedFieldSet( (error) => ({ deferredFragmentRecords, path: pathToArray(path), - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }), ); } return buildDeferredGroupedFieldSetResult( - errors, + incrementalContext.errors, deferredFragmentRecords, path, result, @@ -2053,7 +2145,7 @@ function firstSyncStreamItems( initialPath, initialItem, exeContext, - [], + { errors: [] }, fieldGroup, info, itemType, @@ -2075,7 +2167,7 @@ function firstSyncStreamItems( currentPath, item, exeContext, - [], + { errors: [] }, fieldGroup, info, itemType, @@ -2191,7 +2283,7 @@ async function getNextAsyncStreamItemsResult( itemPath, iteration.value, exeContext, - [], + { errors: [] }, fieldGroup, info, itemType, @@ -2219,7 +2311,7 @@ function completeStreamItems( itemPath: Path, item: unknown, exeContext: ExecutionContext, - errors: Array, + incrementalContext: IncrementalContext, fieldGroup: FieldGroup, info: GraphQLResolveInfo, itemType: GraphQLOutputType, @@ -2232,14 +2324,18 @@ function completeStreamItems( info, itemPath, item, - errors, + incrementalContext, new Map(), ).then( (resolvedItem) => - buildStreamItemsResult(errors, streamRecord, resolvedItem), + buildStreamItemsResult( + incrementalContext.errors, + streamRecord, + resolvedItem, + ), (error) => ({ streamRecord, - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }), ); } @@ -2254,37 +2350,57 @@ function completeStreamItems( info, itemPath, item, - errors, + incrementalContext, new Map(), ); } catch (rawError) { - handleFieldError(rawError, itemType, fieldGroup, itemPath, errors); + handleFieldError( + rawError, + itemType, + fieldGroup, + itemPath, + incrementalContext.errors, + ); result = [null, []]; } } catch (error) { return { streamRecord, - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }; } if (isPromise(result)) { return result .then(undefined, (rawError) => { - handleFieldError(rawError, itemType, fieldGroup, itemPath, errors); + handleFieldError( + rawError, + itemType, + fieldGroup, + itemPath, + incrementalContext.errors, + ); return [null, []] as GraphQLWrappedResult; }) .then( (resolvedItem) => - buildStreamItemsResult(errors, streamRecord, resolvedItem), + buildStreamItemsResult( + incrementalContext.errors, + streamRecord, + resolvedItem, + ), (error) => ({ streamRecord, - errors: withError(errors, error), + errors: withError(incrementalContext.errors, error), }), ); } - return buildStreamItemsResult(errors, streamRecord, result); + return buildStreamItemsResult( + incrementalContext.errors, + streamRecord, + result, + ); } function buildStreamItemsResult( diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 06d9545fbc..700bc0bda7 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -10,15 +10,13 @@ import type { import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; -import type { FieldDetails } from '../../execution/collectFields.js'; +import type { FieldGroup } from '../../execution/collectFields.js'; import { collectFields } from '../../execution/collectFields.js'; import type { ValidationContext } from '../ValidationContext.js'; -function toNodes( - fieldDetailsList: ReadonlyArray, -): ReadonlyArray { - return fieldDetailsList.map((fieldDetails) => fieldDetails.node); +function toNodes(fieldGroup: FieldGroup): ReadonlyArray { + return fieldGroup.map((fieldDetails) => fieldDetails.node); } /** @@ -49,15 +47,15 @@ export function SingleFieldSubscriptionsRule( fragments[definition.name.value] = definition; } } - const { fields } = collectFields( + const { groupedFieldSet } = collectFields( schema, fragments, variableValues, subscriptionType, node, ); - if (fields.size > 1) { - const fieldGroups = [...fields.values()]; + if (groupedFieldSet.size > 1) { + const fieldGroups = [...groupedFieldSet.values()]; const extraFieldGroups = fieldGroups.slice(1); const extraFieldSelections = extraFieldGroups.flatMap( (fieldGroup) => toNodes(fieldGroup), @@ -71,7 +69,7 @@ export function SingleFieldSubscriptionsRule( ), ); } - for (const fieldGroup of fields.values()) { + for (const fieldGroup of groupedFieldSet.values()) { const fieldName = toNodes(fieldGroup)[0].name.value; if (fieldName.startsWith('__')) { context.reportError( From 4e5051a1383840e77b003b1d4c589d5a7ccbdb17 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 19 Apr 2024 07:52:11 +0300 Subject: [PATCH 2/7] refactor collectAndExecuteSubfields (#185) Co-authored-by: Rob Richard --- src/execution/execute.ts | 68 ++++++++++------------------------------ 1 file changed, 16 insertions(+), 52 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 163d2fdfb0..ea638b7a07 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1588,64 +1588,28 @@ function collectAndExecuteSubfields( // Collect sub-fields to execute to complete this value. const { groupedFieldSet: nonPartitionedGroupedFieldSet, newDeferUsages } = collectSubfields(exeContext, returnType, fieldGroup); + let groupedFieldSet = nonPartitionedGroupedFieldSet; + let newGroupedFieldSets; + let newDeferMap = deferMap; + let hasDefers = Boolean(deferMap) || Boolean(newDeferUsages.length); - if (newDeferUsages.length === 0) { - if (deferMap === undefined) { - return executeFields( - exeContext, - returnType, - result, - path, - nonPartitionedGroupedFieldSet, - incrementalContext, - undefined, - ); - } - - const { groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( + if (hasDefers) { + ({ groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( nonPartitionedGroupedFieldSet, incrementalContext?.deferUsageSet, - ); - - const subFields = executeFields( - exeContext, - returnType, - result, - path, - groupedFieldSet, - incrementalContext, - deferMap, - ); - + )); if (newGroupedFieldSets.size > 0) { - const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( - exeContext, - returnType, - result, - path, - incrementalContext?.deferUsageSet, - newGroupedFieldSets, - deferMap, - ); - - return withNewDeferredGroupedFieldSets( - subFields, - newDeferredGroupedFieldSetRecords, - ); + hasDefers = true; } - return subFields; } - const { groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( - nonPartitionedGroupedFieldSet, - incrementalContext?.deferUsageSet, - ); - - const newDeferMap = addNewDeferredFragments( - newDeferUsages, - new Map(deferMap), - path, - ); + if (hasDefers) { + newDeferMap = addNewDeferredFragments( + newDeferUsages, + new Map(deferMap), + path, + ); + } const subFields = executeFields( exeContext, @@ -1657,7 +1621,7 @@ function collectAndExecuteSubfields( newDeferMap, ); - if (newGroupedFieldSets.size > 0) { + if (newGroupedFieldSets && newDeferMap) { const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( exeContext, returnType, From d0ab281d2856ed56d433fd5f2e7ef2b054e12ff1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 19 Apr 2024 14:49:02 +0300 Subject: [PATCH 3/7] avoid Boolean (used rarely in this codebase) --- src/execution/execute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index ea638b7a07..f1aa78c59e 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1591,7 +1591,7 @@ function collectAndExecuteSubfields( let groupedFieldSet = nonPartitionedGroupedFieldSet; let newGroupedFieldSets; let newDeferMap = deferMap; - let hasDefers = Boolean(deferMap) || Boolean(newDeferUsages.length); + let hasDefers = deferMap !== undefined || newDeferUsages.length > 0; if (hasDefers) { ({ groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( From 22836f0d9a21b9150cdadb6c2d24ff5bfc768aaf Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 19 Apr 2024 14:51:36 +0300 Subject: [PATCH 4/7] no need to reassign --- src/execution/execute.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index f1aa78c59e..cec7e1f794 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1591,19 +1591,12 @@ function collectAndExecuteSubfields( let groupedFieldSet = nonPartitionedGroupedFieldSet; let newGroupedFieldSets; let newDeferMap = deferMap; - let hasDefers = deferMap !== undefined || newDeferUsages.length > 0; - if (hasDefers) { + if (deferMap !== undefined || newDeferUsages.length > 0) { ({ groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( nonPartitionedGroupedFieldSet, incrementalContext?.deferUsageSet, )); - if (newGroupedFieldSets.size > 0) { - hasDefers = true; - } - } - - if (hasDefers) { newDeferMap = addNewDeferredFragments( newDeferUsages, new Map(deferMap), From e6ac8cca6fe330d5ae82168a4f813c0b7ef4e737 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 19 Apr 2024 14:56:11 +0300 Subject: [PATCH 5/7] drop unnecessary variable --- src/execution/execute.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index cec7e1f794..ac0c7f0a7a 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -274,8 +274,15 @@ function executeOperation( ); } - const { groupedFieldSet: nonPartitionedGroupedFieldSet, newDeferUsages } = - collectFields(schema, fragments, variableValues, rootType, operation); + const collectedFields = collectFields( + schema, + fragments, + variableValues, + rootType, + operation, + ); + let groupedFieldSet = collectedFields.groupedFieldSet; + const newDeferUsages = collectedFields.newDeferUsages; let graphqlWrappedResult: PromiseOrValue< GraphQLWrappedResult> >; @@ -285,13 +292,13 @@ function executeOperation( operation.operation, rootType, rootValue, - nonPartitionedGroupedFieldSet, + groupedFieldSet, undefined, ); } else { - const { groupedFieldSet, newGroupedFieldSets } = buildFieldPlan( - nonPartitionedGroupedFieldSet, - ); + let newGroupedFieldSets; + ({ groupedFieldSet, newGroupedFieldSets } = + buildFieldPlan(groupedFieldSet)); const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); @@ -1586,15 +1593,18 @@ function collectAndExecuteSubfields( deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // Collect sub-fields to execute to complete this value. - const { groupedFieldSet: nonPartitionedGroupedFieldSet, newDeferUsages } = - collectSubfields(exeContext, returnType, fieldGroup); - let groupedFieldSet = nonPartitionedGroupedFieldSet; + const collectedSubfields = collectSubfields( + exeContext, + returnType, + fieldGroup, + ); + let groupedFieldSet = collectedSubfields.groupedFieldSet; + const newDeferUsages = collectedSubfields.newDeferUsages; let newGroupedFieldSets; let newDeferMap = deferMap; - if (deferMap !== undefined || newDeferUsages.length > 0) { ({ groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( - nonPartitionedGroupedFieldSet, + groupedFieldSet, incrementalContext?.deferUsageSet, )); newDeferMap = addNewDeferredFragments( From d38c4c1446117eb767b48581c56da940abe4b1e1 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 19 Apr 2024 15:09:19 +0300 Subject: [PATCH 6/7] put non-defer first --- src/execution/execute.ts | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index ac0c7f0a7a..f7b779be51 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1600,19 +1600,29 @@ function collectAndExecuteSubfields( ); let groupedFieldSet = collectedSubfields.groupedFieldSet; const newDeferUsages = collectedSubfields.newDeferUsages; - let newGroupedFieldSets; - let newDeferMap = deferMap; - if (deferMap !== undefined || newDeferUsages.length > 0) { - ({ groupedFieldSet, newGroupedFieldSets } = buildSubFieldPlan( - groupedFieldSet, - incrementalContext?.deferUsageSet, - )); - newDeferMap = addNewDeferredFragments( - newDeferUsages, - new Map(deferMap), + if (deferMap === undefined && newDeferUsages.length === 0) { + return executeFields( + exeContext, + returnType, + result, path, + groupedFieldSet, + incrementalContext, + undefined, ); } + const subFieldPlan = buildSubFieldPlan( + groupedFieldSet, + incrementalContext?.deferUsageSet, + ); + + groupedFieldSet = subFieldPlan.groupedFieldSet; + const newGroupedFieldSets = subFieldPlan.newGroupedFieldSets; + const newDeferMap = addNewDeferredFragments( + newDeferUsages, + new Map(deferMap), + path, + ); const subFields = executeFields( exeContext, @@ -1624,7 +1634,7 @@ function collectAndExecuteSubfields( newDeferMap, ); - if (newGroupedFieldSets && newDeferMap) { + if (newGroupedFieldSets.size > 0) { const newDeferredGroupedFieldSetRecords = executeDeferredGroupedFieldSets( exeContext, returnType, From 21e3a3cc8ec16177f1f4f8de397acf3de853f880 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 19 Apr 2024 15:10:27 +0300 Subject: [PATCH 7/7] avoid funny construction --- src/execution/execute.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index f7b779be51..23bfa50702 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -296,10 +296,9 @@ function executeOperation( undefined, ); } else { - let newGroupedFieldSets; - ({ groupedFieldSet, newGroupedFieldSets } = - buildFieldPlan(groupedFieldSet)); - + const fieldPLan = buildFieldPlan(groupedFieldSet); + groupedFieldSet = fieldPLan.groupedFieldSet; + const newGroupedFieldSets = fieldPLan.newGroupedFieldSets; const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); graphqlWrappedResult = executeRootGroupedFieldSet(