diff --git a/src/execution/IncrementalPublisher.ts b/src/execution/IncrementalPublisher.ts index 8e40c90819..9dc4bc957a 100644 --- a/src/execution/IncrementalPublisher.ts +++ b/src/execution/IncrementalPublisher.ts @@ -11,6 +11,7 @@ import type { import type { GroupedFieldSet } from './collectFields.js'; interface IncrementalUpdate> { + pending: ReadonlyArray; incremental: ReadonlyArray>; completed: ReadonlyArray; } @@ -59,6 +60,7 @@ export interface InitialIncrementalExecutionResult< TExtensions = ObjMap, > extends ExecutionResult { data: TData; + pending: ReadonlyArray; hasNext: true; extensions?: TExtensions; } @@ -68,6 +70,7 @@ export interface FormattedInitialIncrementalExecutionResult< TExtensions = ObjMap, > extends FormattedExecutionResult { data: TData; + pending: ReadonlyArray; hasNext: boolean; extensions?: TExtensions; } @@ -85,6 +88,7 @@ export interface FormattedSubsequentIncrementalExecutionResult< TExtensions = ObjMap, > { hasNext: boolean; + pending?: ReadonlyArray; incremental?: ReadonlyArray>; completed?: ReadonlyArray; extensions?: TExtensions; @@ -141,6 +145,11 @@ export type FormattedIncrementalResult< | FormattedIncrementalDeferResult | FormattedIncrementalStreamResult; +export interface PendingResult { + path: ReadonlyArray; + label?: string; +} + export interface CompletedResult { path: ReadonlyArray; label?: string; @@ -296,10 +305,20 @@ export class IncrementalPublisher { const errors = initialResultRecord.errors; const initialResult = errors.length === 0 ? { data } : { errors, data }; - if (this._pending.size > 0) { + const pending = this._pending; + if (pending.size > 0) { + const pendingSources = new Set(); + for (const subsequentResultRecord of pending) { + const pendingSource = isStreamItemsRecord(subsequentResultRecord) + ? subsequentResultRecord.streamRecord + : subsequentResultRecord; + pendingSources.add(pendingSource); + } + return { initialResult: { ...initialResult, + pending: this.pendingSourcesToResults(pendingSources), hasNext: true, }, subsequentResults: this._subscribe(), @@ -347,6 +366,23 @@ export class IncrementalPublisher { }); } + pendingSourcesToResults( + pendingSources: ReadonlySet, + ): Array { + const pendingResults: Array = []; + for (const pendingSource of pendingSources) { + pendingSource.pendingSent = true; + const pendingResult: PendingResult = { + path: pendingSource.path, + }; + if (pendingSource.label !== undefined) { + pendingResult.label = pendingSource.label; + } + pendingResults.push(pendingResult); + } + return pendingResults; + } + private _subscribe(): AsyncGenerator< SubsequentIncrementalExecutionResult, void, @@ -461,7 +497,8 @@ export class IncrementalPublisher { private _getIncrementalResult( completedRecords: ReadonlySet, ): SubsequentIncrementalExecutionResult | undefined { - const { incremental, completed } = this._processPending(completedRecords); + const { pending, incremental, completed } = + this._processPending(completedRecords); const hasNext = this._pending.size > 0; if (incremental.length === 0 && completed.length === 0 && hasNext) { @@ -469,6 +506,9 @@ export class IncrementalPublisher { } const result: SubsequentIncrementalExecutionResult = { hasNext }; + if (pending.length) { + result.pending = pending; + } if (incremental.length) { result.incremental = incremental; } @@ -482,6 +522,7 @@ export class IncrementalPublisher { private _processPending( completedRecords: ReadonlySet, ): IncrementalUpdate { + const newPendingSources = new Set(); const incrementalResults: Array = []; const completedResults: Array = []; for (const subsequentResultRecord of completedRecords) { @@ -489,10 +530,17 @@ export class IncrementalPublisher { if (child.filtered) { continue; } + const pendingSource = isStreamItemsRecord(child) + ? child.streamRecord + : child; + if (!pendingSource.pendingSent) { + newPendingSources.add(pendingSource); + } this._publish(child); } if (isStreamItemsRecord(subsequentResultRecord)) { if (subsequentResultRecord.isFinalRecord) { + newPendingSources.delete(subsequentResultRecord.streamRecord); completedResults.push( this._completedRecordToResult(subsequentResultRecord.streamRecord), ); @@ -513,6 +561,7 @@ export class IncrementalPublisher { } incrementalResults.push(incrementalResult); } else { + newPendingSources.delete(subsequentResultRecord); completedResults.push( this._completedRecordToResult(subsequentResultRecord), ); @@ -537,6 +586,7 @@ export class IncrementalPublisher { } return { + pending: this.pendingSourcesToResults(newPendingSources), incremental: incrementalResults, completed: completedResults, }; @@ -690,6 +740,7 @@ export class DeferredFragmentRecord { deferredGroupedFieldSetRecords: Set; errors: Array; filtered: boolean; + pendingSent?: boolean; _pending: Set; constructor(opts: { path: Path | undefined; label: string | undefined }) { @@ -709,6 +760,7 @@ export class StreamRecord { path: ReadonlyArray; errors: Array; earlyReturn?: (() => Promise) | undefined; + pendingSent?: boolean; constructor(opts: { label: string | undefined; path: Path; diff --git a/src/execution/__tests__/defer-test.ts b/src/execution/__tests__/defer-test.ts index 7fca565f31..33c310523b 100644 --- a/src/execution/__tests__/defer-test.ts +++ b/src/execution/__tests__/defer-test.ts @@ -176,6 +176,7 @@ describe('Execute: defer directive', () => { id: '1', }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -231,6 +232,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -261,6 +263,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [{ path: [], label: 'DeferQuery' }], hasNext: true, }, { @@ -302,6 +305,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [{ path: [], label: 'DeferQuery' }], hasNext: true, }, { @@ -351,6 +355,10 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [ + { path: ['hero'], label: 'DeferTop' }, + { path: ['hero'], label: 'DeferNested' }, + ], hasNext: true, }, { @@ -396,6 +404,7 @@ describe('Execute: defer directive', () => { name: 'Luke', }, }, + pending: [{ path: ['hero'], label: 'DeferTop' }], hasNext: true, }, { @@ -424,6 +433,7 @@ describe('Execute: defer directive', () => { name: 'Luke', }, }, + pending: [{ path: ['hero'], label: 'DeferTop' }], hasNext: true, }, { @@ -449,6 +459,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'], label: 'InlineDeferred' }], hasNext: true, }, { @@ -478,6 +489,7 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -506,6 +518,10 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [ + { path: ['hero'], label: 'DeferID' }, + { path: ['hero'], label: 'DeferName' }, + ], hasNext: true, }, { @@ -551,6 +567,10 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [ + { path: [], label: 'DeferID' }, + { path: [], label: 'DeferName' }, + ], hasNext: true, }, { @@ -601,6 +621,10 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [ + { path: [], label: 'DeferID' }, + { path: [], label: 'DeferName' }, + ], hasNext: true, }, { @@ -648,6 +672,10 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [ + { path: [], label: 'DeferName' }, + { path: ['hero'], label: 'DeferID' }, + ], hasNext: true, }, { @@ -691,9 +719,11 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [{ path: [], label: 'DeferName' }], hasNext: true, }, { + pending: [{ path: ['hero'], label: 'DeferID' }], incremental: [ { data: { @@ -753,6 +783,20 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { friends: [{}, {}, {}] } }, + pending: [ + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 2] }, + { path: ['hero', 'friends', 2] }, + { path: ['hero', 'friends', 2] }, + { path: ['hero', 'friends', 2] }, + ], hasNext: true, }, { @@ -831,6 +875,7 @@ describe('Execute: defer directive', () => { }, }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -872,9 +917,11 @@ describe('Execute: defer directive', () => { data: { hero: {}, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { + pending: [{ path: ['hero', 'nestedObject', 'deeperObject'] }], incremental: [ { data: { @@ -954,9 +1001,11 @@ describe('Execute: defer directive', () => { }, }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { + pending: [{ path: ['hero', 'nestedObject'] }], incremental: [ { data: { bar: 'bar' }, @@ -967,6 +1016,7 @@ describe('Execute: defer directive', () => { hasNext: true, }, { + pending: [{ path: ['hero', 'nestedObject', 'deeperObject'] }], incremental: [ { data: { baz: 'baz' }, @@ -1025,9 +1075,14 @@ describe('Execute: defer directive', () => { }, }, }, + pending: [ + { path: ['hero'] }, + { path: ['hero', 'nestedObject', 'deeperObject'] }, + ], hasNext: true, }, { + pending: [{ path: ['hero', 'nestedObject', 'deeperObject'] }], incremental: [ { data: { @@ -1106,6 +1161,7 @@ describe('Execute: defer directive', () => { }, }, }, + pending: [{ path: [] }, { path: ['a', 'b'] }], hasNext: true, }, { @@ -1157,6 +1213,7 @@ describe('Execute: defer directive', () => { data: { a: {}, }, + pending: [{ path: [] }, { path: ['a'] }], hasNext: true, }, { @@ -1224,6 +1281,7 @@ describe('Execute: defer directive', () => { data: { a: {}, }, + pending: [{ path: [] }, { path: ['a'] }], hasNext: true, }, { @@ -1299,6 +1357,7 @@ describe('Execute: defer directive', () => { data: { a: {}, }, + pending: [{ path: [] }, { path: ['a'] }], hasNext: true, }, { @@ -1388,6 +1447,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: {}, + pending: [{ path: [] }], hasNext: true, }, { @@ -1436,6 +1496,7 @@ describe('Execute: defer directive', () => { friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }], }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1471,6 +1532,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { friends: [{ name: 'Han' }] } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1507,6 +1569,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { friends: [] } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1539,6 +1602,7 @@ describe('Execute: defer directive', () => { friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }], }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1586,6 +1650,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { friends: [] } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1619,6 +1684,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { nestedObject: null } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1651,6 +1717,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { nestedObject: { name: 'foo' } } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1683,6 +1750,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1725,6 +1793,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1803,6 +1872,7 @@ describe('Execute: defer directive', () => { expectJSON(result).toDeepEqual([ { data: { hero: { id: '1' } }, + pending: [{ path: ['hero'] }], hasNext: true, }, { @@ -1855,9 +1925,15 @@ describe('Execute: defer directive', () => { data: { hero: { id: '1' }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { + pending: [ + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 2] }, + ], incremental: [ { data: { name: 'slow', friends: [{}, {}, {}] }, @@ -1906,9 +1982,15 @@ describe('Execute: defer directive', () => { data: { hero: { id: '1' }, }, + pending: [{ path: ['hero'] }], hasNext: true, }, { + pending: [ + { path: ['hero', 'friends', 0] }, + { path: ['hero', 'friends', 1] }, + { path: ['hero', 'friends', 2] }, + ], incremental: [ { data: { diff --git a/src/execution/__tests__/mutations-test.ts b/src/execution/__tests__/mutations-test.ts index 64262ea020..13003f7d6b 100644 --- a/src/execution/__tests__/mutations-test.ts +++ b/src/execution/__tests__/mutations-test.ts @@ -237,6 +237,7 @@ describe('Execute: Handles mutation execution ordering', () => { first: {}, second: { theNumber: 2 }, }, + pending: [{ path: ['first'], label: 'defer-label' }], hasNext: true, }, { @@ -312,6 +313,7 @@ describe('Execute: Handles mutation execution ordering', () => { data: { second: { theNumber: 2 }, }, + pending: [{ path: [], label: 'defer-label' }], hasNext: true, }, { diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index be1e96be5a..12d4ddd43f 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -142,6 +142,7 @@ describe('Execute: stream directive', () => { data: { scalarList: ['apple'], }, + pending: [{ path: ['scalarList'] }], hasNext: true, }, { @@ -165,6 +166,7 @@ describe('Execute: stream directive', () => { data: { scalarList: [], }, + pending: [{ path: ['scalarList'] }], hasNext: true, }, { @@ -217,6 +219,7 @@ describe('Execute: stream directive', () => { data: { scalarList: ['apple'], }, + pending: [{ path: ['scalarList'], label: 'scalar-stream' }], hasNext: true, }, { @@ -261,6 +264,7 @@ describe('Execute: stream directive', () => { expectJSON(result).toDeepEqual([ { data: { scalarList: ['apple', 'banana'] }, + pending: [{ path: ['scalarList'] }], hasNext: true, }, { @@ -284,6 +288,7 @@ describe('Execute: stream directive', () => { data: { scalarListList: [['apple', 'apple', 'apple']], }, + pending: [{ path: ['scalarListList'] }], hasNext: true, }, { @@ -333,6 +338,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -369,6 +375,7 @@ describe('Execute: stream directive', () => { data: { friendList: [], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -431,6 +438,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -480,6 +488,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ name: 'Luke', id: '1' }, null], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -517,6 +526,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ name: 'Luke', id: '1' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -568,6 +578,7 @@ describe('Execute: stream directive', () => { data: { friendList: [], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -627,6 +638,7 @@ describe('Execute: stream directive', () => { { name: 'Han', id: '2' }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -696,6 +708,7 @@ describe('Execute: stream directive', () => { { name: 'Han', id: '2' }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, }, @@ -769,6 +782,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ name: 'Luke', id: '1' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -805,6 +819,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ name: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -851,6 +866,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ name: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -885,6 +901,7 @@ describe('Execute: stream directive', () => { data: { scalarList: ['Luke'], }, + pending: [{ path: ['scalarList'] }], hasNext: true, }, { @@ -928,6 +945,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -978,6 +996,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -1030,6 +1049,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1069,6 +1089,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1110,6 +1131,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -1167,6 +1189,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1234,6 +1257,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1311,6 +1335,7 @@ describe('Execute: stream directive', () => { data: { nonNullFriendList: [{ nonNullName: 'Luke' }], }, + pending: [{ path: ['nonNullFriendList'] }], hasNext: true, }, { @@ -1426,6 +1451,10 @@ describe('Execute: stream directive', () => { otherNestedObject: {}, nestedObject: { nestedFriendList: [] }, }, + pending: [ + { path: ['otherNestedObject'] }, + { path: ['nestedObject', 'nestedFriendList'] }, + ], hasNext: true, }, { @@ -1485,6 +1514,7 @@ describe('Execute: stream directive', () => { data: { nestedObject: {}, }, + pending: [{ path: ['nestedObject'] }], hasNext: true, }, { @@ -1537,6 +1567,7 @@ describe('Execute: stream directive', () => { data: { friendList: [], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -1627,6 +1658,7 @@ describe('Execute: stream directive', () => { data: { nestedObject: {}, }, + pending: [{ path: ['nestedObject'] }], hasNext: true, }); @@ -1688,6 +1720,7 @@ describe('Execute: stream directive', () => { data: { friendList: [{ id: '1', name: 'Luke' }], }, + pending: [{ path: ['friendList'] }], hasNext: true, }, { @@ -1747,6 +1780,10 @@ describe('Execute: stream directive', () => { nestedFriendList: [], }, }, + pending: [ + { path: ['nestedObject'] }, + { path: ['nestedObject', 'nestedFriendList'] }, + ], hasNext: true, }, { @@ -1811,6 +1848,7 @@ describe('Execute: stream directive', () => { data: { nestedObject: {}, }, + pending: [{ path: ['nestedObject'] }], hasNext: true, }); @@ -1819,6 +1857,7 @@ describe('Execute: stream directive', () => { const result2 = await result2Promise; expectJSON(result2).toDeepEqual({ value: { + pending: [{ path: ['nestedObject', 'nestedFriendList'] }], incremental: [ { data: { scalarField: 'slow', nestedFriendList: [] }, @@ -1912,6 +1951,10 @@ describe('Execute: stream directive', () => { data: { friendList: [{ id: '1' }], }, + pending: [ + { path: ['friendList', 0], label: 'DeferName' }, + { path: ['friendList'], label: 'stream-label' }, + ], hasNext: true, }); @@ -1920,6 +1963,7 @@ describe('Execute: stream directive', () => { const result2 = await result2Promise; expectJSON(result2).toDeepEqual({ value: { + pending: [{ path: ['friendList', 1], label: 'DeferName' }], incremental: [ { data: { name: 'Luke' }, @@ -2008,6 +2052,10 @@ describe('Execute: stream directive', () => { data: { friendList: [{ id: '1' }], }, + pending: [ + { path: ['friendList', 0], label: 'DeferName' }, + { path: ['friendList'], label: 'stream-label' }, + ], hasNext: true, }); @@ -2016,6 +2064,7 @@ describe('Execute: stream directive', () => { const result2 = await result2Promise; expectJSON(result2).toDeepEqual({ value: { + pending: [{ path: ['friendList', 1], label: 'DeferName' }], incremental: [ { data: { name: 'Luke' }, @@ -2111,6 +2160,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList', 0] }, { path: ['friendList'] }], hasNext: true, }); const returnPromise = iterator.return(); @@ -2166,6 +2216,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList'] }], hasNext: true, }); @@ -2225,6 +2276,7 @@ describe('Execute: stream directive', () => { }, ], }, + pending: [{ path: ['friendList', 0] }, { path: ['friendList'] }], hasNext: true, });