Skip to content

Commit a6e3522

Browse files
committed
add legacy branching Executor
1 parent 1e84ff1 commit a6e3522

File tree

8 files changed

+6681
-6
lines changed

8 files changed

+6681
-6
lines changed

src/execution/__tests__/executor-test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ import {
4242
} from '../execute.js';
4343
import type { ExecutionResult } from '../Executor.js';
4444
import { collectSubfields, getStreamUsage } from '../Executor.js';
45+
import { legacyExecuteIncrementally } from '../legacyIncremental/legacyExecuteIncrementally.js';
4546

4647
function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
4748
return expectEqualPromisesOrValues([
4849
executeThrowingOnIncremental(args),
4950
executeIgnoringIncremental(args),
5051
experimentalExecuteIncrementally(args),
52+
legacyExecuteIncrementally(args),
5153
]) as PromiseOrValue<ExecutionResult>;
5254
}
5355

@@ -56,6 +58,7 @@ function executeSync(args: ExecutionArgs): ExecutionResult {
5658
executeSyncWrappingThrowingOnIncremental(args),
5759
executeIgnoringIncremental(args),
5860
experimentalExecuteIncrementally(args),
61+
legacyExecuteIncrementally(args),
5962
]) as ExecutionResult;
6063
}
6164

src/execution/incremental/IncrementalExecutor.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,12 @@ export class IncrementalExecutor<
273273
this.streams = [];
274274
}
275275

