diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index 08208a1b589..233df79c8bb 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -174,7 +174,7 @@ export interface FormattedCompletedResult { export function buildIncrementalResponse( context: IncrementalPublisherContext, result: ObjMap, - errors: ReadonlyArray, + errors: ReadonlyArray | undefined, incrementalDataRecords: ReadonlyArray, ): ExperimentalIncrementalExecutionResults { const incrementalPublisher = new IncrementalPublisher(context); @@ -186,7 +186,7 @@ export function buildIncrementalResponse( } interface IncrementalPublisherContext { - cancellableStreams: Set; + cancellableStreams?: Set | undefined; } /** @@ -220,7 +220,7 @@ class IncrementalPublisher { buildResponse( data: ObjMap, - errors: ReadonlyArray, + errors: ReadonlyArray | undefined, incrementalDataRecords: ReadonlyArray, ): ExperimentalIncrementalExecutionResults { this._addIncrementalDataRecords(incrementalDataRecords); @@ -229,7 +229,7 @@ class IncrementalPublisher { const pending = this._pendingSourcesToResults(); const initialResult: InitialIncrementalExecutionResult = - errors.length === 0 + errors === undefined ? { data, pending, hasNext: true } : { errors, data, pending, hasNext: true }; @@ -441,8 +441,12 @@ class IncrementalPublisher { }; const returnStreamIterators = async (): Promise => { + const cancellableStreams = this._context.cancellableStreams; + if (cancellableStreams === undefined) { + return; + } const promises: Array> = []; - for (const streamRecord of this._context.cancellableStreams) { + for (const streamRecord of cancellableStreams) { if (streamRecord.earlyReturn !== undefined) { promises.push(streamRecord.earlyReturn()); } @@ -516,7 +520,7 @@ class IncrementalPublisher { ); } - if (deferredGroupedFieldSetResult.incrementalDataRecords.length > 0) { + if (deferredGroupedFieldSetResult.incrementalDataRecords !== undefined) { this._addIncrementalDataRecords( deferredGroupedFieldSetResult.incrementalDataRecords, ); @@ -586,14 +590,20 @@ class IncrementalPublisher { if (streamItemsResult.result === undefined) { this._completed.push({ id }); this._pending.delete(streamRecord); - this._context.cancellableStreams.delete(streamRecord); + const cancellableStreams = this._context.cancellableStreams; + if (cancellableStreams !== undefined) { + cancellableStreams.delete(streamRecord); + } } else if (streamItemsResult.result === null) { this._completed.push({ id, errors: streamItemsResult.errors, }); this._pending.delete(streamRecord); - this._context.cancellableStreams.delete(streamRecord); + const cancellableStreams = this._context.cancellableStreams; + if (cancellableStreams !== undefined) { + cancellableStreams.delete(streamRecord); + } streamRecord.earlyReturn?.().catch(() => { /* c8 ignore next 1 */ // ignore error @@ -606,7 +616,7 @@ class IncrementalPublisher { this._incremental.push(incrementalEntry); - if (streamItemsResult.incrementalDataRecords.length > 0) { + if (streamItemsResult.incrementalDataRecords !== undefined) { this._addIncrementalDataRecords( streamItemsResult.incrementalDataRecords, ); @@ -663,7 +673,7 @@ function isDeferredGroupedFieldSetRecord( export interface IncrementalContext { deferUsageSet: DeferUsageSet | undefined; path: Path | undefined; - errors: Array; + errors?: Array | undefined; } export type DeferredGroupedFieldSetResult = @@ -680,7 +690,7 @@ interface ReconcilableDeferredGroupedFieldSetResult { deferredFragmentRecords: ReadonlyArray; path: Array; result: BareDeferredGroupedFieldSetResult; - incrementalDataRecords: ReadonlyArray; + incrementalDataRecords: ReadonlyArray | undefined; sent?: true | undefined; } @@ -718,7 +728,6 @@ export class DeferredGroupedFieldSetRecord { const incrementalContext: IncrementalContext = { deferUsageSet, path, - errors: [], }; for (const deferredFragmentRecord of deferredFragmentRecords) { @@ -786,7 +795,7 @@ interface NonReconcilableStreamItemsResult { interface NonTerminatingStreamItemsResult { streamRecord: StreamRecord; result: BareStreamItemsResult; - incrementalDataRecords: ReadonlyArray; + incrementalDataRecords: ReadonlyArray | undefined; } interface TerminatingStreamItemsResult { @@ -826,7 +835,6 @@ export class StreamItemsRecord { const incrementalContext: IncrementalContext = { deferUsageSet: undefined, path: itemPath, - errors: [], }; this._result = executor(incrementalContext); @@ -850,7 +858,7 @@ export class StreamItemsRecord { ? { ...result, incrementalDataRecords: - result.incrementalDataRecords.length === 0 + result.incrementalDataRecords === undefined ? [this.nextStreamItems] : [this.nextStreamItems, ...result.incrementalDataRecords], } diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 0c3f8034a53..9388ff31b1b 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -140,8 +140,8 @@ export interface ExecutionContext { fieldResolver: GraphQLFieldResolver; typeResolver: GraphQLTypeResolver; subscribeFieldResolver: GraphQLFieldResolver; - errors: Array; - cancellableStreams: Set; + errors: Array | undefined; + cancellableStreams: Set | undefined; } export interface ExecutionArgs { @@ -162,7 +162,7 @@ export interface StreamUsage { fieldGroup: FieldGroup; } -type GraphQLResult = [T, ReadonlyArray]; +type GraphQLResult = [T, ReadonlyArray | undefined]; const UNEXPECTED_EXPERIMENTAL_DIRECTIVES = 'The provided schema unexpectedly contains experimental directives (@defer or @stream). These directives may only be utilized if experimental execution features are explicitly enabled.'; @@ -328,20 +328,20 @@ function executeOperation( } function withError( - errors: Array, + errors: Array | undefined, error: GraphQLError, ): ReadonlyArray { - return errors.length === 0 ? [error] : [...errors, error]; + return errors === undefined ? [error] : [...errors, error]; } function buildDataResponse( exeContext: ExecutionContext, data: ObjMap, - incrementalDataRecords: ReadonlyArray, + incrementalDataRecords: ReadonlyArray | undefined, ): ExecutionResult | ExperimentalIncrementalExecutionResults { const { errors } = exeContext; - if (incrementalDataRecords.length === 0) { - return errors.length > 0 ? { errors, data } : { data }; + if (incrementalDataRecords === undefined) { + return errors !== undefined ? { errors, data } : { data }; } return buildIncrementalResponse( @@ -453,8 +453,8 @@ export function buildExecutionContext( fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, - errors: [], - cancellableStreams: new Set(), + errors: undefined, + cancellableStreams: undefined, }; } @@ -465,7 +465,7 @@ function buildPerEventExecutionContext( return { ...exeContext, rootValue: payload, - errors: [], + errors: undefined, }; } @@ -551,16 +551,16 @@ function executeFieldsSerially( appendNewIncrementalDataRecords(acc, result[1]); return acc; }, - [Object.create(null), []] as GraphQLResult>, + [Object.create(null), undefined] as GraphQLResult>, ); } function appendNewIncrementalDataRecords( acc: GraphQLResult, - newRecords: ReadonlyArray, + newRecords: ReadonlyArray | undefined, ): void { - if (newRecords.length > 0) { - acc[1] = acc[1].length === 0 ? newRecords : [...acc[1], ...newRecords]; + if (newRecords !== undefined) { + acc[1] = acc[1] === undefined ? newRecords : [...acc[1], ...newRecords]; } } @@ -577,7 +577,7 @@ function executeFields( incrementalContext: IncrementalContext | undefined, deferMap: ReadonlyMap | undefined, ): PromiseOrValue>> { - const acc: GraphQLResult> = [Object.create(null), []]; + const acc: GraphQLResult> = [Object.create(null), undefined]; const promises: Array> = []; try { @@ -719,7 +719,7 @@ function executeField( path, incrementalContext, ); - return [null, []]; + return [null, undefined]; }); } return completed; @@ -732,7 +732,7 @@ function executeField( path, incrementalContext, ); - return [null, []]; + return [null, undefined]; } } @@ -781,7 +781,13 @@ function handleFieldError( // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. - (incrementalContext ?? exeContext).errors.push(error); + const context = incrementalContext ?? exeContext; + let errors = context.errors; + if (errors === undefined) { + errors = []; + context.errors = errors; + } + errors.push(error); } /** @@ -843,7 +849,7 @@ function completeValue( // If result value is null or undefined then return null. if (result == null) { - return [null, []]; + return [null, undefined]; } // If field type is List, complete each item in the list with the inner type @@ -863,7 +869,7 @@ function completeValue( // If field type is a leaf type, Scalar or Enum, serialize to a valid value, // returning null if serialization is not possible. if (isLeafType(returnType)) { - return [completeLeafValue(returnType, result), []]; + return [completeLeafValue(returnType, result), undefined]; } // If field type is an abstract type, Interface or Union, determine the @@ -938,7 +944,7 @@ async function completePromisedValue( path, incrementalContext, ); - return [null, []]; + return [null, undefined]; } } @@ -1028,7 +1034,7 @@ async function completeAsyncIteratorValue( ): Promise>> { let containsPromise = false; const completedResults: Array = []; - const acc: GraphQLResult> = [completedResults, []]; + const acc: GraphQLResult> = [completedResults, undefined]; let index = 0; // eslint-disable-next-line no-constant-condition while (true) { @@ -1105,7 +1111,7 @@ async function completeAsyncIteratorValueWithPossibleStream( ): Promise>> { let containsPromise = false; const completedResults: Array = []; - const acc: GraphQLResult> = [completedResults, []]; + const acc: GraphQLResult> = [completedResults, undefined]; let index = 0; const initialCount = streamUsage.initialCount; // eslint-disable-next-line no-constant-condition @@ -1117,6 +1123,9 @@ async function completeAsyncIteratorValueWithPossibleStream( earlyReturn: asyncIterator.return?.bind(asyncIterator), }); + if (exeContext.cancellableStreams === undefined) { + exeContext.cancellableStreams = new Set(); + } exeContext.cancellableStreams.add(streamRecord); const firstStreamItems = firstAsyncStreamItems( @@ -1298,7 +1307,7 @@ function completeIterableValue( // where the list contains no Promises by avoiding creating another Promise. let containsPromise = false; const completedResults: Array = []; - const acc: GraphQLResult> = [completedResults, []]; + const acc: GraphQLResult> = [completedResults, undefined]; let index = 0; for (const item of items) { // No need to modify the info object containing the path, @@ -1359,7 +1368,7 @@ function completeIterableValueWithPossibleStream( // where the list contains no Promises by avoiding creating another Promise. let containsPromise = false; const completedResults: Array = []; - const acc: GraphQLResult> = [completedResults, []]; + const acc: GraphQLResult> = [completedResults, undefined]; let index = 0; const initialCount = streamUsage.initialCount; const iterator = items[Symbol.iterator](); @@ -2330,7 +2339,7 @@ function buildDeferredGroupedFieldSetResult( deferredFragmentRecords, path: pathToArray(path), result: - errors.length === 0 ? { data: result[0] } : { data: result[0], errors }, + errors === undefined ? { data: result[0] } : { data: result[0], errors }, incrementalDataRecords: result[1], }; } @@ -2546,7 +2555,7 @@ function completeStreamItems( itemPath, incrementalContext, ); - return [null, []] as GraphQLResult; + return [null, undefined] as GraphQLResult; }) .then( (resolvedItem) => @@ -2585,7 +2594,7 @@ function completeStreamItems( itemPath, incrementalContext, ); - result = [null, []]; + result = [null, undefined]; } } catch (error) { return { @@ -2606,7 +2615,7 @@ function completeStreamItems( itemPath, incrementalContext, ); - return [null, []] as GraphQLResult; + return [null, undefined] as GraphQLResult; }) .then( (resolvedItem) => @@ -2635,7 +2644,7 @@ function buildStreamItemsResult( return { streamRecord, result: - errors.length === 0 + errors === undefined ? { items: [result[0]] } : { items: [result[0]],