Skip to content

Commit cb6fac3

Browse files
committed
introduce executeStreamIterator
avoids needlessly rechecking whether a stream has begun
1 parent c7d216e commit cb6fac3

File tree

2 files changed

+148
-82
lines changed

2 files changed

+148
-82
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,48 @@ describe('Execute: stream directive', () => {
636636
},
637637
]);
638638
});
639+
it('Can stream a field from a deferred path', async () => {
640+
const document = parse(`
641+
query {
642+
... @defer {
643+
friendList @stream(initialCount: 2) {
644+
name
645+
id
646+
}
647+
}
648+
}
649+
`);
650+
const result = await complete(document, { friendList: friends });
651+
expectJSON(result).toDeepEqual([
652+
{
653+
data: {},
654+
hasNext: true,
655+
},
656+
{
657+
incremental: [
658+
{
659+
data: {
660+
friendList: [
661+
{ name: 'Luke', id: '1' },
662+
{ name: 'Han', id: '2' },
663+
],
664+
},
665+
path: [],
666+
},
667+
],
668+
hasNext: true,
669+
},
670+
{
671+
incremental: [
672+
{
673+
items: [{ name: 'Leia', id: '3' }],
674+
path: ['friendList', 2],
675+
},
676+
],
677+
hasNext: false,
678+
},
679+
]);
680+
});
639681
it('Negative values of initialCount throw field errors on a field that returns an async iterable', async () => {
640682
const document = parse(`
641683
query {

src/execution/execute.ts

Lines changed: 106 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,33 +1195,39 @@ function completeListValue(
11951195
// This is specified as a simple map, however we're optimizing the path
11961196
// where the list contains no Promises by avoiding creating another Promise.
11971197
let containsPromise = false;
1198-
let previousAsyncPayloadRecord = asyncPayloadRecord;
11991198
const completedResults: Array<unknown> = [];
1199+
const iterator = result[Symbol.iterator]();
12001200
let index = 0;
1201-
for (const item of result) {
1201+
// eslint-disable-next-line no-constant-condition
1202+
while (true) {
12021203
// No need to modify the info object containing the path,
12031204
// since from here on it is not ever accessed by resolver functions.
1204-
const itemPath = exeContext.addPath(path, index, undefined);
12051205

12061206
if (
12071207
stream &&
12081208
typeof stream.initialCount === 'number' &&
12091209
index >= stream.initialCount
12101210
) {
1211-
previousAsyncPayloadRecord = executeStreamField(
1212-
path,
1213-
itemPath,
1214-
item,
1211+
executeStreamIterator(
1212+
index,
1213+
iterator,
12151214
exeContext,
12161215
fieldGroup,
12171216
info,
12181217
itemType,
1219-
previousAsyncPayloadRecord,
1218+
path,
1219+
asyncPayloadRecord,
12201220
);
1221-
index++;
1222-
continue;
1221+
break;
1222+
}
1223+
1224+
const { done, value: item } = iterator.next();
1225+
if (done) {
1226+
break;
12231227
}
12241228

1229+
const itemPath = exeContext.addPath(path, index, undefined);
1230+
12251231
if (
12261232
completeListItemValue(
12271233
item,
@@ -1886,107 +1892,125 @@ function executeDeferredFragment(
18861892
asyncPayloadRecord.addData(promiseOrData);
18871893
}
18881894

1889-
function executeStreamField(
1890-
path: Path,
1891-
itemPath: Path,
1892-
item: PromiseOrValue<unknown>,
1895+
function executeStreamIterator(
1896+
initialIndex: number,
1897+
iterator: Iterator<unknown>,
18931898
exeContext: ExecutionContext,
18941899
fieldGroup: FieldGroup,
18951900
info: GraphQLResolveInfo,
18961901
itemType: GraphQLOutputType,
1902+
path: Path,
18971903
parentContext?: AsyncPayloadRecord,
1898-
): AsyncPayloadRecord {
1899-
const asyncPayloadRecord = new StreamRecord({
1900-
deferDepth: parentContext?.deferDepth,
1901-
path: itemPath,
1902-
parentContext,
1903-
exeContext,
1904-
});
1905-
if (isPromise(item)) {
1906-
const completedItems = completePromisedValue(
1907-
exeContext,
1908-
itemType,
1909-
fieldGroup,
1910-
info,
1911-
itemPath,
1912-
item,
1913-
asyncPayloadRecord,
1914-
).then(
1915-
(value) => [value],
1916-
(error) => {
1917-
asyncPayloadRecord.errors.push(error);
1918-
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1919-
return null;
1920-
},
1921-
);
1904+
): void {
1905+
let index = initialIndex;
1906+
const deferDepth = parentContext?.deferDepth;
1907+
let previousAsyncPayloadRecord = parentContext ?? undefined;
1908+
// eslint-disable-next-line no-constant-condition
1909+
while (true) {
1910+
const { done, value: item } = iterator.next();
1911+
if (done) {
1912+
break;
1913+
}
19221914

1923-
asyncPayloadRecord.addItems(completedItems);
1924-
return asyncPayloadRecord;
1925-
}
1915+
const itemPath = exeContext.addPath(path, index, undefined);
1916+
const asyncPayloadRecord = new StreamRecord({
1917+
deferDepth,
1918+
path: itemPath,
1919+
parentContext: previousAsyncPayloadRecord,
1920+
exeContext,
1921+
});
19261922

1927-
let completedItem: PromiseOrValue<unknown>;
1928-
try {
1929-
try {
1930-
completedItem = completeValue(
1923+
if (isPromise(item)) {
1924+
const completedItems = completePromisedValue(
19311925
exeContext,
19321926
itemType,
19331927
fieldGroup,
19341928
info,
19351929
itemPath,
19361930
item,
19371931
asyncPayloadRecord,
1932+
).then(
1933+
(value) => [value],
1934+
(error) => {
1935+
asyncPayloadRecord.errors.push(error);
1936+
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1937+
return null;
1938+
},
19381939
);
1939-
} catch (rawError) {
1940-
const error = locatedError(
1941-
rawError,
1942-
toNodes(fieldGroup),
1943-
pathToArray(itemPath),
1944-
);
1945-
completedItem = handleFieldError(
1946-
error,
1947-
itemType,
1948-
asyncPayloadRecord.errors,
1949-
);
1950-
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
1940+
1941+
asyncPayloadRecord.addItems(completedItems);
1942+
previousAsyncPayloadRecord = asyncPayloadRecord;
1943+
index++;
1944+
continue;
19511945
}
1952-
} catch (error) {
1953-
asyncPayloadRecord.errors.push(error);
1954-
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1955-
asyncPayloadRecord.addItems(null);
1956-
return asyncPayloadRecord;
1957-
}
19581946

1959-
if (isPromise(completedItem)) {
1960-
const completedItems = completedItem
1961-
.then(undefined, (rawError) => {
1947+
let completedItem: PromiseOrValue<unknown>;
1948+
try {
1949+
try {
1950+
completedItem = completeValue(
1951+
exeContext,
1952+
itemType,
1953+
fieldGroup,
1954+
info,
1955+
itemPath,
1956+
item,
1957+
asyncPayloadRecord,
1958+
);
1959+
} catch (rawError) {
19621960
const error = locatedError(
19631961
rawError,
19641962
toNodes(fieldGroup),
19651963
pathToArray(itemPath),
19661964
);
1967-
const handledError = handleFieldError(
1965+
completedItem = handleFieldError(
19681966
error,
19691967
itemType,
19701968
asyncPayloadRecord.errors,
19711969
);
19721970
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
1973-
return handledError;
1974-
})
1975-
.then(
1976-
(value) => [value],
1977-
(error) => {
1978-
asyncPayloadRecord.errors.push(error);
1979-
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1980-
return null;
1981-
},
1982-
);
1971+
}
1972+
} catch (error) {
1973+
asyncPayloadRecord.errors.push(error);
1974+
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
1975+
asyncPayloadRecord.addItems(null);
1976+
previousAsyncPayloadRecord = asyncPayloadRecord;
1977+
index++;
1978+
continue;
1979+
}
19831980

1984-
asyncPayloadRecord.addItems(completedItems);
1985-
return asyncPayloadRecord;
1986-
}
1981+
if (isPromise(completedItem)) {
1982+
const completedItems = completedItem
1983+
.then(undefined, (rawError) => {
1984+
const error = locatedError(
1985+
rawError,
1986+
toNodes(fieldGroup),
1987+
pathToArray(itemPath),
1988+
);
1989+
const handledError = handleFieldError(
1990+
error,
1991+
itemType,
1992+
asyncPayloadRecord.errors,
1993+
);
1994+
filterSubsequentPayloads(exeContext, itemPath, asyncPayloadRecord);
1995+
return handledError;
1996+
})
1997+
.then(
1998+
(value) => [value],
1999+
(error) => {
2000+
asyncPayloadRecord.errors.push(error);
2001+
filterSubsequentPayloads(exeContext, path, asyncPayloadRecord);
2002+
return null;
2003+
},
2004+
);
2005+
2006+
asyncPayloadRecord.addItems(completedItems);
2007+
} else {
2008+
asyncPayloadRecord.addItems([completedItem]);
2009+
}
19872010

1988-
asyncPayloadRecord.addItems([completedItem]);
1989-
return asyncPayloadRecord;
2011+
previousAsyncPayloadRecord = asyncPayloadRecord;
2012+
index++;
2013+
}
19902014
}
19912015

19922016
async function executeStreamAsyncIteratorItem(

0 commit comments

Comments
 (0)