From 2aedf25e157d1d1c8fdfeaa4c0d2f3d9d3457dba Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Sat, 16 Dec 2023 21:15:54 +0200 Subject: [PATCH] simplify CollectFields for `@defer` and `@stream` (#3994) minimizes the changes to `CollectFields` required for incremental delivery (inspired by #3982) -- but retains a single memoized incremental field plan per list item. --- src/execution/IncrementalPublisher.ts | 2 +- src/execution/buildFieldPlan.ts | 165 ++++++++++ src/execution/collectFields.ts | 307 ++++-------------- src/execution/execute.ts | 105 +++--- .../rules/SingleFieldSubscriptionsRule.ts | 16 +- 5 files changed, 286 insertions(+), 309 deletions(-) create mode 100644 src/execution/buildFieldPlan.ts diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index 760043c78b..9cda62dea8 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -8,7 +8,7 @@ import type { GraphQLFormattedError, } from '../error/GraphQLError.js'; -import type { GroupedFieldSet } from './collectFields.js'; +import type { GroupedFieldSet } from './buildFieldPlan.js'; interface IncrementalUpdate> { pending: ReadonlyArray; diff --git a/src/execution/buildFieldPlan.ts b/src/execution/buildFieldPlan.ts new file mode 100644 index 0000000000..7f9f6bc98e --- /dev/null +++ b/src/execution/buildFieldPlan.ts @@ -0,0 +1,165 @@ +import { getBySet } from '../jsutils/getBySet.js'; +import { isSameSet } from '../jsutils/isSameSet.js'; + +import type { DeferUsage, FieldDetails } from './collectFields.js'; + +export type DeferUsageSet = ReadonlySet; + +export interface FieldGroup { + fields: ReadonlyArray; + deferUsages?: DeferUsageSet | undefined; + knownDeferUsages?: DeferUsageSet | undefined; +} + +export type GroupedFieldSet = Map; + +export interface NewGroupedFieldSetDetails { + groupedFieldSet: GroupedFieldSet; + shouldInitiateDefer: boolean; +} + +export function buildFieldPlan( + fields: Map>, + parentDeferUsages: DeferUsageSet = new Set(), + knownDeferUsages: DeferUsageSet = new Set(), +): { + groupedFieldSet: GroupedFieldSet; + newGroupedFieldSetDetailsMap: Map; + newDeferUsages: ReadonlyArray; +} { + const newDeferUsages: Set = new Set(); + const newKnownDeferUsages = new Set(knownDeferUsages); + + const groupedFieldSet = new Map< + string, + { + fields: Array; + deferUsages: DeferUsageSet; + knownDeferUsages: DeferUsageSet; + } + >(); + + const newGroupedFieldSetDetailsMap = new Map< + DeferUsageSet, + { + groupedFieldSet: Map< + string, + { + fields: Array; + deferUsages: DeferUsageSet; + knownDeferUsages: DeferUsageSet; + } + >; + shouldInitiateDefer: boolean; + } + >(); + + const map = new Map< + string, + { + deferUsageSet: DeferUsageSet; + fieldDetailsList: ReadonlyArray; + } + >(); + + for (const [responseKey, fieldDetailsList] of fields) { + const deferUsageSet = new Set(); + let inOriginalResult = false; + for (const fieldDetails of fieldDetailsList) { + const deferUsage = fieldDetails.deferUsage; + if (deferUsage === undefined) { + inOriginalResult = true; + continue; + } + deferUsageSet.add(deferUsage); + if (!knownDeferUsages.has(deferUsage)) { + newDeferUsages.add(deferUsage); + newKnownDeferUsages.add(deferUsage); + } + } + if (inOriginalResult) { + deferUsageSet.clear(); + } else { + deferUsageSet.forEach((deferUsage) => { + const ancestors = getAncestors(deferUsage); + for (const ancestor of ancestors) { + if (deferUsageSet.has(ancestor)) { + deferUsageSet.delete(deferUsage); + } + } + }); + } + map.set(responseKey, { deferUsageSet, fieldDetailsList }); + } + + for (const [responseKey, { deferUsageSet, fieldDetailsList }] of map) { + if (isSameSet(deferUsageSet, parentDeferUsages)) { + let fieldGroup = groupedFieldSet.get(responseKey); + if (fieldGroup === undefined) { + fieldGroup = { + fields: [], + deferUsages: deferUsageSet, + knownDeferUsages: newKnownDeferUsages, + }; + groupedFieldSet.set(responseKey, fieldGroup); + } + fieldGroup.fields.push(...fieldDetailsList); + continue; + } + + let newGroupedFieldSetDetails = getBySet( + newGroupedFieldSetDetailsMap, + deferUsageSet, + ); + let newGroupedFieldSet; + if (newGroupedFieldSetDetails === undefined) { + newGroupedFieldSet = new Map< + string, + { + fields: Array; + deferUsages: DeferUsageSet; + knownDeferUsages: DeferUsageSet; + } + >(); + + newGroupedFieldSetDetails = { + groupedFieldSet: newGroupedFieldSet, + shouldInitiateDefer: Array.from(deferUsageSet).some( + (deferUsage) => !parentDeferUsages.has(deferUsage), + ), + }; + newGroupedFieldSetDetailsMap.set( + deferUsageSet, + newGroupedFieldSetDetails, + ); + } else { + newGroupedFieldSet = newGroupedFieldSetDetails.groupedFieldSet; + } + let fieldGroup = newGroupedFieldSet.get(responseKey); + if (fieldGroup === undefined) { + fieldGroup = { + fields: [], + deferUsages: deferUsageSet, + knownDeferUsages: newKnownDeferUsages, + }; + newGroupedFieldSet.set(responseKey, fieldGroup); + } + fieldGroup.fields.push(...fieldDetailsList); + } + + return { + groupedFieldSet, + newGroupedFieldSetDetailsMap, + newDeferUsages: Array.from(newDeferUsages), + }; +} + +function getAncestors(deferUsage: DeferUsage): ReadonlyArray { + const ancestors: Array = []; + let parentDeferUsage: DeferUsage | undefined = deferUsage.parentDeferUsage; + while (parentDeferUsage !== undefined) { + ancestors.unshift(parentDeferUsage); + parentDeferUsage = parentDeferUsage.parentDeferUsage; + } + return ancestors; +} diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index 1d0341b4cc..7625e1af18 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -1,7 +1,5 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap.js'; -import { getBySet } from '../jsutils/getBySet.js'; import { invariant } from '../jsutils/invariant.js'; -import { isSameSet } from '../jsutils/isSameSet.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; import type { @@ -30,36 +28,12 @@ import { getDirectiveValues } from './values.js'; export interface DeferUsage { label: string | undefined; - ancestors: ReadonlyArray; + parentDeferUsage: DeferUsage | undefined; } -export const NON_DEFERRED_TARGET_SET: TargetSet = new Set([undefined]); - -export type Target = DeferUsage | undefined; -export type TargetSet = ReadonlySet; -export type DeferUsageSet = ReadonlySet; - export interface FieldDetails { node: FieldNode; - target: Target; -} - -export interface FieldGroup { - fields: ReadonlyArray; - targets: TargetSet; -} - -export type GroupedFieldSet = Map; - -export interface GroupedFieldSetDetails { - groupedFieldSet: GroupedFieldSet; - shouldInitiateDefer: boolean; -} - -export interface CollectFieldsResult { - groupedFieldSet: GroupedFieldSet; - newGroupedFieldSetDetails: Map; - newDeferUsages: ReadonlyArray; + deferUsage: DeferUsage | undefined; } interface CollectFieldsContext { @@ -68,9 +42,6 @@ interface CollectFieldsContext { variableValues: { [variable: string]: unknown }; operation: OperationDefinitionNode; runtimeType: GraphQLObjectType; - targetsByKey: Map>; - fieldsByTarget: Map>; - newDeferUsages: Array; visitedFragmentNames: Set; } @@ -89,25 +60,19 @@ export function collectFields( variableValues: { [variable: string]: unknown }, runtimeType: GraphQLObjectType, operation: OperationDefinitionNode, -): CollectFieldsResult { +): Map> { + const groupedFieldSet = new AccumulatorMap(); const context: CollectFieldsContext = { schema, fragments, variableValues, runtimeType, operation, - fieldsByTarget: new Map(), - targetsByKey: new Map(), - newDeferUsages: [], visitedFragmentNames: new Set(), }; - collectFieldsImpl(context, operation.selectionSet); - - return { - ...buildGroupedFieldSets(context.targetsByKey, context.fieldsByTarget), - newDeferUsages: context.newDeferUsages, - }; + collectFieldsImpl(context, operation.selectionSet, groupedFieldSet); + return groupedFieldSet; } /** @@ -127,42 +92,39 @@ export function collectSubfields( variableValues: { [variable: string]: unknown }, operation: OperationDefinitionNode, returnType: GraphQLObjectType, - fieldGroup: FieldGroup, -): CollectFieldsResult { + fieldDetails: ReadonlyArray, +): Map> { const context: CollectFieldsContext = { schema, fragments, variableValues, runtimeType: returnType, operation, - fieldsByTarget: new Map(), - targetsByKey: new Map(), - newDeferUsages: [], visitedFragmentNames: new Set(), }; + const subGroupedFieldSet = new AccumulatorMap(); - for (const fieldDetails of fieldGroup.fields) { - const node = fieldDetails.node; + for (const fieldDetail of fieldDetails) { + const node = fieldDetail.node; if (node.selectionSet) { - collectFieldsImpl(context, node.selectionSet, fieldDetails.target); + collectFieldsImpl( + context, + node.selectionSet, + subGroupedFieldSet, + fieldDetail.deferUsage, + ); } } - return { - ...buildGroupedFieldSets( - context.targetsByKey, - context.fieldsByTarget, - fieldGroup.targets, - ), - newDeferUsages: context.newDeferUsages, - }; + return subGroupedFieldSet; } function collectFieldsImpl( context: CollectFieldsContext, selectionSet: SelectionSetNode, - parentTarget?: Target, - newTarget?: Target, + groupedFieldSet: AccumulatorMap, + parentDeferUsage?: DeferUsage, + deferUsage?: DeferUsage, ): void { const { schema, @@ -170,9 +132,6 @@ function collectFieldsImpl( variableValues, runtimeType, operation, - targetsByKey, - fieldsByTarget, - newDeferUsages, visitedFragmentNames, } = context; @@ -182,20 +141,10 @@ function collectFieldsImpl( if (!shouldIncludeNode(variableValues, selection)) { continue; } - const key = getFieldEntryKey(selection); - const target = newTarget ?? parentTarget; - let keyTargets = targetsByKey.get(key); - if (keyTargets === undefined) { - keyTargets = new Set(); - targetsByKey.set(key, keyTargets); - } - keyTargets.add(target); - let targetFields = fieldsByTarget.get(target); - if (targetFields === undefined) { - targetFields = new AccumulatorMap(); - fieldsByTarget.set(target, targetFields); - } - targetFields.add(key, selection); + groupedFieldSet.add(getFieldEntryKey(selection), { + node: selection, + deferUsage: deferUsage ?? parentDeferUsage, + }); break; } case Kind.INLINE_FRAGMENT: { @@ -206,25 +155,19 @@ function collectFieldsImpl( continue; } - const defer = getDeferValues(operation, variableValues, selection); - - let target: Target; - if (!defer) { - target = newTarget; - } else { - const ancestors = - parentTarget === undefined - ? [parentTarget] - : [parentTarget, ...parentTarget.ancestors]; - target = { ...defer, ancestors }; - newDeferUsages.push(target); - } + const newDeferUsage = getDeferUsage( + operation, + variableValues, + selection, + parentDeferUsage, + ); collectFieldsImpl( context, selection.selectionSet, - parentTarget, - target, + groupedFieldSet, + parentDeferUsage, + newDeferUsage ?? deferUsage, ); break; @@ -232,12 +175,18 @@ function collectFieldsImpl( case Kind.FRAGMENT_SPREAD: { const fragName = selection.name.value; - if (!shouldIncludeNode(variableValues, selection)) { - continue; - } + const newDeferUsage = getDeferUsage( + operation, + variableValues, + selection, + parentDeferUsage, + ); - const defer = getDeferValues(operation, variableValues, selection); - if (visitedFragmentNames.has(fragName) && !defer) { + if ( + !newDeferUsage && + (visitedFragmentNames.has(fragName) || + !shouldIncludeNode(variableValues, selection)) + ) { continue; } @@ -248,21 +197,17 @@ function collectFieldsImpl( ) { continue; } - - let target: Target; - if (!defer) { + if (!newDeferUsage) { visitedFragmentNames.add(fragName); - target = newTarget; - } else { - const ancestors = - parentTarget === undefined - ? [parentTarget] - : [parentTarget, ...parentTarget.ancestors]; - target = { ...defer, ancestors }; - newDeferUsages.push(target); } - collectFieldsImpl(context, fragment.selectionSet, parentTarget, target); + collectFieldsImpl( + context, + fragment.selectionSet, + groupedFieldSet, + parentDeferUsage, + newDeferUsage ?? deferUsage, + ); break; } } @@ -274,11 +219,12 @@ function collectFieldsImpl( * deferred based on the experimental flag, defer directive present and * not disabled by the "if" argument. */ -function getDeferValues( +function getDeferUsage( operation: OperationDefinitionNode, variableValues: { [variable: string]: unknown }, node: FragmentSpreadNode | InlineFragmentNode, -): undefined | { label: string | undefined } { + parentDeferUsage: DeferUsage | undefined, +): DeferUsage | undefined { const defer = getDirectiveValues(GraphQLDeferDirective, node, variableValues); if (!defer) { @@ -296,6 +242,7 @@ function getDeferValues( return { label: typeof defer.label === 'string' ? defer.label : undefined, + parentDeferUsage, }; } @@ -351,143 +298,3 @@ function doesFragmentConditionMatch( function getFieldEntryKey(node: FieldNode): string { return node.alias ? node.alias.value : node.name.value; } - -function buildGroupedFieldSets( - targetsByKey: Map>, - fieldsByTarget: Map>>, - parentTargets = NON_DEFERRED_TARGET_SET, -): { - groupedFieldSet: GroupedFieldSet; - newGroupedFieldSetDetails: Map; -} { - const { parentTargetKeys, targetSetDetailsMap } = getTargetSetDetails( - targetsByKey, - parentTargets, - ); - - const groupedFieldSet = - parentTargetKeys.size > 0 - ? getOrderedGroupedFieldSet( - parentTargetKeys, - parentTargets, - targetsByKey, - fieldsByTarget, - ) - : new Map(); - - const newGroupedFieldSetDetails = new Map< - DeferUsageSet, - GroupedFieldSetDetails - >(); - - for (const [maskingTargets, targetSetDetails] of targetSetDetailsMap) { - const { keys, shouldInitiateDefer } = targetSetDetails; - - const newGroupedFieldSet = getOrderedGroupedFieldSet( - keys, - maskingTargets, - targetsByKey, - fieldsByTarget, - ); - - // All TargetSets that causes new grouped field sets consist only of DeferUsages - // and have shouldInitiateDefer defined - newGroupedFieldSetDetails.set(maskingTargets as DeferUsageSet, { - groupedFieldSet: newGroupedFieldSet, - shouldInitiateDefer, - }); - } - - return { - groupedFieldSet, - newGroupedFieldSetDetails, - }; -} - -interface TargetSetDetails { - keys: Set; - shouldInitiateDefer: boolean; -} - -function getTargetSetDetails( - targetsByKey: Map>, - parentTargets: TargetSet, -): { - parentTargetKeys: ReadonlySet; - targetSetDetailsMap: Map; -} { - const parentTargetKeys = new Set(); - const targetSetDetailsMap = new Map(); - - for (const [responseKey, targets] of targetsByKey) { - const maskingTargetList: Array = []; - for (const target of targets) { - if ( - target === undefined || - target.ancestors.every((ancestor) => !targets.has(ancestor)) - ) { - maskingTargetList.push(target); - } - } - - const maskingTargets: TargetSet = new Set(maskingTargetList); - if (isSameSet(maskingTargets, parentTargets)) { - parentTargetKeys.add(responseKey); - continue; - } - - let targetSetDetails = getBySet(targetSetDetailsMap, maskingTargets); - if (targetSetDetails === undefined) { - targetSetDetails = { - keys: new Set(), - shouldInitiateDefer: maskingTargetList.some( - (deferUsage) => !parentTargets.has(deferUsage), - ), - }; - targetSetDetailsMap.set(maskingTargets, targetSetDetails); - } - targetSetDetails.keys.add(responseKey); - } - - return { - parentTargetKeys, - targetSetDetailsMap, - }; -} - -function getOrderedGroupedFieldSet( - keys: ReadonlySet, - maskingTargets: TargetSet, - targetsByKey: Map>, - fieldsByTarget: Map>>, -): GroupedFieldSet { - const groupedFieldSet = new Map< - string, - { fields: Array; targets: TargetSet } - >(); - - const firstTarget = maskingTargets.values().next().value as Target; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const firstFields = fieldsByTarget.get(firstTarget)!; - for (const [key] of firstFields) { - if (keys.has(key)) { - let fieldGroup = groupedFieldSet.get(key); - if (fieldGroup === undefined) { - fieldGroup = { fields: [], targets: maskingTargets }; - groupedFieldSet.set(key, fieldGroup); - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const target of targetsByKey.get(key)!) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const fieldsForTarget = fieldsByTarget.get(target)!; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const nodes = fieldsForTarget.get(key)!; - // the following line is an optional minor optimization - fieldsForTarget.delete(key); - fieldGroup.fields.push(...nodes.map((node) => ({ node, target }))); - } - } - } - - return groupedFieldSet; -} diff --git a/src/execution/execute.ts b/src/execution/execute.ts index a19a51a217..baf6b41ea1 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -48,17 +48,14 @@ import type { GraphQLSchema } from '../type/schema.js'; import { assertValidSchema } from '../type/validate.js'; import type { - DeferUsage, DeferUsageSet, FieldGroup, GroupedFieldSet, - GroupedFieldSetDetails, -} from './collectFields.js'; -import { - collectFields, - collectSubfields as _collectSubfields, - NON_DEFERRED_TARGET_SET, -} from './collectFields.js'; + NewGroupedFieldSetDetails, +} from './buildFieldPlan.js'; +import { buildFieldPlan } from './buildFieldPlan.js'; +import type { DeferUsage, FieldDetails } from './collectFields.js'; +import { collectFields, collectSubfields } from './collectFields.js'; import type { ExecutionResult, ExperimentalIncrementalExecutionResults, @@ -84,24 +81,30 @@ import { // so just disable it for entire file. /** - * A memoized collection of relevant subfields with regard to the return - * type. Memoizing ensures the subfields are not repeatedly calculated, which + * A memoized function for building subfield plans with regard to the return + * type. Memoizing ensures the subfield plans are not repeatedly calculated, which * saves overhead when resolving lists of values. */ -const collectSubfields = memoize3( +const buildSubFieldPlan = memoize3( ( exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldGroup: FieldGroup, - ) => - _collectSubfields( + ) => { + const subFields = collectSubfields( exeContext.schema, exeContext.fragments, exeContext.variableValues, exeContext.operation, returnType, - fieldGroup, - ), + fieldGroup.fields, + ); + return buildFieldPlan( + subFields, + fieldGroup.deferUsages, + fieldGroup.knownDeferUsages, + ); + }, ); /** @@ -405,8 +408,15 @@ function executeOperation( ); } - const { groupedFieldSet, newGroupedFieldSetDetails, newDeferUsages } = - collectFields(schema, fragments, variableValues, rootType, operation); + const fields = collectFields( + schema, + fragments, + variableValues, + rootType, + operation, + ); + const { groupedFieldSet, newGroupedFieldSetDetailsMap, newDeferUsages } = + buildFieldPlan(fields); const newDeferMap = addNewDeferredFragments( incrementalPublisher, @@ -418,7 +428,7 @@ function executeOperation( const newDeferredGroupedFieldSetRecords = addNewDeferredGroupedFieldSets( incrementalPublisher, - newGroupedFieldSetDetails, + newGroupedFieldSetDetailsMap, newDeferMap, path, ); @@ -603,7 +613,7 @@ function executeField( const info = buildResolveInfo( exeContext, fieldDef, - fieldGroup, + toNodes(fieldGroup), parentType, path, ); @@ -688,7 +698,7 @@ function executeField( export function buildResolveInfo( exeContext: ExecutionContext, fieldDef: GraphQLField, - fieldGroup: FieldGroup, + fieldNodes: ReadonlyArray, parentType: GraphQLObjectType, path: Path, ): GraphQLResolveInfo { @@ -696,7 +706,7 @@ export function buildResolveInfo( // information about the current execution state. return { fieldName: fieldDef.name, - fieldNodes: toNodes(fieldGroup), + fieldNodes, returnType: fieldDef.type, parentType, path, @@ -946,9 +956,8 @@ function getStreamUsage( const streamedFieldGroup: FieldGroup = { fields: fieldGroup.fields.map((fieldDetails) => ({ node: fieldDetails.node, - target: undefined, + deferUsage: undefined, })), - targets: NON_DEFERRED_TARGET_SET, }; const streamUsage = { @@ -1455,18 +1464,15 @@ function addNewDeferredFragments( // For each new deferUsage object: for (const newDeferUsage of newDeferUsages) { - // DeferUsage objects track their parent targets; the immediate parent is always the first member of this list. - const parentTarget = newDeferUsage.ancestors[0]; + const parentDeferUsage = newDeferUsage.parentDeferUsage; - // If the parent target is defined, the parent target is a DeferUsage object and - // the parent result record is the DeferredFragmentRecord corresponding to that DeferUsage. - // If the parent target is not defined, the parent result record is either: + // If the parent defer usage is not defined, the parent result record is either: // - the InitialResultRecord, or // - a StreamItemsRecord, as `@defer` may be nested under `@stream`. const parent = - parentTarget === undefined + parentDeferUsage === undefined ? (incrementalDataRecord as InitialResultRecord | StreamItemsRecord) - : deferredFragmentRecordFromDeferUsage(parentTarget, newDeferMap); + : deferredFragmentRecordFromDeferUsage(parentDeferUsage, newDeferMap); // Instantiate the new record. const deferredFragmentRecord = new DeferredFragmentRecord({ @@ -1497,7 +1503,7 @@ function deferredFragmentRecordFromDeferUsage( function addNewDeferredGroupedFieldSets( incrementalPublisher: IncrementalPublisher, - newGroupedFieldSetDetails: Map, + newGroupedFieldSetDetailsMap: Map, deferMap: ReadonlyMap, path?: Path | undefined, ): ReadonlyArray { @@ -1505,11 +1511,11 @@ function addNewDeferredGroupedFieldSets( []; for (const [ - newGroupedFieldSetDeferUsages, + deferUsageSet, { groupedFieldSet, shouldInitiateDefer }, - ] of newGroupedFieldSetDetails) { + ] of newGroupedFieldSetDetailsMap) { const deferredFragmentRecords = getDeferredFragmentRecords( - newGroupedFieldSetDeferUsages, + deferUsageSet, deferMap, ); const deferredGroupedFieldSetRecord = new DeferredGroupedFieldSetRecord({ @@ -1546,8 +1552,8 @@ function collectAndExecuteSubfields( deferMap: ReadonlyMap, ): PromiseOrValue> { // Collect sub-fields to execute to complete this value. - const { groupedFieldSet, newGroupedFieldSetDetails, newDeferUsages } = - collectSubfields(exeContext, returnType, fieldGroup); + const { groupedFieldSet, newGroupedFieldSetDetailsMap, newDeferUsages } = + buildSubFieldPlan(exeContext, returnType, fieldGroup); const incrementalPublisher = exeContext.incrementalPublisher; @@ -1561,7 +1567,7 @@ function collectAndExecuteSubfields( const newDeferredGroupedFieldSetRecords = addNewDeferredGroupedFieldSets( incrementalPublisher, - newGroupedFieldSetDetails, + newGroupedFieldSetDetailsMap, newDeferMap, path, ); @@ -1801,7 +1807,7 @@ function executeSubscription( ); } - const { groupedFieldSet } = collectFields( + const fields = collectFields( schema, fragments, variableValues, @@ -1809,18 +1815,19 @@ function executeSubscription( operation, ); - const firstRootField = groupedFieldSet.entries().next().value as [ + const firstRootField = fields.entries().next().value as [ string, - FieldGroup, + ReadonlyArray, ]; - const [responseName, fieldGroup] = firstRootField; - const fieldName = fieldGroup.fields[0].node.name.value; + const [responseName, fieldDetailsList] = firstRootField; + const fieldName = fieldDetailsList[0].node.name.value; const fieldDef = schema.getField(rootType, fieldName); + const fieldNodes = fieldDetailsList.map((fieldDetails) => fieldDetails.node); if (!fieldDef) { throw new GraphQLError( `The subscription field "${fieldName}" is not defined.`, - { nodes: toNodes(fieldGroup) }, + { nodes: fieldNodes }, ); } @@ -1828,7 +1835,7 @@ function executeSubscription( const info = buildResolveInfo( exeContext, fieldDef, - fieldGroup, + fieldNodes, rootType, path, ); @@ -1839,11 +1846,7 @@ function executeSubscription( // Build a JS object of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. - const args = getArgumentValues( - fieldDef, - fieldGroup.fields[0].node, - variableValues, - ); + const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues); // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly @@ -1857,13 +1860,13 @@ function executeSubscription( if (isPromise(result)) { return result.then(assertEventStream).then(undefined, (error) => { - throw locatedError(error, toNodes(fieldGroup), pathToArray(path)); + throw locatedError(error, fieldNodes, pathToArray(path)); }); } return assertEventStream(result); } catch (error) { - throw locatedError(error, toNodes(fieldGroup), pathToArray(path)); + throw locatedError(error, fieldNodes, pathToArray(path)); } } diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index c0d1031103..15aee1ece4 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -10,13 +10,15 @@ import type { import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; -import type { FieldGroup } from '../../execution/collectFields.js'; +import type { FieldDetails } from '../../execution/collectFields.js'; import { collectFields } from '../../execution/collectFields.js'; import type { ValidationContext } from '../ValidationContext.js'; -function toNodes(fieldGroup: FieldGroup): ReadonlyArray { - return fieldGroup.fields.map((fieldDetails) => fieldDetails.node); +function toNodes( + fieldDetailsList: ReadonlyArray, +): ReadonlyArray { + return fieldDetailsList.map((fieldDetails) => fieldDetails.node); } /** @@ -47,15 +49,15 @@ export function SingleFieldSubscriptionsRule( fragments[definition.name.value] = definition; } } - const { groupedFieldSet } = collectFields( + const fields = collectFields( schema, fragments, variableValues, subscriptionType, node, ); - if (groupedFieldSet.size > 1) { - const fieldGroups = [...groupedFieldSet.values()]; + if (fields.size > 1) { + const fieldGroups = [...fields.values()]; const extraFieldGroups = fieldGroups.slice(1); const extraFieldSelections = extraFieldGroups.flatMap( (fieldGroup) => toNodes(fieldGroup), @@ -69,7 +71,7 @@ export function SingleFieldSubscriptionsRule( ), ); } - for (const fieldGroup of groupedFieldSet.values()) { + for (const fieldGroup of fields.values()) { const fieldName = toNodes(fieldGroup)[0].name.value; if (fieldName.startsWith('__')) { context.reportError(