From 3c6b5c1c02c0af0f3088f24be78416a60e05de2c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Thu, 25 Jul 2024 16:37:56 +0300 Subject: [PATCH] polish(incremental): lazily create DeferredFragments goal: avoid creating or passing around the deferMap methodology: each DeferredFragmentRecord will be unique for a given deferUsage and creationPath - we annotate the deferUsage with a "depth" property representing the path length in the response for wherever this defer is delivered. - from a given execution group path, we can derive the path for the deferredFragment for a given deferUsage by "rewinding" the execution group path to the depth annotated on the given deferUsage --- src/execution/DeferredFragments.ts | 108 +++++++++++ src/execution/IncrementalGraph.ts | 108 +++++++++-- src/execution/IncrementalPublisher.ts | 84 ++++----- src/execution/collectFields.ts | 67 +++---- src/execution/execute.ts | 174 +++--------------- src/execution/types.ts | 45 +---- src/jsutils/memoize3.ts | 11 +- .../rules/SingleFieldSubscriptionsRule.ts | 2 +- 8 files changed, 303 insertions(+), 296 deletions(-) create mode 100644 src/execution/DeferredFragments.ts diff --git a/src/execution/DeferredFragments.ts b/src/execution/DeferredFragments.ts new file mode 100644 index 0000000000..41ccc03927 --- /dev/null +++ b/src/execution/DeferredFragments.ts @@ -0,0 +1,108 @@ +import type { Path } from '../jsutils/Path.js'; + +import type { DeferUsage } from './collectFields.js'; +import type { + PendingExecutionGroup, + StreamRecord, + SuccessfulExecutionGroup, +} from './types.js'; + +export type DeliveryGroup = DeferredFragmentRecord | StreamRecord; + +/** @internal */ +export class DeferredFragmentRecord { + path: Path | undefined; + label: string | undefined; + parentDeferUsage: DeferUsage | undefined; + id?: string | undefined; + pendingExecutionGroups: Set; + successfulExecutionGroups: Set; + children: Set; + + constructor( + path: Path | undefined, + label: string | undefined, + parentDeferUsage: DeferUsage | undefined, + ) { + this.path = path; + this.label = label; + this.parentDeferUsage = parentDeferUsage; + this.pendingExecutionGroups = new Set(); + this.successfulExecutionGroups = new Set(); + this.children = new Set(); + } +} + +export function isDeferredFragmentRecord( + deliveryGroup: DeliveryGroup, +): deliveryGroup is DeferredFragmentRecord { + return deliveryGroup instanceof DeferredFragmentRecord; +} + +/** + * @internal + */ +export class DeferredFragmentFactory { + private _rootDeferredFragments = new Map< + DeferUsage, + DeferredFragmentRecord + >(); + + get(deferUsage: DeferUsage, path: Path | undefined): DeferredFragmentRecord { + const deferUsagePath = this._pathAtDepth(path, deferUsage.depth); + let deferredFragmentRecords: + | Map + | undefined; + if (deferUsagePath === undefined) { + deferredFragmentRecords = this._rootDeferredFragments; + } else { + // A doubly nested Map> + // could be used, but could leak memory in long running operations. + // A WeakMap could be used instead. The below implementation is + // WeakMap-Like, saving the Map on the Path object directly. + // Alternatively, memory could be reclaimed manually, taking care to + // also reclaim memory for nested DeferredFragmentRecords if the parent + // is removed secondary to an error. + deferredFragmentRecords = ( + deferUsagePath as unknown as { + deferredFragmentRecords: Map; + } + ).deferredFragmentRecords; + if (deferredFragmentRecords === undefined) { + deferredFragmentRecords = new Map(); + ( + deferUsagePath as unknown as { + deferredFragmentRecords: Map; + } + ).deferredFragmentRecords = deferredFragmentRecords; + } + } + let deferredFragmentRecord = deferredFragmentRecords.get(deferUsage); + if (deferredFragmentRecord === undefined) { + const { label, parentDeferUsage } = deferUsage; + deferredFragmentRecord = new DeferredFragmentRecord( + deferUsagePath, + label, + parentDeferUsage, + ); + deferredFragmentRecords.set(deferUsage, deferredFragmentRecord); + } + return deferredFragmentRecord; + } + + private _pathAtDepth( + path: Path | undefined, + depth: number, + ): Path | undefined { + if (depth === 0) { + return; + } + const stack: Array = []; + let currentPath = path; + while (currentPath !== undefined) { + stack.unshift(currentPath); + currentPath = currentPath.prev; + } + return stack[depth - 1]; + } +} diff --git a/src/execution/IncrementalGraph.ts b/src/execution/IncrementalGraph.ts index 2fe86e7d11..e8a1b7eb9e 100644 --- a/src/execution/IncrementalGraph.ts +++ b/src/execution/IncrementalGraph.ts @@ -1,13 +1,21 @@ import { BoxedPromiseOrValue } from '../jsutils/BoxedPromiseOrValue.js'; import { invariant } from '../jsutils/invariant.js'; import { isPromise } from '../jsutils/isPromise.js'; +import type { Path } from '../jsutils/Path.js'; import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js'; import type { GraphQLError } from '../error/GraphQLError.js'; +import type { DeferUsage } from './collectFields.js'; import type { DeferredFragmentRecord, DeliveryGroup, +} from './DeferredFragments.js'; +import { + DeferredFragmentFactory, + isDeferredFragmentRecord, +} from './DeferredFragments.js'; +import type { IncrementalDataRecord, IncrementalDataRecordResult, PendingExecutionGroup, @@ -15,20 +23,21 @@ import type { StreamRecord, SuccessfulExecutionGroup, } from './types.js'; -import { isDeferredFragmentRecord, isPendingExecutionGroup } from './types.js'; +import { isPendingExecutionGroup } from './types.js'; /** * @internal */ export class IncrementalGraph { private _rootNodes: Set; - + private _deferredFragmentFactory: DeferredFragmentFactory; private _completedQueue: Array; private _nextQueue: Array< (iterable: Iterable | undefined) => void >; constructor() { + this._deferredFragmentFactory = new DeferredFragmentFactory(); this._rootNodes = new Set(); this._completedQueue = []; this._nextQueue = []; @@ -51,11 +60,15 @@ export class IncrementalGraph { ): void { const { pendingExecutionGroup, incrementalDataRecords } = successfulExecutionGroup; + const { deferUsages, path } = pendingExecutionGroup; - const deferredFragmentRecords = - pendingExecutionGroup.deferredFragmentRecords; - - for (const deferredFragmentRecord of deferredFragmentRecords) { + const deferredFragmentRecords: Array = []; + for (const deferUsage of deferUsages) { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); + deferredFragmentRecords.push(deferredFragmentRecord); const { pendingExecutionGroups, successfulExecutionGroups } = deferredFragmentRecord; pendingExecutionGroups.delete(pendingExecutionGroup); @@ -70,6 +83,26 @@ export class IncrementalGraph { } } + getDeepestDeferredFragmentAtRoot( + initialDeferUsage: DeferUsage, + deferUsages: ReadonlySet, + path: Path | undefined, + ): DeferredFragmentRecord { + let bestDeferUsage = initialDeferUsage; + let maxDepth = initialDeferUsage.depth; + for (const deferUsage of deferUsages) { + if (deferUsage === initialDeferUsage) { + continue; + } + const depth = deferUsage.depth; + if (depth > maxDepth) { + maxDepth = depth; + bestDeferUsage = deferUsage; + } + } + return this._deferredFragmentFactory.get(bestDeferUsage, path); + } + *currentCompletedBatch(): Generator { let completed; while ((completed = this._completedQueue.shift()) !== undefined) { @@ -102,12 +135,20 @@ export class IncrementalGraph { return this._rootNodes.size > 0; } - completeDeferredFragment(deferredFragmentRecord: DeferredFragmentRecord): + completeDeferredFragment( + deferUsage: DeferUsage, + path: Path | undefined, + ): | { + deferredFragmentRecord: DeferredFragmentRecord; newRootNodes: ReadonlyArray; successfulExecutionGroups: ReadonlyArray; } | undefined { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); if ( !this._rootNodes.has(deferredFragmentRecord) || deferredFragmentRecord.pendingExecutionGroups.size > 0 @@ -119,8 +160,13 @@ export class IncrementalGraph { ); this._rootNodes.delete(deferredFragmentRecord); for (const successfulExecutionGroup of successfulExecutionGroups) { - for (const otherDeferredFragmentRecord of successfulExecutionGroup - .pendingExecutionGroup.deferredFragmentRecords) { + const { deferUsages, path: resultPath } = + successfulExecutionGroup.pendingExecutionGroup; + for (const otherDeferUsage of deferUsages) { + const otherDeferredFragmentRecord = this._deferredFragmentFactory.get( + otherDeferUsage, + resultPath, + ); otherDeferredFragmentRecord.successfulExecutionGroups.delete( successfulExecutionGroup, ); @@ -129,17 +175,22 @@ export class IncrementalGraph { const newRootNodes = this._promoteNonEmptyToRoot( deferredFragmentRecord.children, ); - return { newRootNodes, successfulExecutionGroups }; + return { deferredFragmentRecord, newRootNodes, successfulExecutionGroups }; } removeDeferredFragment( - deferredFragmentRecord: DeferredFragmentRecord, - ): boolean { + deferUsage: DeferUsage, + path: Path | undefined, + ): DeferredFragmentRecord | undefined { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); if (!this._rootNodes.has(deferredFragmentRecord)) { - return false; + return; } this._rootNodes.delete(deferredFragmentRecord); - return true; + return deferredFragmentRecord; } removeStream(streamRecord: StreamRecord): void { @@ -153,7 +204,12 @@ export class IncrementalGraph { ): void { for (const incrementalDataRecord of incrementalDataRecords) { if (isPendingExecutionGroup(incrementalDataRecord)) { - for (const deferredFragmentRecord of incrementalDataRecord.deferredFragmentRecords) { + const { deferUsages, path } = incrementalDataRecord; + for (const deferUsage of deferUsages) { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); this._addDeferredFragment( deferredFragmentRecord, initialResultChildren, @@ -210,9 +266,17 @@ export class IncrementalGraph { private _completesRootNode( pendingExecutionGroup: PendingExecutionGroup, ): boolean { - return pendingExecutionGroup.deferredFragmentRecords.some( - (deferredFragmentRecord) => this._rootNodes.has(deferredFragmentRecord), - ); + const { deferUsages, path } = pendingExecutionGroup; + for (const deferUsage of deferUsages) { + const deferredFragmentRecord = this._deferredFragmentFactory.get( + deferUsage, + path, + ); + if (this._rootNodes.has(deferredFragmentRecord)) { + return true; + } + } + return false; } private _addDeferredFragment( @@ -222,12 +286,16 @@ export class IncrementalGraph { if (this._rootNodes.has(deferredFragmentRecord)) { return; } - const parent = deferredFragmentRecord.parent; - if (parent === undefined) { + const parentDeferUsage = deferredFragmentRecord.parentDeferUsage; + if (parentDeferUsage === undefined) { invariant(initialResultChildren !== undefined); initialResultChildren.add(deferredFragmentRecord); return; } + const parent = this._deferredFragmentFactory.get( + parentDeferUsage, + deferredFragmentRecord.path, + ); parent.children.add(deferredFragmentRecord); this._addDeferredFragment(parent, initialResultChildren); } diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index 51815d56e9..23d6e62f14 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -4,13 +4,12 @@ import { pathToArray } from '../jsutils/Path.js'; import type { GraphQLError } from '../error/GraphQLError.js'; +import type { DeliveryGroup } from './DeferredFragments.js'; import { IncrementalGraph } from './IncrementalGraph.js'; import type { CancellableStreamRecord, CompletedExecutionGroup, CompletedResult, - DeferredFragmentRecord, - DeliveryGroup, ExperimentalIncrementalExecutionResults, IncrementalDataRecord, IncrementalDataRecordResult, @@ -218,16 +217,16 @@ class IncrementalPublisher { completedExecutionGroup: CompletedExecutionGroup, context: SubsequentIncrementalExecutionResultContext, ): void { + const { deferUsages, path } = completedExecutionGroup.pendingExecutionGroup; if (isFailedExecutionGroup(completedExecutionGroup)) { - for (const deferredFragmentRecord of completedExecutionGroup - .pendingExecutionGroup.deferredFragmentRecords) { - const id = deferredFragmentRecord.id; - if ( - !this._incrementalGraph.removeDeferredFragment(deferredFragmentRecord) - ) { + for (const deferUsage of deferUsages) { + const deferredFragmentRecord = + this._incrementalGraph.removeDeferredFragment(deferUsage, path); + if (deferredFragmentRecord === undefined) { // This can occur if multiple deferred grouped field sets error for a fragment. continue; } + const id = deferredFragmentRecord.id; invariant(id !== undefined); context.completed.push({ id, @@ -241,34 +240,46 @@ class IncrementalPublisher { completedExecutionGroup, ); - for (const deferredFragmentRecord of completedExecutionGroup - .pendingExecutionGroup.deferredFragmentRecords) { + for (const deferUsage of deferUsages) { const completion = this._incrementalGraph.completeDeferredFragment( - deferredFragmentRecord, + deferUsage, + path, ); if (completion === undefined) { continue; } - const id = deferredFragmentRecord.id; - invariant(id !== undefined); const incremental = context.incremental; - const { newRootNodes, successfulExecutionGroups } = completion; + const { + deferredFragmentRecord, + newRootNodes, + successfulExecutionGroups, + } = completion; context.pending.push(...this._toPendingResults(newRootNodes)); for (const successfulExecutionGroup of successfulExecutionGroups) { - const { bestId, subPath } = this._getBestIdAndSubPath( - id, - deferredFragmentRecord, - successfulExecutionGroup, - ); + const { deferUsages: resultDeferUsages, path: resultPath } = + successfulExecutionGroup.pendingExecutionGroup; + const bestDeferredFragmentRecord = + this._incrementalGraph.getDeepestDeferredFragmentAtRoot( + deferUsage, + resultDeferUsages, + resultPath, + ); + const bestId = bestDeferredFragmentRecord.id; + invariant(bestId !== undefined); const incrementalEntry: IncrementalDeferResult = { ...successfulExecutionGroup.result, id: bestId, }; - if (subPath !== undefined) { + const subPath = pathToArray(resultPath).slice( + pathToArray(bestDeferredFragmentRecord.path).length, + ); + if (subPath.length > 0) { incrementalEntry.subPath = subPath; } incremental.push(incrementalEntry); } + const id = deferredFragmentRecord.id; + invariant(id !== undefined); context.completed.push({ id }); } } @@ -319,39 +330,6 @@ class IncrementalPublisher { } } - private _getBestIdAndSubPath( - initialId: string, - initialDeferredFragmentRecord: DeferredFragmentRecord, - completedExecutionGroup: CompletedExecutionGroup, - ): { bestId: string; subPath: ReadonlyArray | undefined } { - let maxLength = pathToArray(initialDeferredFragmentRecord.path).length; - let bestId = initialId; - - for (const deferredFragmentRecord of completedExecutionGroup - .pendingExecutionGroup.deferredFragmentRecords) { - if (deferredFragmentRecord === initialDeferredFragmentRecord) { - continue; - } - const id = deferredFragmentRecord.id; - // TODO: add test case for when an fragment has not been released, but might be processed for the shortest path. - /* c8 ignore next 3 */ - if (id === undefined) { - continue; - } - const fragmentPath = pathToArray(deferredFragmentRecord.path); - const length = fragmentPath.length; - if (length > maxLength) { - maxLength = length; - bestId = id; - } - } - const subPath = completedExecutionGroup.path.slice(maxLength); - return { - bestId, - subPath: subPath.length > 0 ? subPath : undefined, - }; - } - private async _returnAsyncIterators(): Promise { const cancellableStreams = this._context.cancellableStreams; if (cancellableStreams === undefined) { diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index d411ff3f77..6d29429935 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -1,6 +1,8 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap.js'; import { invariant } from '../jsutils/invariant.js'; import type { ObjMap } from '../jsutils/ObjMap.js'; +import type { Path } from '../jsutils/Path.js'; +import { pathToArray } from '../jsutils/Path.js'; import type { FieldNode, @@ -29,6 +31,7 @@ import { getDirectiveValues } from './values.js'; export interface DeferUsage { label: string | undefined; parentDeferUsage: DeferUsage | undefined; + depth: number; } export interface FieldDetails { @@ -38,7 +41,9 @@ export interface FieldDetails { export type FieldGroup = ReadonlyArray; -export type GroupedFieldSet = ReadonlyMap; +export type GroupedFieldSet = ReadonlyMap & { + encounteredDefer?: boolean; +}; interface CollectFieldsContext { schema: GraphQLSchema; @@ -47,6 +52,7 @@ interface CollectFieldsContext { operation: OperationDefinitionNode; runtimeType: GraphQLObjectType; visitedFragmentNames: Set; + encounteredDefer: boolean; } /** @@ -64,12 +70,8 @@ export function collectFields( variableValues: { [variable: string]: unknown }, runtimeType: GraphQLObjectType, operation: OperationDefinitionNode, -): { - groupedFieldSet: GroupedFieldSet; - newDeferUsages: ReadonlyArray; -} { +): GroupedFieldSet { const groupedFieldSet = new AccumulatorMap(); - const newDeferUsages: Array = []; const context: CollectFieldsContext = { schema, fragments, @@ -77,15 +79,14 @@ export function collectFields( runtimeType, operation, visitedFragmentNames: new Set(), + encounteredDefer: false, }; - collectFieldsImpl( - context, - operation.selectionSet, - groupedFieldSet, - newDeferUsages, - ); - return { groupedFieldSet, newDeferUsages }; + collectFieldsImpl(context, operation.selectionSet, groupedFieldSet); + if (context.encounteredDefer) { + (groupedFieldSet as GroupedFieldSet).encounteredDefer = true; + } + return groupedFieldSet; } /** @@ -106,10 +107,8 @@ export function collectSubfields( operation: OperationDefinitionNode, returnType: GraphQLObjectType, fieldGroup: FieldGroup, -): { - groupedFieldSet: GroupedFieldSet; - newDeferUsages: ReadonlyArray; -} { + path: Path, +): GroupedFieldSet { const context: CollectFieldsContext = { schema, fragments, @@ -117,34 +116,34 @@ export function collectSubfields( runtimeType: returnType, operation, visitedFragmentNames: new Set(), + encounteredDefer: false, }; const subGroupedFieldSet = new AccumulatorMap(); - const newDeferUsages: Array = []; for (const fieldDetail of fieldGroup) { - const node = fieldDetail.node; + const { node, deferUsage } = fieldDetail; if (node.selectionSet) { collectFieldsImpl( context, node.selectionSet, subGroupedFieldSet, - newDeferUsages, - fieldDetail.deferUsage, + path, + deferUsage, ); } } - return { - groupedFieldSet: subGroupedFieldSet, - newDeferUsages, - }; + if (context.encounteredDefer) { + (subGroupedFieldSet as GroupedFieldSet).encounteredDefer = true; + } + return subGroupedFieldSet; } function collectFieldsImpl( context: CollectFieldsContext, selectionSet: SelectionSetNode, groupedFieldSet: AccumulatorMap, - newDeferUsages: Array, + path?: Path, deferUsage?: DeferUsage, ): void { const { @@ -180,6 +179,7 @@ function collectFieldsImpl( operation, variableValues, selection, + path, deferUsage, ); @@ -188,16 +188,16 @@ function collectFieldsImpl( context, selection.selectionSet, groupedFieldSet, - newDeferUsages, + path, deferUsage, ); } else { - newDeferUsages.push(newDeferUsage); + context.encounteredDefer = true; collectFieldsImpl( context, selection.selectionSet, groupedFieldSet, - newDeferUsages, + path, newDeferUsage, ); } @@ -211,6 +211,7 @@ function collectFieldsImpl( operation, variableValues, selection, + path, deferUsage, ); @@ -235,16 +236,16 @@ function collectFieldsImpl( context, fragment.selectionSet, groupedFieldSet, - newDeferUsages, + path, deferUsage, ); } else { - newDeferUsages.push(newDeferUsage); + context.encounteredDefer = true; collectFieldsImpl( context, fragment.selectionSet, groupedFieldSet, - newDeferUsages, + path, newDeferUsage, ); } @@ -263,6 +264,7 @@ function getDeferUsage( operation: OperationDefinitionNode, variableValues: { [variable: string]: unknown }, node: FragmentSpreadNode | InlineFragmentNode, + path: Path | undefined, parentDeferUsage: DeferUsage | undefined, ): DeferUsage | undefined { const defer = getDirectiveValues(GraphQLDeferDirective, node, variableValues); @@ -283,6 +285,7 @@ function getDeferUsage( return { label: typeof defer.label === 'string' ? defer.label : undefined, parentDeferUsage, + depth: pathToArray(path).length, }; } diff --git a/src/execution/execute.ts b/src/execution/execute.ts index acc3f86040..7e9b0cf68e 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -50,11 +50,7 @@ import { assertValidSchema } from '../type/validate.js'; import type { DeferUsageSet, ExecutionPlan } from './buildExecutionPlan.js'; import { buildExecutionPlan } from './buildExecutionPlan.js'; -import type { - DeferUsage, - FieldGroup, - GroupedFieldSet, -} from './collectFields.js'; +import type { FieldGroup, GroupedFieldSet } from './collectFields.js'; import { collectFields, collectSubfields as _collectSubfields, @@ -72,7 +68,6 @@ import type { StreamItemResult, StreamRecord, } from './types.js'; -import { DeferredFragmentRecord } from './types.js'; import { getArgumentValues, getDirectiveValues, @@ -93,6 +88,7 @@ const collectSubfields = memoize3( exeContext: ExecutionContext, returnType: GraphQLObjectType, fieldGroup: FieldGroup, + path: Path, ) => _collectSubfields( exeContext.schema, @@ -101,6 +97,7 @@ const collectSubfields = memoize3( exeContext.operation, returnType, fieldGroup, + path, ), ); @@ -142,6 +139,7 @@ export interface ExecutionContext { subscribeFieldResolver: GraphQLFieldResolver; enableEarlyExecution: boolean; errors: Array | undefined; + encounteredDefer: boolean; cancellableStreams: Set | undefined; } @@ -274,32 +272,29 @@ function executeOperation( ); } - const collectedFields = collectFields( + const originalGroupedFieldSet = collectFields( schema, fragments, variableValues, rootType, operation, ); - let groupedFieldSet = collectedFields.groupedFieldSet; - const newDeferUsages = collectedFields.newDeferUsages; let graphqlWrappedResult: PromiseOrValue< GraphQLWrappedResult> >; - if (newDeferUsages.length === 0) { + if (!originalGroupedFieldSet.encounteredDefer) { graphqlWrappedResult = executeRootGroupedFieldSet( exeContext, operation.operation, rootType, rootValue, - groupedFieldSet, - undefined, + originalGroupedFieldSet, ); } else { - const executionPlan = buildExecutionPlan(groupedFieldSet); - groupedFieldSet = executionPlan.groupedFieldSet; - const newGroupedFieldSets = executionPlan.newGroupedFieldSets; - const newDeferMap = addNewDeferredFragments(newDeferUsages, new Map()); + exeContext.encounteredDefer = true; + const { groupedFieldSet, newGroupedFieldSets } = buildExecutionPlan( + originalGroupedFieldSet, + ); graphqlWrappedResult = executeRootGroupedFieldSet( exeContext, @@ -307,7 +302,6 @@ function executeOperation( rootType, rootValue, groupedFieldSet, - newDeferMap, ); if (newGroupedFieldSets.size > 0) { @@ -318,7 +312,6 @@ function executeOperation( undefined, undefined, newGroupedFieldSets, - newDeferMap, ); graphqlWrappedResult = withNewExecutionGroups( @@ -504,6 +497,7 @@ export function buildExecutionContext( subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, enableEarlyExecution: enableEarlyExecution === true, errors: undefined, + encounteredDefer: false, cancellableStreams: undefined, }; } @@ -525,7 +519,6 @@ function executeRootGroupedFieldSet( rootType: GraphQLObjectType, rootValue: unknown, groupedFieldSet: GroupedFieldSet, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { switch (operation) { case OperationTypeNode.QUERY: @@ -536,7 +529,6 @@ function executeRootGroupedFieldSet( undefined, groupedFieldSet, undefined, - deferMap, ); case OperationTypeNode.MUTATION: return executeFieldsSerially( @@ -546,7 +538,6 @@ function executeRootGroupedFieldSet( undefined, groupedFieldSet, undefined, - deferMap, ); case OperationTypeNode.SUBSCRIPTION: // TODO: deprecate `subscribe` and move all logic here @@ -558,7 +549,6 @@ function executeRootGroupedFieldSet( undefined, groupedFieldSet, undefined, - deferMap, ); } } @@ -574,7 +564,6 @@ function executeFieldsSerially( path: Path | undefined, groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { return promiseReduce( groupedFieldSet, @@ -587,7 +576,6 @@ function executeFieldsSerially( fieldGroup, fieldPath, incrementalContext, - deferMap, ); if (result === undefined) { return graphqlWrappedResult; @@ -618,7 +606,6 @@ function executeFields( path: Path | undefined, groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const results = Object.create(null); const graphqlWrappedResult: GraphQLWrappedResult> = [ @@ -637,7 +624,6 @@ function executeFields( fieldGroup, fieldPath, incrementalContext, - deferMap, ); if (result !== undefined) { @@ -696,7 +682,6 @@ function executeField( fieldGroup: FieldGroup, path: Path, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue> | undefined { const fieldName = fieldGroup[0].node.name.value; const fieldDef = exeContext.schema.getField(parentType, fieldName); @@ -742,7 +727,6 @@ function executeField( path, result, incrementalContext, - deferMap, ); } @@ -754,7 +738,6 @@ function executeField( path, result, incrementalContext, - deferMap, ); if (isPromise(completed)) { @@ -869,7 +852,6 @@ function completeValue( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue> { // If result is an Error, throw a located error. if (result instanceof Error) { @@ -887,7 +869,6 @@ function completeValue( path, result, incrementalContext, - deferMap, ); if ((completed as GraphQLWrappedResult)[0] === null) { throw new Error( @@ -912,7 +893,6 @@ function completeValue( path, result, incrementalContext, - deferMap, ); } @@ -933,7 +913,6 @@ function completeValue( path, result, incrementalContext, - deferMap, ); } @@ -947,7 +926,6 @@ function completeValue( path, result, incrementalContext, - deferMap, ); } /* c8 ignore next 6 */ @@ -966,7 +944,6 @@ async function completePromisedValue( path: Path, result: Promise, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): Promise> { try { const resolved = await result; @@ -978,7 +955,6 @@ async function completePromisedValue( path, resolved, incrementalContext, - deferMap, ); if (isPromise(completed)) { @@ -1083,7 +1059,6 @@ async function completeAsyncIteratorValue( path: Path, asyncIterator: AsyncIterator, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): Promise>> { let containsPromise = false; const completedResults: Array = []; @@ -1164,7 +1139,6 @@ async function completeAsyncIteratorValue( info, itemPath, incrementalContext, - deferMap, ), ); containsPromise = true; @@ -1180,7 +1154,6 @@ async function completeAsyncIteratorValue( info, itemPath, incrementalContext, - deferMap, ) // TODO: add tests for stream backed by asyncIterator that completes to a promise /* c8 ignore start */ @@ -1220,7 +1193,6 @@ function completeListValue( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const itemType = returnType.ofType; @@ -1235,7 +1207,6 @@ function completeListValue( path, asyncIterator, incrementalContext, - deferMap, ); } @@ -1253,7 +1224,6 @@ function completeListValue( path, result, incrementalContext, - deferMap, ); } @@ -1265,7 +1235,6 @@ function completeIterableValue( path: Path, items: Iterable, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // This is specified as a simple map, however we're optimizing the path // where the list contains no Promises by avoiding creating another Promise. @@ -1317,7 +1286,6 @@ function completeIterableValue( info, itemPath, incrementalContext, - deferMap, ), ); containsPromise = true; @@ -1332,7 +1300,6 @@ function completeIterableValue( info, itemPath, incrementalContext, - deferMap, ) ) { containsPromise = true; @@ -1365,7 +1332,6 @@ function completeListItemValue( info: GraphQLResolveInfo, itemPath: Path, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): boolean { try { const completedItem = completeValue( @@ -1376,7 +1342,6 @@ function completeListItemValue( itemPath, item, incrementalContext, - deferMap, ); if (isPromise(completedItem)) { @@ -1429,7 +1394,6 @@ async function completePromisedListItemValue( info: GraphQLResolveInfo, itemPath: Path, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): Promise { try { const resolved = await item; @@ -1441,7 +1405,6 @@ async function completePromisedListItemValue( itemPath, resolved, incrementalContext, - deferMap, ); if (isPromise(completed)) { completed = await completed; @@ -1491,7 +1454,6 @@ function completeAbstractValue( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { const resolveTypeFn = returnType.resolveType ?? exeContext.typeResolver; const contextValue = exeContext.contextValue; @@ -1514,7 +1476,6 @@ function completeAbstractValue( path, result, incrementalContext, - deferMap, ), ); } @@ -1534,7 +1495,6 @@ function completeAbstractValue( path, result, incrementalContext, - deferMap, ); } @@ -1604,7 +1564,6 @@ function completeObjectValue( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather @@ -1624,7 +1583,6 @@ function completeObjectValue( path, result, incrementalContext, - deferMap, ); }); } @@ -1641,7 +1599,6 @@ function completeObjectValue( path, result, incrementalContext, - deferMap, ); } @@ -1656,59 +1613,6 @@ function invalidReturnTypeError( ); } -/** - * Instantiates new DeferredFragmentRecords for the given path within an - * incremental data record, returning an updated map of DeferUsage - * objects to DeferredFragmentRecords. - * - * Note: As defer directives may be used with operations returning lists, - * a DeferUsage object may correspond to many DeferredFragmentRecords. - * - * DeferredFragmentRecord creation includes the following steps: - * 1. The new DeferredFragmentRecord is instantiated at the given path. - * 2. The parent result record is calculated from the given incremental data - * record. - * 3. The IncrementalPublisher is notified that a new DeferredFragmentRecord - * with the calculated parent has been added; the record will be released only - * after the parent has completed. - * - */ -function addNewDeferredFragments( - newDeferUsages: ReadonlyArray, - newDeferMap: Map, - path?: Path | undefined, -): ReadonlyMap { - // For each new deferUsage object: - for (const newDeferUsage of newDeferUsages) { - const parentDeferUsage = newDeferUsage.parentDeferUsage; - - const parent = - parentDeferUsage === undefined - ? undefined - : deferredFragmentRecordFromDeferUsage(parentDeferUsage, newDeferMap); - - // Instantiate the new record. - const deferredFragmentRecord = new DeferredFragmentRecord( - path, - newDeferUsage.label, - parent, - ); - - // Update the map. - newDeferMap.set(newDeferUsage, deferredFragmentRecord); - } - - return newDeferMap; -} - -function deferredFragmentRecordFromDeferUsage( - deferUsage: DeferUsage, - deferMap: ReadonlyMap, -): DeferredFragmentRecord { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return deferMap.get(deferUsage)!; -} - function collectAndExecuteSubfields( exeContext: ExecutionContext, returnType: GraphQLObjectType, @@ -1716,40 +1620,33 @@ function collectAndExecuteSubfields( path: Path, result: unknown, incrementalContext: IncrementalContext | undefined, - deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { // Collect sub-fields to execute to complete this value. - const collectedSubfields = collectSubfields( + const originalGroupedFieldSet = collectSubfields( exeContext, returnType, fieldGroup, + path, ); - let groupedFieldSet = collectedSubfields.groupedFieldSet; - const newDeferUsages = collectedSubfields.newDeferUsages; - if (deferMap === undefined && newDeferUsages.length === 0) { + if ( + !exeContext.encounteredDefer && + !originalGroupedFieldSet.encounteredDefer + ) { return executeFields( exeContext, returnType, result, path, - groupedFieldSet, + originalGroupedFieldSet, incrementalContext, - undefined, ); } - const subExecutionPlan = buildSubExecutionPlan( - groupedFieldSet, + exeContext.encounteredDefer = true; + const { groupedFieldSet, newGroupedFieldSets } = buildSubExecutionPlan( + originalGroupedFieldSet, incrementalContext?.deferUsageSet, ); - groupedFieldSet = subExecutionPlan.groupedFieldSet; - const newGroupedFieldSets = subExecutionPlan.newGroupedFieldSets; - const newDeferMap = addNewDeferredFragments( - newDeferUsages, - new Map(deferMap), - path, - ); - const subFields = executeFields( exeContext, returnType, @@ -1757,7 +1654,6 @@ function collectAndExecuteSubfields( path, groupedFieldSet, incrementalContext, - newDeferMap, ); if (newGroupedFieldSets.size > 0) { @@ -1768,7 +1664,6 @@ function collectAndExecuteSubfields( path, incrementalContext?.deferUsageSet, newGroupedFieldSets, - newDeferMap, ); return withNewExecutionGroups(subFields, newPendingExecutionGroups); @@ -2006,7 +1901,7 @@ function executeSubscription( ); } - const { groupedFieldSet } = collectFields( + const groupedFieldSet = collectFields( schema, fragments, variableValues, @@ -2092,18 +1987,13 @@ function collectExecutionGroups( path: Path | undefined, parentDeferUsages: DeferUsageSet | undefined, newGroupedFieldSets: Map, - deferMap: ReadonlyMap, ): ReadonlyArray { const newPendingExecutionGroups: Array = []; for (const [deferUsageSet, groupedFieldSet] of newGroupedFieldSets) { - const deferredFragmentRecords = getDeferredFragmentRecords( - deferUsageSet, - deferMap, - ); - const pendingExecutionGroup: PendingExecutionGroup = { - deferredFragmentRecords, + deferUsages: deferUsageSet, + path, result: undefined as unknown as BoxedPromiseOrValue, }; @@ -2120,7 +2010,6 @@ function collectExecutionGroups( errors: undefined, deferUsageSet, }, - deferMap, ); if (exeContext.enableEarlyExecution) { @@ -2163,7 +2052,6 @@ function executeExecutionGroup( path: Path | undefined, groupedFieldSet: GroupedFieldSet, incrementalContext: IncrementalContext, - deferMap: ReadonlyMap, ): PromiseOrValue { let result; try { @@ -2174,7 +2062,6 @@ function executeExecutionGroup( path, groupedFieldSet, incrementalContext, - deferMap, ); } catch (error) { return { @@ -2224,15 +2111,6 @@ function buildCompletedExecutionGroup( }; } -function getDeferredFragmentRecords( - deferUsages: DeferUsageSet, - deferMap: ReadonlyMap, -): ReadonlyArray { - return Array.from(deferUsages).map((deferUsage) => - deferredFragmentRecordFromDeferUsage(deferUsage, deferMap), - ); -} - function buildSyncStreamItemQueue( initialItem: PromiseOrValue, initialIndex: number, @@ -2422,7 +2300,6 @@ function completeStreamItem( itemPath, item, incrementalContext, - new Map(), ).then( (resolvedItem) => buildStreamItemResult(incrementalContext.errors, resolvedItem), @@ -2443,7 +2320,6 @@ function completeStreamItem( itemPath, item, incrementalContext, - new Map(), ); } catch (rawError) { handleFieldError( diff --git a/src/execution/types.ts b/src/execution/types.ts index 2c642f2bfb..4a5eaf9149 100644 --- a/src/execution/types.ts +++ b/src/execution/types.ts @@ -7,6 +7,8 @@ import type { GraphQLFormattedError, } from '../error/GraphQLError.js'; +import type { DeferUsage } from './collectFields.js'; + /** * The result of GraphQL execution. * @@ -169,7 +171,7 @@ export interface FormattedCompletedResult { export function isPendingExecutionGroup( incrementalDataRecord: IncrementalDataRecord, ): incrementalDataRecord is PendingExecutionGroup { - return 'deferredFragmentRecords' in incrementalDataRecord; + return 'deferUsages' in incrementalDataRecord; } export type CompletedExecutionGroup = @@ -208,40 +210,9 @@ type ThunkIncrementalResult = | (() => BoxedPromiseOrValue); export interface PendingExecutionGroup { - deferredFragmentRecords: ReadonlyArray; - result: ThunkIncrementalResult; -} - -export type DeliveryGroup = DeferredFragmentRecord | StreamRecord; - -/** @internal */ -export class DeferredFragmentRecord { + deferUsages: ReadonlySet; path: Path | undefined; - label: string | undefined; - id?: string | undefined; - parent: DeferredFragmentRecord | undefined; - pendingExecutionGroups: Set; - successfulExecutionGroups: Set; - children: Set; - - constructor( - path: Path | undefined, - label: string | undefined, - parent: DeferredFragmentRecord | undefined, - ) { - this.path = path; - this.label = label; - this.parent = parent; - this.pendingExecutionGroups = new Set(); - this.successfulExecutionGroups = new Set(); - this.children = new Set(); - } -} - -export function isDeferredFragmentRecord( - deliveryGroup: DeliveryGroup, -): deliveryGroup is DeferredFragmentRecord { - return deliveryGroup instanceof DeferredFragmentRecord; + result: ThunkIncrementalResult; } export interface StreamItemResult { @@ -271,9 +242,9 @@ export interface CancellableStreamRecord extends StreamRecord { } export function isCancellableStreamRecord( - deliveryGroup: DeliveryGroup, -): deliveryGroup is CancellableStreamRecord { - return 'earlyReturn' in deliveryGroup; + streamRecord: StreamRecord, +): streamRecord is CancellableStreamRecord { + return 'earlyReturn' in streamRecord; } export type IncrementalDataRecord = PendingExecutionGroup | StreamRecord; diff --git a/src/jsutils/memoize3.ts b/src/jsutils/memoize3.ts index 213cb95d10..20a7dcbbbf 100644 --- a/src/jsutils/memoize3.ts +++ b/src/jsutils/memoize3.ts @@ -1,15 +1,18 @@ /** - * Memoizes the provided three-argument function. + * Memoizes the provided three-argument or more function based on the first three arguments. */ export function memoize3< A1 extends object, A2 extends object, A3 extends object, + V extends Array, R, ->(fn: (a1: A1, a2: A2, a3: A3) => R): (a1: A1, a2: A2, a3: A3) => R { +>( + fn: (a1: A1, a2: A2, a3: A3, ...rest: V) => R, +): (a1: A1, a2: A2, a3: A3, ...rest: V) => 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 700bc0bda7..9707bfe8be 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -47,7 +47,7 @@ export function SingleFieldSubscriptionsRule( fragments[definition.name.value] = definition; } } - const { groupedFieldSet } = collectFields( + const groupedFieldSet = collectFields( schema, fragments, variableValues,