Skip to content

Commit 6ec9242

Browse files
committed
represent root path as an object rather than undefined
so that the root path can also be weakly memoized
1 parent 793ab28 commit 6ec9242

File tree

5 files changed

+63
-74
lines changed

5 files changed

+63
-74
lines changed

src/execution/__tests__/executor-test.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ 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';
8+
import type { Path, Root } from '../../jsutils/Path.js';
99

1010
import { Kind } from '../../language/kinds.js';
1111
import { parse } from '../../language/parser.js';
@@ -251,14 +251,13 @@ describe('Execute: Handles basic execution tasks', () => {
251251
]);
252252

253253
expect(resolvedInfo?.path).to.deep.include({
254-
prev: undefined,
255254
key: 'result',
256255
typename: 'Test',
257256
});
258257
});
259258

260259
it('populates path correctly with complex types', () => {
261-
let path: Path | undefined;
260+
let path: Path | Root | undefined;
262261
const someObject = new GraphQLObjectType({
263262
name: 'SomeObject',
264263
fields: {
@@ -306,15 +305,14 @@ describe('Execute: Handles basic execution tasks', () => {
306305
typename: 'SomeObject',
307306
});
308307

309-
expect(path?.prev).to.deep.include({
308+
expect((path as Path).prev).to.deep.include({
310309
key: 0,
311310
typename: undefined,
312311
});
313312

314-
expect(path?.prev?.prev).to.deep.include({
313+
expect(((path as Path).prev as Path).prev).to.deep.include({
315314
key: 'l1',
316315
typename: 'SomeQuery',
317-
prev: undefined,
318316
});
319317
});
320318

src/execution/execute.ts

Lines changed: 19 additions & 19 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, PathFactory } from '../jsutils/Path.js';
11-
import { createPathFactory, pathToArray } from '../jsutils/Path.js';
10+
import type { Path } from '../jsutils/Path.js';
11+
import { pathToArray, Root } 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';
@@ -121,10 +121,10 @@ export interface ExecutionContext {
121121
fieldResolver: GraphQLFieldResolver<any, any>;
122122
typeResolver: GraphQLTypeResolver<any, any>;
123123
subscribeFieldResolver: GraphQLFieldResolver<any, any>;
124-
addPath: PathFactory;
124+
root: Root;
125125
errors: Array<GraphQLError>;
126126
subsequentPayloads: Set<AsyncPayloadRecord>;
127-
branches: WeakMap<GroupedFieldSet, Set<Path | undefined>>;
127+
branches: WeakMap<GroupedFieldSet, Set<Path | Root>>;
128128
}
129129

130130
/**
@@ -502,7 +502,7 @@ export function buildExecutionContext(
502502
fieldResolver: fieldResolver ?? defaultFieldResolver,
503503
typeResolver: typeResolver ?? defaultTypeResolver,
504504
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
505-
addPath: createPathFactory(),
505+
root: new Root(),
506506
subsequentPayloads: new Set(),
507507
branches: new WeakMap(),
508508
errors: [],
@@ -516,7 +516,7 @@ function buildPerEventExecutionContext(
516516
return {
517517
...exeContext,
518518
rootValue: payload,
519-
addPath: createPathFactory(),
519+
root: new Root(),
520520
subsequentPayloads: new Set(),
521521
branches: new WeakMap(),
522522
errors: [],
@@ -526,7 +526,7 @@ function buildPerEventExecutionContext(
526526
function shouldBranch(
527527
groupedFieldSet: GroupedFieldSet,
528528
exeContext: ExecutionContext,
529-
path: Path | undefined,
529+
path: Path | Root,
530530
): boolean {
531531
const set = exeContext.branches.get(groupedFieldSet);
532532
if (set === undefined) {
@@ -563,7 +563,7 @@ function executeOperation(
563563
rootType,
564564
operation,
565565
);
566-
const path = undefined;
566+
const path = new Root();
567567
let result;
568568

569569
switch (operation.operation) {
@@ -622,13 +622,13 @@ function executeFieldsSerially(
622622
exeContext: ExecutionContext,
623623
parentType: GraphQLObjectType,
624624
sourceValue: unknown,
625-
path: Path | undefined,
625+
path: Path | Root,
626626
groupedFieldSet: GroupedFieldSet,
627627
): PromiseOrValue<ObjMap<unknown>> {
628628
return promiseReduce(
629629
groupedFieldSet,
630630
(results, [responseName, fieldGroup]) => {
631-
const fieldPath = exeContext.addPath(path, responseName, parentType.name);
631+
const fieldPath = path.addPath(responseName, parentType.name);
632632

633633
if (!shouldExecute(fieldGroup)) {
634634
return results;
@@ -672,7 +672,7 @@ function executeFields(
672672
exeContext: ExecutionContext,
673673
parentType: GraphQLObjectType,
674674
sourceValue: unknown,
675-
path: Path | undefined,
675+
path: Path | Root,
676676
groupedFieldSet: GroupedFieldSet,
677677
asyncPayloadRecord?: AsyncPayloadRecord,
678678
): PromiseOrValue<ObjMap<unknown>> {
@@ -681,7 +681,7 @@ function executeFields(
681681

682682
try {
683683
for (const [responseName, fieldGroup] of groupedFieldSet) {
684-
const fieldPath = exeContext.addPath(path, responseName, parentType.name);
684+
const fieldPath = path.addPath(responseName, parentType.name);
685685

686686
if (shouldExecute(fieldGroup, asyncPayloadRecord?.deferDepth)) {
687687
const result = executeField(
@@ -1121,7 +1121,7 @@ async function completeAsyncIteratorValue(
11211121
break;
11221122
}
11231123

1124-
const itemPath = exeContext.addPath(path, index, undefined);
1124+
const itemPath = path.addPath(index, undefined);
11251125
let iteration;
11261126
try {
11271127
// eslint-disable-next-line no-await-in-loop
@@ -1207,7 +1207,7 @@ function completeListValue(
12071207
for (const item of result) {
12081208
// No need to modify the info object containing the path,
12091209
// since from here on it is not ever accessed by resolver functions.
1210-
const itemPath = exeContext.addPath(path, index, undefined);
1210+
const itemPath = path.addPath(index, undefined);
12111211

12121212
if (
12131213
stream &&
@@ -1798,7 +1798,7 @@ function executeSubscription(
17981798
);
17991799
}
18001800

1801-
const path = exeContext.addPath(undefined, responseName, rootType.name);
1801+
const path = exeContext.root.addPath(responseName, rootType.name);
18021802
const info = buildResolveInfo(
18031803
exeContext,
18041804
fieldDef,
@@ -1859,7 +1859,7 @@ function executeDeferredFragment(
18591859
sourceValue: unknown,
18601860
groupedFieldSet: GroupedFieldSet,
18611861
newDeferDepth: number,
1862-
path?: Path,
1862+
path: Path | Root,
18631863
parentContext?: AsyncPayloadRecord,
18641864
): void {
18651865
const asyncPayloadRecord = new DeferredFragmentRecord({
@@ -2079,7 +2079,7 @@ async function executeStreamIterator(
20792079
let previousAsyncPayloadRecord = parentContext ?? undefined;
20802080
// eslint-disable-next-line no-constant-condition
20812081
while (true) {
2082-
const itemPath = exeContext.addPath(path, index, undefined);
2082+
const itemPath = path.addPath(index, undefined);
20832083
const asyncPayloadRecord = new StreamRecord({
20842084
path: itemPath,
20852085
deferDepth,
@@ -2282,7 +2282,7 @@ class DeferredFragmentRecord {
22822282
_exeContext: ExecutionContext;
22832283
_resolve?: (arg: PromiseOrValue<ObjMap<unknown> | null>) => void;
22842284
constructor(opts: {
2285-
path: Path | undefined;
2285+
path: Path | Root;
22862286
deferDepth: number | undefined;
22872287
parentContext: AsyncPayloadRecord | undefined;
22882288
exeContext: ExecutionContext;
@@ -2330,7 +2330,7 @@ class StreamRecord {
23302330
_exeContext: ExecutionContext;
23312331
_resolve?: (arg: PromiseOrValue<Array<unknown> | null>) => void;
23322332
constructor(opts: {
2333-
path: Path | undefined;
2333+
path: Path | Root;
23342334
deferDepth: number | undefined;
23352335
iterator?: AsyncIterator<unknown>;
23362336
parentContext: AsyncPayloadRecord | undefined;

src/jsutils/Path.ts

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,40 @@
1-
import type { Maybe } from './Maybe.js';
1+
/**
2+
* @internal
3+
*/
4+
export class Root {
5+
readonly _subPaths: Map<string | number, Path>;
6+
7+
constructor() {
8+
this._subPaths = new Map();
9+
}
10+
11+
/**
12+
* Given a Path and a key, return a new Path containing the new key.
13+
*/
14+
addPath(key: string | number, typeName: string | undefined): Path {
15+
let path = this._subPaths.get(key);
16+
if (path !== undefined) {
17+
return path;
18+
}
19+
20+
path = new Path(this, key, typeName);
21+
this._subPaths.set(key, path);
22+
return path;
23+
}
24+
}
225

326
/**
427
* @internal
528
*/
629
export class Path {
7-
readonly prev: Path | undefined;
30+
readonly prev: Path | Root;
831
readonly key: string | number;
932
readonly typename: string | undefined;
1033

1134
readonly _subPaths: Map<string | number, Path>;
1235

1336
constructor(
14-
prev: Path | undefined,
37+
prev: Path | Root,
1538
key: string | number,
1639
typename: string | undefined,
1740
) {
@@ -40,37 +63,13 @@ export class Path {
4063
* Given a Path, return an Array of the path keys.
4164
*/
4265
export function pathToArray(
43-
path: Maybe<Readonly<Path>>,
66+
path: Readonly<Path | Root>,
4467
): Array<string | number> {
4568
const flattened = [];
4669
let curr = path;
47-
while (curr) {
70+
while (curr instanceof Path) {
4871
flattened.push(curr.key);
4972
curr = curr.prev;
5073
}
5174
return flattened.reverse();
5275
}
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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

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

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

1011
expect(first).to.deep.include({
11-
prev: undefined,
12+
prev: root,
1213
key: 1,
1314
typename: 'First',
1415
});
1516
});
1617

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

2122
expect(second).to.deep.include({
@@ -26,7 +27,7 @@ describe('Path', () => {
2627
});
2728

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

src/utilities/coerceInputValue.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ 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 { Path, pathToArray } from '../jsutils/Path.js';
6+
import type { Path } from '../jsutils/Path.js';
7+
import { pathToArray, Root } from '../jsutils/Path.js';
78
import { printPathArray } from '../jsutils/printPathArray.js';
89
import { suggestionList } from '../jsutils/suggestionList.js';
910

@@ -31,7 +32,7 @@ export function coerceInputValue(
3132
type: GraphQLInputType,
3233
onError: OnErrorCB = defaultOnError,
3334
): unknown {
34-
return coerceInputValueImpl(inputValue, type, onError, undefined);
35+
return coerceInputValueImpl(inputValue, type, onError, new Root());
3536
}
3637

3738
function defaultOnError(
@@ -47,21 +48,11 @@ function defaultOnError(
4748
throw error;
4849
}
4950

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-
6051
function coerceInputValueImpl(
6152
inputValue: unknown,
6253
type: GraphQLInputType,
6354
onError: OnErrorCB,
64-
path: Path | undefined,
55+
path: Path | Root,
6556
): unknown {
6657
if (isNonNullType(type)) {
6758
if (inputValue != null) {
@@ -86,7 +77,7 @@ function coerceInputValueImpl(
8677
const itemType = type.ofType;
8778
if (isIterableObject(inputValue)) {
8879
return Array.from(inputValue, (itemValue, index) => {
89-
const itemPath = addPath(path, index, undefined);
80+
const itemPath = path.addPath(index, undefined);
9081
return coerceInputValueImpl(itemValue, itemType, onError, itemPath);
9182
});
9283
}
@@ -130,7 +121,7 @@ function coerceInputValueImpl(
130121
fieldValue,
131122
field.type,
132123
onError,
133-
addPath(path, field.name, type.name),
124+
path.addPath(field.name, type.name),
134125
);
135126
}
136127

0 commit comments

Comments
 (0)