276+
createSubExecutor(
277+
deferUsageSet?: DeferUsageSet,
278+
): IncrementalExecutor<TExperimental> {
279+
return new IncrementalExecutor(this.validatedExecutionArgs, deferUsageSet);
280+
}
281+
276282
override cancel(reason?: unknown): void {
277283
super.cancel(reason);
278284
for (const task of this.tasks) {
@@ -442,10 +448,7 @@ export class IncrementalExecutor<
442448
for (const [deferUsageSet, groupedFieldSet] of newGroupedFieldSets) {
443449
const deliveryGroups = getDeliveryGroups(deferUsageSet, deliveryGroupMap);
444450

445-
const executor = new IncrementalExecutor(
446-
this.validatedExecutionArgs,
447-
deferUsageSet,
448-
);
451+
const executor = this.createSubExecutor(deferUsageSet);
449452

450453
const executionGroup: ExecutionGroup = {
451454
groups: deliveryGroups,
@@ -708,7 +711,7 @@ export class IncrementalExecutor<
708711

709712
const itemPath = addPath(streamPath, index, undefined);
710713

711-
const executor = new IncrementalExecutor(this.validatedExecutionArgs);
714+
const executor = this.createSubExecutor();
712715

713716
let streamItemResult = executor.completeStreamItem(
714717
itemPath,

src/execution/incremental/__tests__/defer-test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2132,7 +2132,43 @@ describe('Execute: defer directive', () => {
21322132
]);
21332133
});
21342134

2135-
it('Cancels deferred fields when initial result exhibits null bubbling', async () => {
2135+
it('Cancels deferred fields when initial result exhibits null bubbling cancelling the defer', async () => {
2136+
const document = parse(`
2137+
query {
2138+
hero {
2139+
nonNullName
2140+
... @defer {
2141+
name
2142+
}
2143+
}
2144+
}
2145+
`);
2146+
const result = await complete(
2147+
document,
2148+
{
2149+
hero: {
2150+
...hero,
2151+
nonNullName: () => null,
2152+
},
2153+
},
2154+
true,
2155+
);
2156+
expectJSON(result).toDeepEqual({
2157+
data: {
2158+
hero: null,
2159+
},
2160+
errors: [
2161+
{
2162+
message:
2163+
'Cannot return null for non-nullable field Hero.nonNullName.',
2164+
locations: [{ line: 4, column: 11 }],
2165+
path: ['hero', 'nonNullName'],
2166+
},
2167+
],
2168+
});
2169+
});
2170+
2171+
it('Cancels deferred fields when initial result exhibits null bubbling cancelling new fields', async () => {
21362172
const document = parse(`
21372173
query {
21382174
hero {
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { AccumulatorMap } from '../../jsutils/AccumulatorMap.js';
2+
import { getBySet } from '../../jsutils/getBySet.js';
3+
import { invariant } from '../../jsutils/invariant.js';
4+
import { isSameSet } from '../../jsutils/isSameSet.js';
5+
import { memoize1 } from '../../jsutils/memoize1.js';
6+
import { memoize2 } from '../../jsutils/memoize2.js';
7+
import type { ObjMap } from '../../jsutils/ObjMap.js';
8+
9+
import type { GraphQLError } from '../../error/GraphQLError.js';
10+
11+
import type {
12+
DeferUsage,
13+
FieldDetails,
14+
GroupedFieldSet,
15+
} from '../collectFields.js';
16+
import type { ExecutionResult } from '../Executor.js';
17+
import type {
18+
DeferUsageSet,
19+
ExecutionPlan,
20+
} from '../incremental/buildExecutionPlan.js';
21+
import { IncrementalExecutor } from '../incremental/IncrementalExecutor.js';
22+
23+
import { BranchingIncrementalPublisher } from './BranchingIncrementalPublisher.js';
24+
25+
export interface ExperimentalIncrementalExecutionResults {
26+
initialResult: InitialIncrementalExecutionResult;
27+
subsequentResults: AsyncGenerator<
28+
SubsequentIncrementalExecutionResult,
29+
void,
30+
void
31+
>;
32+
}
33+
34+
export interface InitialIncrementalExecutionResult<
35+
TData = ObjMap<unknown>,
36+
TExtensions = ObjMap<unknown>,
37+
> extends ExecutionResult<TData, TExtensions> {
38+
data: TData;
39+
hasNext: true;
40+
extensions?: TExtensions;
41+
}
42+
43+
export interface SubsequentIncrementalExecutionResult<
44+
TData = unknown,
45+
TExtensions = ObjMap<unknown>,
46+
> {
47+
incremental?: ReadonlyArray<IncrementalResult<TData, TExtensions>>;
48+
hasNext: boolean;
49+
extensions?: TExtensions;
50+
}
51+
52+
export type IncrementalResult<TData = unknown, TExtensions = ObjMap<unknown>> =
53+
| IncrementalDeferResult<TData, TExtensions>
54+
| IncrementalStreamResult<TData, TExtensions>;
55+
56+
export interface IncrementalDeferResult<
57+
TData = ObjMap<unknown>,
58+
TExtensions = ObjMap<unknown>,
59+
> extends ExecutionResult<TData, TExtensions> {
60+
path: ReadonlyArray<string | number>;
61+
label?: string;
62+
}
63+
64+
export interface IncrementalStreamResult<
65+
TData = ReadonlyArray<unknown>,
66+
TExtensions = ObjMap<unknown>,
67+
> {
68+
errors?: ReadonlyArray<GraphQLError>;
69+
items: TData | null;
70+
path: ReadonlyArray<string | number>;
71+
label?: string;
72+
extensions?: TExtensions;
73+
}
74+
75+
const buildBranchingExecutionPlanFromInitial = memoize1(
76+
(groupedFieldSet: GroupedFieldSet) =>
77+
buildBranchingExecutionPlan(groupedFieldSet),
78+
);
79+
80+
const buildBranchingExecutionPlanFromDeferred = memoize2(
81+
(groupedFieldSet: GroupedFieldSet, deferUsageSet: DeferUsageSet) =>
82+
buildBranchingExecutionPlan(groupedFieldSet, deferUsageSet),
83+
);
84+
85+
/** @internal */
86+
export class BranchingIncrementalExecutor extends IncrementalExecutor<ExperimentalIncrementalExecutionResults> {
87+
override createSubExecutor(
88+
deferUsageSet?: DeferUsageSet,
89+
): IncrementalExecutor<ExperimentalIncrementalExecutionResults> {
90+
return new BranchingIncrementalExecutor(
91+
this.validatedExecutionArgs,
92+
deferUsageSet,
93+
);
94+
}
95+
96+
override buildResponse(
97+
data: ObjMap<unknown> | null,
98+
): ExecutionResult | ExperimentalIncrementalExecutionResults {
99+
const errors = this.collectedErrors.errors;
100+
const work = this.getIncrementalWork();
101+
const { tasks, streams } = work;
102+
if (tasks?.length === 0 && streams?.length === 0) {
103+
return errors.length ? { errors, data } : { data };
104+
}
105+
106+
invariant(data !== null);
107+
const incrementalPublisher = new BranchingIncrementalPublisher();
108+
return incrementalPublisher.buildResponse(
109+
data,
110+
errors,
111+
work,
112+
this.validatedExecutionArgs.externalAbortSignal,
113+
);
114+
}
115+
116+
override buildRootExecutionPlan(
117+
originalGroupedFieldSet: GroupedFieldSet,
118+
): ExecutionPlan {
119+
return buildBranchingExecutionPlanFromInitial(originalGroupedFieldSet);
120+
}
121+
122+
override buildSubExecutionPlan(
123+
originalGroupedFieldSet: GroupedFieldSet,
124+
): ExecutionPlan {
125+
return this.deferUsageSet === undefined
126+
? buildBranchingExecutionPlanFromInitial(originalGroupedFieldSet)
127+
: buildBranchingExecutionPlanFromDeferred(
128+
originalGroupedFieldSet,
129+
this.deferUsageSet,
130+
);
131+
}
132+
}
133+
134+
function buildBranchingExecutionPlan(
135+
originalGroupedFieldSet: GroupedFieldSet,
136+
parentDeferUsages: DeferUsageSet = new Set<DeferUsage>(),
137+
): ExecutionPlan {
138+
const groupedFieldSet = new AccumulatorMap<string, FieldDetails>();
139+
140+
const newGroupedFieldSets = new Map<
141+
DeferUsageSet,
142+
AccumulatorMap<string, FieldDetails>
143+
>();
144+
145+
for (const [responseKey, fieldGroup] of originalGroupedFieldSet) {
146+
for (const fieldDetails of fieldGroup) {
147+
const deferUsage = fieldDetails.deferUsage;
148+
const deferUsageSet =
149+
deferUsage === undefined
150+
? new Set<DeferUsage>()
151+
: new Set([deferUsage]);
152+
if (isSameSet(parentDeferUsages, deferUsageSet)) {
153+
groupedFieldSet.add(responseKey, fieldDetails);
154+
} else {
155+
let newGroupedFieldSet = getBySet(newGroupedFieldSets, deferUsageSet);
156+
if (newGroupedFieldSet === undefined) {
157+
newGroupedFieldSet = new AccumulatorMap();
158+
newGroupedFieldSets.set(deferUsageSet, newGroupedFieldSet);
159+
}
160+
newGroupedFieldSet.add(responseKey, fieldDetails);
161+
}
162+
}
163+
}
164+
165+
return {
166+
groupedFieldSet,
167+
newGroupedFieldSets,
168+
};
169+
}

0 commit comments

Comments
 (0)