diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index 1ca31acb88..47fbd33eca 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -8,7 +8,8 @@ import type { GraphQLFormattedError, } from '../error/GraphQLError.js'; -import type { GroupedFieldSet } from './buildFieldPlan.js'; +import type { DeferUsageSet } from './buildFieldPlan.js'; +import type { GroupedFieldSet } from './collectFields.js'; interface IncrementalUpdate> { pending: ReadonlyArray; @@ -739,7 +740,7 @@ export class IncrementalPublisher { } } -function isDeferredGroupedFieldSetRecord( +export function isDeferredGroupedFieldSetRecord( incrementalDataRecord: unknown, ): incrementalDataRecord is DeferredGroupedFieldSetRecord { return incrementalDataRecord instanceof DeferredGroupedFieldSetRecord; @@ -764,6 +765,7 @@ export class InitialResultRecord { /** @internal */ export class DeferredGroupedFieldSetRecord { path: ReadonlyArray; + deferUsageSet: DeferUsageSet; deferredFragmentRecords: ReadonlyArray; groupedFieldSet: GroupedFieldSet; shouldInitiateDefer: boolean; @@ -773,11 +775,13 @@ export class DeferredGroupedFieldSetRecord { constructor(opts: { path: Path | undefined; + deferUsageSet: DeferUsageSet; deferredFragmentRecords: ReadonlyArray; groupedFieldSet: GroupedFieldSet; shouldInitiateDefer: boolean; }) { this.path = pathToArray(opts.path); + this.deferUsageSet = opts.deferUsageSet; this.deferredFragmentRecords = opts.deferredFragmentRecords; this.groupedFieldSet = opts.groupedFieldSet; this.shouldInitiateDefer = opts.shouldInitiateDefer; diff --git a/src/execution/buildFieldPlan.ts b/src/execution/buildFieldPlan.ts index 390e2cf813..c9b369b4b5 100644 --- a/src/execution/buildFieldPlan.ts +++ b/src/execution/buildFieldPlan.ts @@ -1,63 +1,47 @@ 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 type GroupedFieldSet = Map; - export interface NewGroupedFieldSetDetails { groupedFieldSet: GroupedFieldSet; shouldInitiateDefer: boolean; } -export function buildFieldPlan( - fields: Map>, - parentDeferUsages: DeferUsageSet = new Set(), -): { +export interface FieldPlan { groupedFieldSet: GroupedFieldSet; newGroupedFieldSetDetailsMap: Map; -} { - const groupedFieldSet = new Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - } - >(); +} + +export function buildFieldPlan( + originalGroupedFieldSet: GroupedFieldSet, + parentDeferUsages: DeferUsageSet = new Set(), +): FieldPlan { + const groupedFieldSet: GroupedFieldSet = new Map(); const newGroupedFieldSetDetailsMap = new Map< DeferUsageSet, - { - groupedFieldSet: Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - } - >; - shouldInitiateDefer: boolean; - } + NewGroupedFieldSetDetails >(); 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; @@ -77,20 +61,12 @@ 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; } @@ -100,15 +76,7 @@ export function buildFieldPlan( ); let newGroupedFieldSet; if (newGroupedFieldSetDetails === undefined) { - newGroupedFieldSet = new Map< - string, - { - fields: Array; - deferUsages: DeferUsageSet; - knownDeferUsages: DeferUsageSet; - } - >(); - + newGroupedFieldSet = new Map(); newGroupedFieldSetDetails = { groupedFieldSet: newGroupedFieldSet, shouldInitiateDefer: Array.from(deferUsageSet).some( @@ -122,15 +90,7 @@ export function buildFieldPlan( } else { newGroupedFieldSet = newGroupedFieldSetDetails.groupedFieldSet; } - 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..2815c04c61 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 = Map; + 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 370186f552..010294207b 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -49,12 +49,14 @@ import { assertValidSchema } from '../type/validate.js'; import type { DeferUsageSet, - FieldGroup, - GroupedFieldSet, NewGroupedFieldSetDetails, } from './buildFieldPlan.js'; import { buildFieldPlan } from './buildFieldPlan.js'; -import type { DeferUsage, FieldDetails } from './collectFields.js'; +import type { + DeferUsage, + FieldGroup, + GroupedFieldSet, +} from './collectFields.js'; import { collectFields, collectSubfields } from './collectFields.js'; import type { ExecutionResult, @@ -66,6 +68,7 @@ import { DeferredGroupedFieldSetRecord, IncrementalPublisher, InitialResultRecord, + isDeferredGroupedFieldSetRecord, StreamItemsRecord, StreamRecord, } from './IncrementalPublisher.js'; @@ -90,17 +93,19 @@ const buildSubFieldPlan = memoize3( exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldGroup: FieldGroup, + deferUsages: DeferUsageSet | undefined, ) => { - const { fields: subFields, newDeferUsages } = collectSubfields( - exeContext.schema, - exeContext.fragments, - exeContext.variableValues, - exeContext.operation, - returnType, - fieldGroup.fields, - ); + const { groupedFieldSet: subGroupedFieldSet, newDeferUsages } = + collectSubfields( + exeContext.schema, + exeContext.fragments, + exeContext.variableValues, + exeContext.operation, + returnType, + fieldGroup, + ); return { - ...buildFieldPlan(subFields, fieldGroup.deferUsages), + ...buildFieldPlan(subGroupedFieldSet, deferUsages), newDeferUsages, }; }, @@ -407,15 +412,11 @@ function executeOperation( ); } - const { fields, newDeferUsages } = collectFields( - schema, - fragments, - variableValues, - rootType, - operation, + const { groupedFieldSet: originalGroupedFieldSet, newDeferUsages } = + collectFields(schema, fragments, variableValues, rootType, operation); + const { groupedFieldSet, newGroupedFieldSetDetailsMap } = buildFieldPlan( + originalGroupedFieldSet, ); - const { groupedFieldSet, newGroupedFieldSetDetailsMap } = - buildFieldPlan(fields); const newDeferMap = addNewDeferredFragments( incrementalPublisher, @@ -582,7 +583,7 @@ function executeFields( } function toNodes(fieldGroup: FieldGroup): ReadonlyArray { - return fieldGroup.fields.map((fieldDetails) => fieldDetails.node); + return fieldGroup.map((fieldDetails) => fieldDetails.node); } /** @@ -600,7 +601,7 @@ function executeField( incrementalDataRecord: IncrementalDataRecord, deferMap: ReadonlyMap, ): PromiseOrValue { - 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; @@ -624,7 +625,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, ); @@ -925,7 +926,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, ); @@ -952,17 +953,15 @@ 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 streamedReadonlyArray: FieldGroup = fieldGroup.map((fieldDetails) => ({ + node: fieldDetails.node, + deferUsage: undefined, + })); const streamUsage = { initialCount: stream.initialCount, label: typeof stream.label === 'string' ? stream.label : undefined, - fieldGroup: streamedFieldGroup, + fieldGroup: streamedReadonlyArray, }; (fieldGroup as unknown as { _streamUsage: StreamUsage })._streamUsage = @@ -1519,6 +1518,7 @@ function addNewDeferredGroupedFieldSets( ); const deferredGroupedFieldSetRecord = new DeferredGroupedFieldSetRecord({ path, + deferUsageSet, deferredFragmentRecords, groupedFieldSet, shouldInitiateDefer, @@ -1551,8 +1551,11 @@ function collectAndExecuteSubfields( deferMap: ReadonlyMap, ): PromiseOrValue> { // Collect sub-fields to execute to complete this value. + const deferUsageSet = isDeferredGroupedFieldSetRecord(incrementalDataRecord) + ? incrementalDataRecord.deferUsageSet + : undefined; const { groupedFieldSet, newGroupedFieldSetDetailsMap, newDeferUsages } = - buildSubFieldPlan(exeContext, returnType, fieldGroup); + buildSubFieldPlan(exeContext, returnType, fieldGroup, deferUsageSet); const incrementalPublisher = exeContext.incrementalPublisher; @@ -1806,7 +1809,7 @@ function executeSubscription( ); } - const { fields } = collectFields( + const { groupedFieldSet } = collectFields( schema, fragments, variableValues, @@ -1814,15 +1817,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.`, diff --git a/src/jsutils/memoize3.ts b/src/jsutils/memoize3.ts index 213cb95d10..7467259e70 100644 --- a/src/jsutils/memoize3.ts +++ b/src/jsutils/memoize3.ts @@ -6,10 +6,13 @@ export function memoize3< A2 extends object, A3 extends object, R, ->(fn: (a1: A1, a2: A2, a3: A3) => R): (a1: A1, a2: A2, a3: A3) => R { + T extends Array, +>( + fn: (a1: A1, a2: A2, a3: A3, ...rest: T) => R, +): (a1: A1, a2: A2, a3: A3, ...rest: T) => R { let cache0: WeakMap>>; - return function memoized(a1, a2, a3) { + return function memoized(a1, a2, a3, ...rest) { if (cache0 === undefined) { cache0 = new WeakMap(); } @@ -28,7 +31,7 @@ export function memoize3< let fnResult = cache2.get(a3); if (fnResult === undefined) { - fnResult = fn(a1, a2, a3); + fnResult = fn(a1, a2, a3, ...rest); cache2.set(a3, fnResult); } 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(