Skip to content

Commit 1564174

Browse files
authored
enhancement: remove extra ticks (#3754)
Promise resolution order changes in some instances, resulting in different orders for some errors within the errors array, as well as in different values of hasNext within incremental delivery payloads. This PR introduces an async `completePromisedValue` helper function rather than using a promise chain (see below links). https://github.com/tc39/proposal-faster-promise-adoption tc39/ecma262#2770 tc39/ecma262#2772 tc39/ecma262#1250 https://v8.dev/blog/fast-async Depends on #3793
1 parent 7fd1ddb commit 1564174

File tree

3 files changed

+100
-88
lines changed

3 files changed

+100
-88
lines changed

src/execution/__tests__/nonnull-test.ts

+15-15
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,16 @@ describe('Execute: handles non-nullable types', () => {
259259
path: ['syncNest', 'syncNest', 'sync'],
260260
locations: [{ line: 6, column: 22 }],
261261
},
262+
{
263+
message: promiseError.message,
264+
path: ['syncNest', 'promise'],
265+
locations: [{ line: 5, column: 11 }],
266+
},
267+
{
268+
message: promiseError.message,
269+
path: ['syncNest', 'syncNest', 'promise'],
270+
locations: [{ line: 6, column: 27 }],
271+
},
262272
{
263273
message: syncError.message,
264274
path: ['syncNest', 'promiseNest', 'sync'],
@@ -274,21 +284,6 @@ describe('Execute: handles non-nullable types', () => {
274284
path: ['promiseNest', 'syncNest', 'sync'],
275285
locations: [{ line: 12, column: 22 }],
276286
},
277-
{
278-
message: promiseError.message,
279-
path: ['syncNest', 'promise'],
280-
locations: [{ line: 5, column: 11 }],
281-
},
282-
{
283-
message: promiseError.message,
284-
path: ['syncNest', 'syncNest', 'promise'],
285-
locations: [{ line: 6, column: 27 }],
286-
},
287-
{
288-
message: syncError.message,
289-
path: ['promiseNest', 'promiseNest', 'sync'],
290-
locations: [{ line: 13, column: 25 }],
291-
},
292287
{
293288
message: promiseError.message,
294289
path: ['syncNest', 'promiseNest', 'promise'],
@@ -304,6 +299,11 @@ describe('Execute: handles non-nullable types', () => {
304299
path: ['promiseNest', 'syncNest', 'promise'],
305300
locations: [{ line: 12, column: 27 }],
306301
},
302+
{
303+
message: syncError.message,
304+
path: ['promiseNest', 'promiseNest', 'sync'],
305+
locations: [{ line: 13, column: 25 }],
306+
},
307307
{
308308
message: promiseError.message,
309309
path: ['promiseNest', 'promiseNest', 'promise'],

src/execution/__tests__/stream-test.ts

+23-11
Original file line numberDiff line numberDiff line change
@@ -1174,6 +1174,9 @@ describe('Execute: stream directive', () => {
11741174
],
11751175
},
11761176
],
1177+
hasNext: true,
1178+
},
1179+
{
11771180
hasNext: false,
11781181
},
11791182
]);
@@ -1197,19 +1200,25 @@ describe('Execute: stream directive', () => {
11971200
} /* c8 ignore stop */,
11981201
},
11991202
});
1200-
expectJSON(result).toDeepEqual({
1201-
errors: [
1202-
{
1203-
message:
1204-
'Cannot return null for non-nullable field NestedObject.nonNullScalarField.',
1205-
locations: [{ line: 4, column: 11 }],
1206-
path: ['nestedObject', 'nonNullScalarField'],
1203+
expectJSON(result).toDeepEqual([
1204+
{
1205+
errors: [
1206+
{
1207+
message:
1208+
'Cannot return null for non-nullable field NestedObject.nonNullScalarField.',
1209+
locations: [{ line: 4, column: 11 }],
1210+
path: ['nestedObject', 'nonNullScalarField'],
1211+
},
1212+
],
1213+
data: {
1214+
nestedObject: null,
12071215
},
1208-
],
1209-
data: {
1210-
nestedObject: null,
1216+
hasNext: true,
12111217
},
1212-
});
1218+
{
1219+
hasNext: false,
1220+
},
1221+
]);
12131222
});
12141223
it('Filters payloads that are nulled by a later synchronous error', async () => {
12151224
const document = parse(`
@@ -1350,6 +1359,9 @@ describe('Execute: stream directive', () => {
13501359
],
13511360
},
13521361
],
1362+
hasNext: true,
1363+
},
1364+
{
13531365
hasNext: false,
13541366
},
13551367
]);

src/execution/execute.ts

+62-62
Original file line numberDiff line numberDiff line change
@@ -715,25 +715,15 @@ function executeField(
715715
const result = resolveFn(source, args, contextValue, info);
716716

717717
if (isPromise(result)) {
718-
const completed = result.then((resolved) =>
719-
completeValue(
720-
exeContext,
721-
returnType,
722-
fieldNodes,
723-
info,
724-
path,
725-
resolved,
726-
asyncPayloadRecord,
727-
),
718+
return completePromisedValue(
719+
exeContext,
720+
returnType,
721+
fieldNodes,
722+
info,
723+
path,
724+
result,
725+
asyncPayloadRecord,
728726
);
729-
// Note: we don't rely on a `catch` method, but we do expect "thenable"
730-
// to take a second callback for the error case.
731-
return completed.then(undefined, (rawError) => {
732-
const error = locatedError(rawError, fieldNodes, pathToArray(path));
733-
const handledError = handleFieldError(error, returnType, errors);
734-
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
735-
return handledError;
736-
});
737727
}
738728

739729
const completed = completeValue(
@@ -922,6 +912,41 @@ function completeValue(
922912
);
923913
}
924914

915+
async function completePromisedValue(
916+
exeContext: ExecutionContext,
917+
returnType: GraphQLOutputType,
918+
fieldNodes: ReadonlyArray<FieldNode>,
919+
info: GraphQLResolveInfo,
920+
path: Path,
921+
result: Promise<unknown>,
922+
asyncPayloadRecord?: AsyncPayloadRecord,
923+
): Promise<unknown> {
924+
try {
925+
const resolved = await result;
926+
let completed = completeValue(
927+
exeContext,
928+
returnType,
929+
fieldNodes,
930+
info,
931+
path,
932+
resolved,
933+
asyncPayloadRecord,
934+
);
935+
if (isPromise(completed)) {
936+
// see: https://github.com/tc39/proposal-faster-promise-adoption
937+
// it is faster to await a promise prior to returning it from an async function
938+
completed = await completed;
939+
}
940+
return completed;
941+
} catch (rawError) {
942+
const errors = asyncPayloadRecord?.errors ?? exeContext.errors;
943+
const error = locatedError(rawError, fieldNodes, pathToArray(path));
944+
const handledError = handleFieldError(error, returnType, errors);
945+
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
946+
return handledError;
947+
}
948+
}
949+
925950
/**
926951
* Returns an object containing the `@stream` arguments if a field should be
927952
* streamed based on the experimental flag, stream directive present and
@@ -1156,29 +1181,18 @@ function completeListItemValue(
11561181
asyncPayloadRecord?: AsyncPayloadRecord,
11571182
): boolean {
11581183
if (isPromise(item)) {
1159-
const completedItem = item.then((resolved) =>
1160-
completeValue(
1184+
completedResults.push(
1185+
completePromisedValue(
11611186
exeContext,
11621187
itemType,
11631188
fieldNodes,
11641189
info,
11651190
itemPath,
1166-
resolved,
1191+
item,
11671192
asyncPayloadRecord,
11681193
),
11691194
);
11701195

1171-
// Note: we don't rely on a `catch` method, but we do expect "thenable"
1172-
// to take a second callback for the error case.
1173-
completedResults.push(
1174-
completedItem.then(undefined, (rawError) => {
1175-
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
1176-
const handledError = handleFieldError(error, itemType, errors);
1177-
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
1178-
return handledError;
1179-
}),
1180-
);
1181-
11821196
return true;
11831197
}
11841198

@@ -1897,36 +1911,22 @@ function executeStreamField(
18971911
exeContext,
18981912
});
18991913
if (isPromise(item)) {
1900-
const completedItems = item
1901-
.then((resolved) =>
1902-
completeValue(
1903-
exeContext,
1904-
itemType,
1905-
fieldNodes,
1906-
info,
1907-
itemPath,
1908-
resolved,
1909-
asyncPayloadRecord,
1910-
),
1911-
)
1912-
.then(undefined, (rawError) => {
1913-
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
1914-
const handledError = handleFieldError(
1915-
error,
1916-
itemType,
1917-
asyncPayloadRecord.errors,
1918-
);
1919-
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
1920-
return handledError;
1921-
})
1922-
.then(
1923-
(value) => [value],
1924-
(error) => {
1925-
asyncPayloadRecord.errors.push(error);
1926-
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1927-
return null;
1928-
},
1929-
);
1914+
const completedItems = completePromisedValue(
1915+
exeContext,
1916+
itemType,
1917+
fieldNodes,
1918+
info,
1919+
itemPath,
1920+
item,
1921+
asyncPayloadRecord,
1922+
).then(
1923+
(value) => [value],
1924+
(error) => {
1925+
asyncPayloadRecord.errors.push(error);
1926+
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1927+
return null;
1928+
},
1929+
);
19301930

19311931
asyncPayloadRecord.addItems(completedItems);
19321932
return asyncPayloadRecord;

0 commit comments

Comments
 (0)