diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index b36c5c7653..ac1f8f0d72 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -8,6 +8,63 @@ import type { GraphQLFormattedError, } from '../error/GraphQLError.js'; +/** + * The result of GraphQL execution. + * + * - `errors` is included when any errors occurred as a non-empty array. + * - `data` is the result of a successful execution of the query. + * - `hasNext` is true if a future payload is expected. + * - `extensions` is reserved for adding non-standard properties. + * - `incremental` is a list of the results from defer/stream directives. + */ +export interface ExecutionResult< + TData = ObjMap, + TExtensions = ObjMap, +> { + errors?: ReadonlyArray; + data?: TData | null; + extensions?: TExtensions; +} + +export interface FormattedExecutionResult< + TData = ObjMap, + TExtensions = ObjMap, +> { + errors?: ReadonlyArray; + data?: TData | null; + extensions?: TExtensions; +} + +export interface ExperimentalIncrementalExecutionResults< + TData = ObjMap, + TExtensions = ObjMap, +> { + initialResult: InitialIncrementalExecutionResult; + subsequentResults: AsyncGenerator< + SubsequentIncrementalExecutionResult, + void, + void + >; +} + +export interface InitialIncrementalExecutionResult< + TData = ObjMap, + TExtensions = ObjMap, +> extends ExecutionResult { + hasNext: boolean; + incremental?: ReadonlyArray>; + extensions?: TExtensions; +} + +export interface FormattedInitialIncrementalExecutionResult< + TData = ObjMap, + TExtensions = ObjMap, +> extends FormattedExecutionResult { + hasNext: boolean; + incremental?: ReadonlyArray>; + extensions?: TExtensions; +} + export interface SubsequentIncrementalExecutionResult< TData = ObjMap, TExtensions = ObjMap, @@ -113,86 +170,6 @@ export class IncrementalPublisher { this._reset(); } - hasNext(): boolean { - return this._pending.size > 0; - } - - subscribe(): AsyncGenerator< - SubsequentIncrementalExecutionResult, - void, - void - > { - let isDone = false; - - const _next = async (): Promise< - IteratorResult - > => { - // eslint-disable-next-line no-constant-condition - while (true) { - if (isDone) { - return { value: undefined, done: true }; - } - - for (const item of this._released) { - this._pending.delete(item); - } - const released = this._released; - this._released = new Set(); - - const result = this._getIncrementalResult(released); - - if (!this.hasNext()) { - isDone = true; - } - - if (result !== undefined) { - return { value: result, done: false }; - } - - // eslint-disable-next-line no-await-in-loop - await this._signalled; - } - }; - - const returnStreamIterators = async (): Promise => { - const promises: Array>> = []; - this._pending.forEach((incrementalDataRecord) => { - if ( - isStreamItemsRecord(incrementalDataRecord) && - incrementalDataRecord.asyncIterator?.return - ) { - promises.push(incrementalDataRecord.asyncIterator.return()); - } - }); - await Promise.all(promises); - }; - - const _return = async (): Promise< - IteratorResult - > => { - isDone = true; - await returnStreamIterators(); - return { value: undefined, done: true }; - }; - - const _throw = async ( - error?: unknown, - ): Promise> => { - isDone = true; - await returnStreamIterators(); - return Promise.reject(error); - }; - - return { - [Symbol.asyncIterator]() { - return this; - }, - next: _next, - return: _return, - throw: _throw, - }; - } - prepareInitialResultRecord(): InitialResultRecord { return { errors: [], @@ -256,19 +233,38 @@ export class IncrementalPublisher { incrementalDataRecord.errors.push(error); } - publishInitial(initialResult: InitialResultRecord) { - for (const child of initialResult.children) { + buildDataResponse( + initialResultRecord: InitialResultRecord, + data: ObjMap | null, + ): ExecutionResult | ExperimentalIncrementalExecutionResults { + for (const child of initialResultRecord.children) { if (child.filtered) { continue; } this._publish(child); } + + const errors = initialResultRecord.errors; + const initialResult = errors.length === 0 ? { data } : { errors, data }; + if (this._pending.size > 0) { + return { + initialResult: { + ...initialResult, + hasNext: true, + }, + subsequentResults: this._subscribe(), + }; + } + return initialResult; } - getInitialErrors( - initialResult: InitialResultRecord, - ): ReadonlyArray { - return initialResult.errors; + buildErrorResponse( + initialResultRecord: InitialResultRecord, + error: GraphQLError, + ): ExecutionResult { + const errors = initialResultRecord.errors; + errors.push(error); + return { data: null, errors }; } filter(nullPath: Path, erroringIncrementalDataRecord: IncrementalDataRecord) { @@ -301,6 +297,82 @@ export class IncrementalPublisher { }); } + private _subscribe(): AsyncGenerator< + SubsequentIncrementalExecutionResult, + void, + void + > { + let isDone = false; + + const _next = async (): Promise< + IteratorResult + > => { + // eslint-disable-next-line no-constant-condition + while (true) { + if (isDone) { + return { value: undefined, done: true }; + } + + for (const item of this._released) { + this._pending.delete(item); + } + const released = this._released; + this._released = new Set(); + + const result = this._getIncrementalResult(released); + + if (this._pending.size === 0) { + isDone = true; + } + + if (result !== undefined) { + return { value: result, done: false }; + } + + // eslint-disable-next-line no-await-in-loop + await this._signalled; + } + }; + + const returnStreamIterators = async (): Promise => { + const promises: Array>> = []; + this._pending.forEach((incrementalDataRecord) => { + if ( + isStreamItemsRecord(incrementalDataRecord) && + incrementalDataRecord.asyncIterator?.return + ) { + promises.push(incrementalDataRecord.asyncIterator.return()); + } + }); + await Promise.all(promises); + }; + + const _return = async (): Promise< + IteratorResult + > => { + isDone = true; + await returnStreamIterators(); + return { value: undefined, done: true }; + }; + + const _throw = async ( + error?: unknown, + ): Promise> => { + isDone = true; + await returnStreamIterators(); + return Promise.reject(error); + }; + + return { + [Symbol.asyncIterator]() { + return this; + }, + next: _next, + return: _return, + throw: _throw, + }; + } + private _trigger() { this._resolve(); this._reset(); @@ -368,9 +440,10 @@ export class IncrementalPublisher { incrementalResults.push(incrementalResult); } + const hasNext = this._pending.size > 0; return incrementalResults.length - ? { incremental: incrementalResults, hasNext: this.hasNext() } - : encounteredCompletedAsyncIterator && !this.hasNext() + ? { incremental: incrementalResults, hasNext } + : encounteredCompletedAsyncIterator && !hasNext ? { hasNext: false } : undefined; } diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts index e2f8834ca4..c2d749ad25 100644 --- a/src/execution/__tests__/defer-test.ts +++ b/src/execution/__tests__/defer-test.ts @@ -16,9 +16,11 @@ import { import { GraphQLID, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import type { InitialIncrementalExecutionResult } from '../execute.js'; import { execute, experimentalExecuteIncrementally } from '../execute.js'; -import type { SubsequentIncrementalExecutionResult } from '../IncrementalPublisher.js'; +import type { + InitialIncrementalExecutionResult, + SubsequentIncrementalExecutionResult, +} from '../IncrementalPublisher.js'; const friendType = new GraphQLObjectType({ fields: { diff --git a/src/execution/__tests__/lists-test.ts b/src/execution/__tests__/lists-test.ts index ecaf4bb10c..167d580ef5 100644 --- a/src/execution/__tests__/lists-test.ts +++ b/src/execution/__tests__/lists-test.ts @@ -18,8 +18,8 @@ import { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import type { ExecutionResult } from '../execute.js'; import { execute, executeSync } from '../execute.js'; +import type { ExecutionResult } from '../IncrementalPublisher.js'; describe('Execute: Accepts any iterable as list value', () => { function complete(rootValue: unknown) { diff --git a/src/execution/__tests__/nonnull-test.ts b/src/execution/__tests__/nonnull-test.ts index d0b1b614b3..12b223a622 100644 --- a/src/execution/__tests__/nonnull-test.ts +++ b/src/execution/__tests__/nonnull-test.ts @@ -13,8 +13,8 @@ import { GraphQLSchema } from '../../type/schema.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import type { ExecutionResult } from '../execute.js'; import { execute, executeSync } from '../execute.js'; +import type { ExecutionResult } from '../IncrementalPublisher.js'; const syncError = new Error('sync'); const syncNonNullError = new Error('syncNonNull'); diff --git a/src/execution/__tests__/oneof-test.ts b/src/execution/__tests__/oneof-test.ts index acde6031b4..af0e0580ab 100644 --- a/src/execution/__tests__/oneof-test.ts +++ b/src/execution/__tests__/oneof-test.ts @@ -6,8 +6,8 @@ import { parse } from '../../language/parser.js'; import { buildSchema } from '../../utilities/buildASTSchema.js'; -import type { ExecutionResult } from '../execute.js'; import { execute } from '../execute.js'; +import type { ExecutionResult } from '../IncrementalPublisher.js'; const schema = buildSchema(` type Query { diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index ce3b920895..c58d6def85 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -17,9 +17,11 @@ import { import { GraphQLID, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import type { InitialIncrementalExecutionResult } from '../execute.js'; import { experimentalExecuteIncrementally } from '../execute.js'; -import type { SubsequentIncrementalExecutionResult } from '../IncrementalPublisher.js'; +import type { + InitialIncrementalExecutionResult, + SubsequentIncrementalExecutionResult, +} from '../IncrementalPublisher.js'; const friendType = new GraphQLObjectType({ fields: { diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 6a903450d9..eff5032811 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -20,8 +20,9 @@ import { } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import type { ExecutionArgs, ExecutionResult } from '../execute.js'; +import type { ExecutionArgs } from '../execute.js'; import { createSourceEventStream, subscribe } from '../execute.js'; +import type { ExecutionResult } from '../IncrementalPublisher.js'; import { SimplePubSub } from './simplePubSub.js'; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index af68c286e1..8e3db0f59c 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -13,7 +13,6 @@ import { promiseForObject } from '../jsutils/promiseForObject.js'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js'; import { promiseReduce } from '../jsutils/promiseReduce.js'; -import type { GraphQLFormattedError } from '../error/GraphQLError.js'; import { GraphQLError } from '../error/GraphQLError.js'; import { locatedError } from '../error/locatedError.js'; @@ -53,13 +52,12 @@ import { collectSubfields as _collectSubfields, } from './collectFields.js'; import type { - FormattedIncrementalResult, + ExecutionResult, + ExperimentalIncrementalExecutionResults, IncrementalDataRecord, - IncrementalResult, InitialResultRecord, StreamItemsRecord, SubsequentDataRecord, - SubsequentIncrementalExecutionResult, } from './IncrementalPublisher.js'; import { IncrementalPublisher } from './IncrementalPublisher.js'; import { mapAsyncIterable } from './mapAsyncIterable.js'; @@ -133,63 +131,6 @@ export interface ExecutionContext { incrementalPublisher: IncrementalPublisher; } -/** - * The result of GraphQL execution. - * - * - `errors` is included when any errors occurred as a non-empty array. - * - `data` is the result of a successful execution of the query. - * - `hasNext` is true if a future payload is expected. - * - `extensions` is reserved for adding non-standard properties. - * - `incremental` is a list of the results from defer/stream directives. - */ -export interface ExecutionResult< - TData = ObjMap, - TExtensions = ObjMap, -> { - errors?: ReadonlyArray; - data?: TData | null; - extensions?: TExtensions; -} - -export interface FormattedExecutionResult< - TData = ObjMap, - TExtensions = ObjMap, -> { - errors?: ReadonlyArray; - data?: TData | null; - extensions?: TExtensions; -} - -export interface ExperimentalIncrementalExecutionResults< - TData = ObjMap, - TExtensions = ObjMap, -> { - initialResult: InitialIncrementalExecutionResult; - subsequentResults: AsyncGenerator< - SubsequentIncrementalExecutionResult, - void, - void - >; -} - -export interface InitialIncrementalExecutionResult< - TData = ObjMap, - TExtensions = ObjMap, -> extends ExecutionResult { - hasNext: boolean; - incremental?: ReadonlyArray>; - extensions?: TExtensions; -} - -export interface FormattedInitialIncrementalExecutionResult< - TData = ObjMap, - TExtensions = ObjMap, -> extends FormattedExecutionResult { - hasNext: boolean; - incremental?: ReadonlyArray>; - extensions?: TExtensions; -} - export interface ExecutionArgs { schema: GraphQLSchema; document: DocumentNode; @@ -293,49 +234,18 @@ function executeImpl( const incrementalPublisher = exeContext.incrementalPublisher; const initialResultRecord = incrementalPublisher.prepareInitialResultRecord(); try { - const result = executeOperation(exeContext, initialResultRecord); - if (isPromise(result)) { - return result.then( - (data) => { - const errors = - incrementalPublisher.getInitialErrors(initialResultRecord); - const initialResult = buildResponse(data, errors); - incrementalPublisher.publishInitial(initialResultRecord); - if (incrementalPublisher.hasNext()) { - return { - initialResult: { - ...initialResult, - hasNext: true, - }, - subsequentResults: incrementalPublisher.subscribe(), - }; - } - return initialResult; - }, - (error) => { - incrementalPublisher.addFieldError(initialResultRecord, error); - const errors = - incrementalPublisher.getInitialErrors(initialResultRecord); - return buildResponse(null, errors); - }, + const data = executeOperation(exeContext, initialResultRecord); + if (isPromise(data)) { + return data.then( + (resolved) => + incrementalPublisher.buildDataResponse(initialResultRecord, resolved), + (error) => + incrementalPublisher.buildErrorResponse(initialResultRecord, error), ); } - const initialResult = buildResponse(result, initialResultRecord.errors); - incrementalPublisher.publishInitial(initialResultRecord); - if (incrementalPublisher.hasNext()) { - return { - initialResult: { - ...initialResult, - hasNext: true, - }, - subsequentResults: incrementalPublisher.subscribe(), - }; - } - return initialResult; + return incrementalPublisher.buildDataResponse(initialResultRecord, data); } catch (error) { - incrementalPublisher.addFieldError(initialResultRecord, error); - const errors = incrementalPublisher.getInitialErrors(initialResultRecord); - return buildResponse(null, errors); + return incrementalPublisher.buildErrorResponse(initialResultRecord, error); } } @@ -355,17 +265,6 @@ export function executeSync(args: ExecutionArgs): ExecutionResult { return result; } -/** - * Given a completed execution context and data, build the `{ errors, data }` - * response defined by the "Response" section of the GraphQL specification. - */ -function buildResponse( - data: ObjMap | null, - errors: ReadonlyArray, -): ExecutionResult { - return errors.length === 0 ? { data } : { errors, data }; -} - /** * Constructs a ExecutionContext object from the arguments passed to * execute, which we will pass throughout the other execution methods. diff --git a/src/execution/index.ts b/src/execution/index.ts index 3c8581c7b0..9d481ea6af 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -10,20 +10,18 @@ export { subscribe, } from './execute.js'; +export type { ExecutionArgs } from './execute.js'; + export type { - ExecutionArgs, ExecutionResult, ExperimentalIncrementalExecutionResults, InitialIncrementalExecutionResult, - FormattedExecutionResult, - FormattedInitialIncrementalExecutionResult, -} from './execute.js'; - -export type { SubsequentIncrementalExecutionResult, IncrementalDeferResult, IncrementalStreamResult, IncrementalResult, + FormattedExecutionResult, + FormattedInitialIncrementalExecutionResult, FormattedSubsequentIncrementalExecutionResult, FormattedIncrementalDeferResult, FormattedIncrementalStreamResult, diff --git a/src/graphql.ts b/src/graphql.ts index 109bc75d92..0c8187ae0e 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -14,8 +14,8 @@ import { validateSchema } from './type/validate.js'; import { validate } from './validation/validate.js'; -import type { ExecutionResult } from './execution/execute.js'; import { execute } from './execution/execute.js'; +import type { ExecutionResult } from './execution/IncrementalPublisher.js'; /** * This is the primary entry point function for fulfilling GraphQL operations