Skip to content

Commit b057818

Browse files
committed
memoize Path generation so paths can be used as keys directly across branches
1 parent 5b6ad7d commit b057818

File tree

6 files changed

+113
-50
lines changed

6 files changed

+113
-50
lines changed

src/execution/__tests__/defer-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ describe('Execute: defer directive', () => {
591591
]);
592592
});
593593

594-
it('Can deduplicate initial fields with deferred fragments at multiple levels', async () => {
594+
it('Can deduplicate fields with deferred fragments at multiple levels', async () => {
595595
const document = parse(`
596596
query {
597597
hero {
@@ -732,7 +732,7 @@ describe('Execute: defer directive', () => {
732732
]);
733733
});
734734

735-
it('can deduplicate initial fields with deferred fragments in different branches at multiple non-overlapping levels', async () => {
735+
it('can deduplicate fields with deferred fragments in different branches at multiple non-overlapping levels', async () => {
736736
const document = parse(`
737737
query {
738738
a {

src/execution/__tests__/executor-test.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import { expectJSON } from '../../__testUtils__/expectJSON.js';
55
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js';
66

77
import { inspect } from '../../jsutils/inspect.js';
8+
import type { Path } from '../../jsutils/Path.js';
89

910
import { Kind } from '../../language/kinds.js';
1011
import { parse } from '../../language/parser.js';
1112

13+
import type { GraphQLResolveInfo } from '../../type/definition.js';
1214
import {
1315
GraphQLInterfaceType,
1416
GraphQLList,
@@ -191,7 +193,7 @@ describe('Execute: Handles basic execution tasks', () => {
191193
});
192194

193195
it('provides info about current execution state', () => {
194-
let resolvedInfo;
196+
let resolvedInfo: GraphQLResolveInfo | undefined;
195197
const testType = new GraphQLObjectType({
196198
name: 'Test',
197199
fields: {
@@ -239,13 +241,18 @@ describe('Execute: Handles basic execution tasks', () => {
239241
const field = operation.selectionSet.selections[0];
240242
expect(resolvedInfo).to.deep.include({
241243
fieldNodes: [field],
242-
path: { prev: undefined, key: 'result', typename: 'Test' },
243244
variableValues: { var: 'abc' },
244245
});
246+
247+
expect(resolvedInfo?.path).to.deep.include({
248+
prev: undefined,
249+
key: 'result',
250+
typename: 'Test',
251+
});
245252
});
246253

247254
it('populates path correctly with complex types', () => {
248-
let path;
255+
let path: Path | undefined;
249256
const someObject = new GraphQLObjectType({
250257
name: 'SomeObject',
251258
fields: {
@@ -288,18 +295,20 @@ describe('Execute: Handles basic execution tasks', () => {
288295

289296
executeSync({ schema, document, rootValue });
290297

291-
expect(path).to.deep.equal({
298+
expect(path).to.deep.include({
292299
key: 'l2',
293300
typename: 'SomeObject',
294-
prev: {
295-
key: 0,
296-
typename: undefined,
297-
prev: {
298-
key: 'l1',
299-
typename: 'SomeQuery',
300-
prev: undefined,
301-
},
302-
},
301+
});
302+
303+
expect(path?.prev).to.deep.include({
304+
key: 0,
305+
typename: undefined,
306+
});
307+
308+
expect(path?.prev?.prev).to.deep.include({
309+
key: 'l1',
310+
typename: 'SomeQuery',
311+
prev: undefined,
303312
});
304313
});
305314

src/execution/execute.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { isPromise } from '../jsutils/isPromise.js';
77
import type { Maybe } from '../jsutils/Maybe.js';
88
import { memoize3 } from '../jsutils/memoize3.js';
99
import type { ObjMap } from '../jsutils/ObjMap.js';
10-
import type { Path } from '../jsutils/Path.js';
11-
import { addPath, pathToArray } from '../jsutils/Path.js';
10+
import type { Path, PathFactory } from '../jsutils/Path.js';
11+
import { createPathFactory, pathToArray } from '../jsutils/Path.js';
1212
import { promiseForObject } from '../jsutils/promiseForObject.js';
1313
import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js';
1414
import { promiseReduce } from '../jsutils/promiseReduce.js';
@@ -122,9 +122,10 @@ export interface ExecutionContext {
122122
fieldResolver: GraphQLFieldResolver<any, any>;
123123
typeResolver: GraphQLTypeResolver<any, any>;
124124
subscribeFieldResolver: GraphQLFieldResolver<any, any>;
125+
addPath: PathFactory;
125126
errors: Array<GraphQLError>;
126127
subsequentPayloads: Set<AsyncPayloadRecord>;
127-
branches: WeakMap<GroupedFieldSet, Set<string>>;
128+
branches: WeakMap<GroupedFieldSet, Set<Path | undefined>>;
128129
}
129130

130131
/**
@@ -502,6 +503,7 @@ export function buildExecutionContext(
502503
fieldResolver: fieldResolver ?? defaultFieldResolver,
503504
typeResolver: typeResolver ?? defaultTypeResolver,
504505
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
506+
addPath: createPathFactory(),
505507
subsequentPayloads: new Set(),
506508
branches: new WeakMap(),
507509
errors: [],
@@ -515,6 +517,7 @@ function buildPerEventExecutionContext(
515517
return {
516518
...exeContext,
517519
rootValue: payload,
520+
addPath: createPathFactory(),
518521
subsequentPayloads: new Set(),
519522
branches: new WeakMap(),
520523
errors: [],
@@ -527,13 +530,12 @@ function shouldBranch(
527530
path: Path | undefined,
528531
): boolean {
529532
const set = exeContext.branches.get(groupedFieldSet);
530-
const key = pathToArray(path).join('.');
531533
if (set === undefined) {
532-
exeContext.branches.set(groupedFieldSet, new Set([key]));
534+
exeContext.branches.set(groupedFieldSet, new Set([path]));
533535
return true;
534536
}
535-
if (!set.has(key)) {
536-
set.add(key);
537+
if (!set.has(path)) {
538+
set.add(path);
537539
return true;
538540
}
539541
return false;
@@ -627,7 +629,7 @@ function executeFieldsSerially(
627629
return promiseReduce(
628630
groupedFieldSet,
629631
(results, [responseName, fieldGroup]) => {
630-
const fieldPath = addPath(path, responseName, parentType.name);
632+
const fieldPath = exeContext.addPath(path, responseName, parentType.name);
631633

632634
const fieldName = fieldGroup[0].fieldNode.name.value;
633635
const fieldDef = exeContext.schema.getField(parentType, fieldName);
@@ -702,7 +704,7 @@ function executeFields(
702704

703705
try {
704706
for (const [responseName, fieldGroup] of groupedFieldSet) {
705-
const fieldPath = addPath(path, responseName, parentType.name);
707+
const fieldPath = exeContext.addPath(path, responseName, parentType.name);
706708

707709
const fieldName = fieldGroup[0].fieldNode.name.value;
708710
const fieldDef = exeContext.schema.getField(parentType, fieldName);
@@ -1141,7 +1143,7 @@ async function completeAsyncIteratorValue(
11411143
break;
11421144
}
11431145

1144-
const itemPath = addPath(path, index, undefined);
1146+
const itemPath = exeContext.addPath(path, index, undefined);
11451147
let iteration;
11461148
try {
11471149
// eslint-disable-next-line no-await-in-loop
@@ -1227,7 +1229,7 @@ function completeListValue(
12271229
for (const item of result) {
12281230
// No need to modify the info object containing the path,
12291231
// since from here on it is not ever accessed by resolver functions.
1230-
const itemPath = addPath(path, index, undefined);
1232+
const itemPath = exeContext.addPath(path, index, undefined);
12311233

12321234
if (
12331235
stream &&
@@ -1815,7 +1817,7 @@ function executeSubscription(
18151817
);
18161818
}
18171819

1818-
const path = addPath(undefined, responseName, rootType.name);
1820+
const path = exeContext.addPath(undefined, responseName, rootType.name);
18191821
const info = buildResolveInfo(
18201822
exeContext,
18211823
fieldDef,
@@ -2100,7 +2102,7 @@ async function executeStreamIterator(
21002102
let previousAsyncPayloadRecord = parentContext ?? undefined;
21012103
// eslint-disable-next-line no-constant-condition
21022104
while (true) {
2103-
const itemPath = addPath(path, index, undefined);
2105+
const itemPath = exeContext.addPath(path, index, undefined);
21042106
const asyncPayloadRecord = new StreamRecord({
21052107
path: itemPath,
21062108
deferDepth,

src/jsutils/Path.ts

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
11
import type { Maybe } from './Maybe.js';
22

3-
export interface Path {
3+
/**
4+
* @internal
5+
*/
6+
export class Path {
47
readonly prev: Path | undefined;
58
readonly key: string | number;
69
readonly typename: string | undefined;
7-
}
810

9-
/**
10-
* Given a Path and a key, return a new Path containing the new key.
11-
*/
12-
export function addPath(
13-
prev: Readonly<Path> | undefined,
14-
key: string | number,
15-
typename: string | undefined,
16-
): Path {
17-
return { prev, key, typename };
11+
readonly _subPaths: Map<string | number, Path>;
12+
13+
constructor(
14+
prev: Path | undefined,
15+
key: string | number,
16+
typename: string | undefined,
17+
) {
18+
this.prev = prev;
19+
this.key = key;
20+
this.typename = typename;
21+
this._subPaths = new Map();
22+
}
23+
24+
/**
25+
* Given a Path and a key, return a new Path containing the new key.
26+
*/
27+
addPath(key: string | number, typeName: string | undefined): Path {
28+
let path = this._subPaths.get(key);
29+
if (path !== undefined) {
30+
return path;
31+
}
32+
33+
path = new Path(this, key, typeName);
34+
this._subPaths.set(key, path);
35+
return path;
36+
}
1837
}
1938

2039
/**
@@ -31,3 +50,27 @@ export function pathToArray(
3150
}
3251
return flattened.reverse();
3352
}
53+
54+
export type PathFactory = (
55+
path: Path | undefined,
56+
key: string | number,
57+
typeName: string | undefined,
58+
) => Path;
59+
60+
export function createPathFactory(): PathFactory {
61+
const paths = new Map<string, Path>();
62+
return (path, key, typeName) => {
63+
if (path !== undefined) {
64+
return path.addPath(key, typeName);
65+
}
66+
67+
let newPath = paths.get(key as string);
68+
if (newPath !== undefined) {
69+
return newPath;
70+
}
71+
72+
newPath = new Path(undefined, key, typeName);
73+
paths.set(key as string, newPath);
74+
return newPath;
75+
};
76+
}

src/jsutils/__tests__/Path-test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

4-
import { addPath, pathToArray } from '../Path.js';
4+
import { Path, pathToArray } from '../Path.js';
55

66
describe('Path', () => {
77
it('can create a Path', () => {
8-
const first = addPath(undefined, 1, 'First');
8+
const first = new Path(undefined, 1, 'First');
99

10-
expect(first).to.deep.equal({
10+
expect(first).to.deep.include({
1111
prev: undefined,
1212
key: 1,
1313
typename: 'First',
1414
});
1515
});
1616

1717
it('can add a new key to an existing Path', () => {
18-
const first = addPath(undefined, 1, 'First');
19-
const second = addPath(first, 'two', 'Second');
18+
const first = new Path(undefined, 1, 'First');
19+
const second = first.addPath('two', 'Second');
2020

21-
expect(second).to.deep.equal({
21+
expect(second).to.deep.include({
2222
prev: first,
2323
key: 'two',
2424
typename: 'Second',
2525
});
2626
});
2727

2828
it('can convert a Path to an array of its keys', () => {
29-
const root = addPath(undefined, 0, 'Root');
30-
const first = addPath(root, 'one', 'First');
31-
const second = addPath(first, 2, 'Second');
29+
const root = new Path(undefined, 0, 'Root');
30+
const first = root.addPath('one', 'First');
31+
const second = first.addPath(2, 'Second');
3232

3333
const path = pathToArray(second);
3434
expect(path).to.deep.equal([0, 'one', 2]);

src/utilities/coerceInputValue.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { inspect } from '../jsutils/inspect.js';
33
import { invariant } from '../jsutils/invariant.js';
44
import { isIterableObject } from '../jsutils/isIterableObject.js';
55
import { isObjectLike } from '../jsutils/isObjectLike.js';
6-
import type { Path } from '../jsutils/Path.js';
7-
import { addPath, pathToArray } from '../jsutils/Path.js';
6+
import { Path, pathToArray } from '../jsutils/Path.js';
87
import { printPathArray } from '../jsutils/printPathArray.js';
98
import { suggestionList } from '../jsutils/suggestionList.js';
109

@@ -48,6 +47,16 @@ function defaultOnError(
4847
throw error;
4948
}
5049

50+
function addPath(
51+
path: Path | undefined,
52+
key: string | number,
53+
typeName: string | undefined,
54+
): Path {
55+
return path
56+
? path.addPath(key, typeName)
57+
: new Path(undefined, key, typeName);
58+
}
59+
5160
function coerceInputValueImpl(
5261
inputValue: unknown,
5362
type: GraphQLInputType,

0 commit comments

Comments
 (0)