Skip to content

Commit 3dd7030

Browse files
committed
fix(race): concurrent next calls with defer/stream (#2975)
* fix(race): concurrent next calls * refactor test * use invariant * disable eslint error * fix
1 parent ff9a61f commit 3dd7030

File tree

2 files changed

+80
-0
lines changed

2 files changed

+80
-0
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,22 @@ async function complete(document: DocumentNode, rootValue: unknown = {}) {
145145
return result;
146146
}
147147

148+
async function completeAsync(document: DocumentNode, numCalls: number) {
149+
const schema = new GraphQLSchema({ query });
150+
151+
const result = await execute({ schema, document, rootValue: {} });
152+
153+
invariant(isAsyncIterable(result));
154+
155+
const iterator = result[Symbol.asyncIterator]();
156+
157+
const promises = [];
158+
for (let i = 0; i < numCalls; i++) {
159+
promises.push(iterator.next());
160+
}
161+
return Promise.all(promises);
162+
}
163+
148164
describe('Execute: stream directive', () => {
149165
it('Can stream a list field', async () => {
150166
const document = parse('{ scalarList @stream(initialCount: 1) }');
@@ -449,6 +465,58 @@ describe('Execute: stream directive', () => {
449465
},
450466
]);
451467
});
468+
it('Can stream a field that returns an async iterable', async () => {
469+
const document = parse(`
470+
query {
471+
asyncIterableList @stream(initialCount: 2) {
472+
name
473+
id
474+
}
475+
}
476+
`);
477+
const result = await completeAsync(document, 4);
478+
expectJSON(result).toDeepEqual([
479+
{
480+
done: false,
481+
value: {
482+
data: {
483+
asyncIterableList: [
484+
{
485+
name: 'Luke',
486+
id: '1',
487+
},
488+
{
489+
name: 'Han',
490+
id: '2',
491+
},
492+
],
493+
},
494+
hasNext: true,
495+
},
496+
},
497+
{
498+
done: false,
499+
value: {
500+
data: {
501+
name: 'Leia',
502+
id: '3',
503+
},
504+
path: ['asyncIterableList', 2],
505+
hasNext: true,
506+
},
507+
},
508+
{
509+
done: false,
510+
value: {
511+
hasNext: false,
512+
},
513+
},
514+
{
515+
done: true,
516+
value: undefined,
517+
},
518+
]);
519+
});
452520
it('Handles error thrown in async iterable before initialCount is reached', async () => {
453521
const document = parse(`
454522
query {

src/execution/execute.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1623,8 +1623,20 @@ export class Dispatcher {
16231623

16241624
resolved = true;
16251625

1626+
if (this._subsequentPayloads.length === 0) {
1627+
// a different call to next has exhausted all payloads
1628+
resolve({ value: undefined, done: true });
1629+
return;
1630+
}
1631+
16261632
const index = this._subsequentPayloads.indexOf(promise);
16271633

1634+
if (index === -1) {
1635+
// a different call to next has consumed this payload
1636+
resolve(this._race());
1637+
return;
1638+
}
1639+
16281640
this._subsequentPayloads.splice(index, 1);
16291641

16301642
const { value, done } = payload;

0 commit comments

Comments
 (0)