Skip to content

Commit 3da1cf0

Browse files
committed
deduplicate (mask) leaf values that have already been sent
causes some additional minor changes to `hasNext` property on some payloads due to modification of returned promises resulting in an extra tick
1 parent dd3d9e9 commit 3da1cf0

File tree

3 files changed

+40
-20
lines changed

3 files changed

+40
-20
lines changed

src/execution/__tests__/defer-test.ts

Lines changed: 5 additions & 12 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 {
@@ -650,19 +650,14 @@ describe('Execute: defer directive', () => {
650650
},
651651
{
652652
data: {
653-
deeperObject: {
654-
bar: 'bar',
655-
baz: 'baz',
656-
},
653+
deeperObject: {},
657654
},
658655
path: ['hero', 'nestedObject'],
659656
},
660657
{
661658
data: {
662659
nestedObject: {
663-
deeperObject: {
664-
bar: 'bar',
665-
},
660+
deeperObject: {},
666661
},
667662
},
668663
path: ['hero'],
@@ -732,7 +727,7 @@ describe('Execute: defer directive', () => {
732727
]);
733728
});
734729

735-
it('can deduplicate initial fields with deferred fragments in different branches at multiple non-overlapping levels', async () => {
730+
it('can deduplicate fields with deferred fragments in different branches at multiple non-overlapping levels', async () => {
736731
const document = parse(`
737732
query {
738733
a {
@@ -789,9 +784,7 @@ describe('Execute: defer directive', () => {
789784
data: {
790785
a: {
791786
b: {
792-
e: {
793-
f: 'f',
794-
},
787+
e: {},
795788
},
796789
},
797790
g: {

src/execution/__tests__/stream-test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,9 +1170,6 @@ describe('Execute: stream directive', () => {
11701170
],
11711171
},
11721172
],
1173-
hasNext: true,
1174-
},
1175-
{
11761173
hasNext: false,
11771174
},
11781175
]);
@@ -1355,9 +1352,6 @@ describe('Execute: stream directive', () => {
13551352
],
13561353
},
13571354
],
1358-
hasNext: true,
1359-
},
1360-
{
13611355
hasNext: false,
13621356
},
13631357
]);

src/execution/execute.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export interface ExecutionContext {
124124
errors: Array<GraphQLError>;
125125
subsequentPayloads: Set<AsyncPayloadRecord>;
126126
branches: WeakMap<GroupedFieldSet, Set<string>>;
127+
leaves: Set<string>;
127128
}
128129

129130
/**
@@ -503,6 +504,7 @@ export function buildExecutionContext(
503504
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
504505
subsequentPayloads: new Set(),
505506
branches: new WeakMap(),
507+
leaves: new Set(),
506508
errors: [],
507509
};
508510
}
@@ -516,6 +518,7 @@ function buildPerEventExecutionContext(
516518
rootValue: payload,
517519
subsequentPayloads: new Set(),
518520
branches: new WeakMap(),
521+
leaves: new Set(),
519522
errors: [],
520523
};
521524
}
@@ -703,6 +706,8 @@ function executeFields(
703706
const results = Object.create(null);
704707
let containsPromise = false;
705708

709+
const shouldMask = Object.create(null);
710+
706711
try {
707712
for (const [responseName, fieldSet] of groupedFieldSet) {
708713
const fieldPath = addPath(path, responseName, parentType.name);
@@ -716,6 +721,20 @@ function executeFields(
716721
const returnType = fieldDef.type;
717722

718723
if (shouldExecuteFieldSet(returnType, fieldSet, deferDepth)) {
724+
if (
725+
asyncPayloadRecord !== undefined &&
726+
isLeafType(getNamedType(returnType))
727+
) {
728+
shouldMask[responseName] = () => {
729+
const key = pathToArray(fieldPath).join('.');
730+
if (exeContext.leaves.has(key)) {
731+
return true;
732+
}
733+
exeContext.leaves.add(key);
734+
return false;
735+
};
736+
}
737+
719738
const result = executeField(
720739
exeContext,
721740
parentType,
@@ -748,13 +767,27 @@ function executeFields(
748767

749768
// If there are no promises, we can just return the object
750769
if (!containsPromise) {
751-
return results;
770+
return asyncPayloadRecord === undefined
771+
? results
772+
: new Proxy(results, {
773+
ownKeys: (target) =>
774+
Reflect.ownKeys(target).filter((key) => !shouldMask[key]?.()),
775+
});
752776
}
753777

754778
// Otherwise, results is a map from field name to the result of resolving that
755779
// field, which is possibly a promise. Return a promise that will return this
756780
// same map, but with any promises replaced with the values they resolved to.
757-
return promiseForObject(results);
781+
const promisedResult = promiseForObject(results);
782+
return asyncPayloadRecord === undefined
783+
? promisedResult
784+
: promisedResult.then(
785+
(resolved) =>
786+
new Proxy(resolved, {
787+
ownKeys: (target) =>
788+
Reflect.ownKeys(target).filter((key) => !shouldMask[key]?.()),
789+
}),
790+
);
758791
}
759792

760793
/**

0 commit comments

Comments
 (0)