@@ -2,6 +2,7 @@ import { assert, expect } from 'chai';
22import { describe , it } from 'mocha' ;
33
44import { expectJSON } from '../../__testUtils__/expectJSON.js' ;
5+ import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js' ;
56
67import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js' ;
78
@@ -21,6 +22,14 @@ import { buildSchema } from '../../utilities/buildASTSchema.js';
2122import { execute , executeSync } from '../execute.js' ;
2223import type { ExecutionResult } from '../Executor.js' ;
2324
25+ function delayedReject ( message : string ) : Promise < never > {
26+ return ( async ( ) => {
27+ await resolveOnNextTick ( ) ;
28+ await resolveOnNextTick ( ) ;
29+ throw new Error ( message ) ;
30+ } ) ( ) ;
31+ }
32+
2433describe ( 'Execute: Accepts any iterable as list value' , ( ) => {
2534 function complete ( rootValue : unknown ) {
2635 return executeSync ( {
@@ -77,13 +86,18 @@ describe('Execute: Accepts any iterable as list value', () => {
7786 } ) ;
7887 } ) ;
7988
80- it ( 'Ignores iterator return errors when iteration throws' , ( ) => {
89+ it ( 'Does not call iterator ` return` when iteration throws' , ( ) => {
8190 let returnCalled = false ;
91+ let nextCalls = 0 ;
8292 const listField = {
8393 [ Symbol . iterator ] ( ) {
8494 return {
8595 next ( ) {
86- throw new Error ( 'bad' ) ;
96+ nextCalls ++ ;
97+ if ( nextCalls === 1 ) {
98+ throw new Error ( 'bad' ) ;
99+ }
100+ return { done : true , value : undefined } ;
87101 } ,
88102 return ( ) {
89103 returnCalled = true ;
@@ -103,7 +117,8 @@ describe('Execute: Accepts any iterable as list value', () => {
103117 } ,
104118 ] ,
105119 } ) ;
106- expect ( returnCalled ) . to . equal ( true ) ;
120+ expect ( nextCalls ) . to . equal ( 2 ) ;
121+ expect ( returnCalled ) . to . equal ( false ) ;
107122 } ) ;
108123} ) ;
109124
@@ -116,7 +131,7 @@ describe('Execute: Handles abrupt completion in synchronous iterables', () => {
116131 } ) ;
117132 }
118133
119- it ( 'closes the iterator when `next` throws' , async ( ) => {
134+ it ( 'drains the iterator when `next` throws' , async ( ) => {
120135 let returned = false ;
121136 let nextCalls = 0 ;
122137
@@ -129,7 +144,10 @@ describe('Execute: Handles abrupt completion in synchronous iterables', () => {
129144 if ( nextCalls === 1 ) {
130145 return { done : false , value : 'ok' } ;
131146 }
132- throw new Error ( 'bad' ) ;
147+ if ( nextCalls === 2 ) {
148+ throw new Error ( 'bad' ) ;
149+ }
150+ return { done : true , value : undefined } ;
133151 } ,
134152 return ( ) : IteratorResult < string > {
135153 returned = true ;
@@ -147,11 +165,11 @@ describe('Execute: Handles abrupt completion in synchronous iterables', () => {
147165 } ,
148166 ] ,
149167 } ) ;
150- expect ( nextCalls ) . to . equal ( 2 ) ;
151- expect ( returned ) . to . equal ( true ) ;
168+ expect ( nextCalls ) . to . equal ( 3 ) ;
169+ expect ( returned ) . to . equal ( false ) ;
152170 } ) ;
153171
154- it ( 'closes the iterator when a null bubbles up from a non-null item' , async ( ) => {
172+ it ( 'drains the iterator when a null bubbles up from a non-null item' , async ( ) => {
155173 const values = [ 1 , null , 2 ] ;
156174 let index = 0 ;
157175 let returned = false ;
@@ -183,34 +201,98 @@ describe('Execute: Handles abrupt completion in synchronous iterables', () => {
183201 } ,
184202 ] ,
185203 } ) ;
186- expect ( index ) . to . equal ( 2 ) ;
187- expect ( returned ) . to . equal ( true ) ;
204+ expect ( index ) . to . equal ( 4 ) ;
205+ expect ( returned ) . to . equal ( false ) ;
188206 } ) ;
189207
190- it ( 'ignores errors thrown by the iterator `return` method' , async ( ) => {
191- const values = [ 1 , null , 2 ] ;
192- let index = 0 ;
208+ it ( 'handles iterator errors with later pending promises without calling `return`' , async ( ) => {
209+ let unhandledRejection : unknown = null ;
210+ const unhandledRejectionListener = ( reason : unknown ) => {
211+ unhandledRejection = reason ;
212+ } ;
213+ // eslint-disable-next-line no-undef
214+ process . on ( 'unhandledRejection' , unhandledRejectionListener ) ;
193215 let returned = false ;
216+ let nextCalls = 0 ;
217+ const laterPromise = delayedReject ( 'later bad' ) ;
194218
195- const listField : IterableIterator < number | null > = {
196- [ Symbol . iterator ] ( ) : IterableIterator < number | null > {
219+ const listField : IterableIterator < number | Promise < never > > = {
220+ [ Symbol . iterator ] ( ) : IterableIterator < number | Promise < never > > {
197221 return this ;
198222 } ,
199- next ( ) : IteratorResult < number | null > {
223+ next ( ) : IteratorResult < number | Promise < never > > {
224+ nextCalls ++ ;
225+ if ( nextCalls === 1 ) {
226+ return { done : false , value : 1 } ;
227+ }
228+ if ( nextCalls === 2 ) {
229+ throw new Error ( 'bad' ) ;
230+ }
231+ if ( nextCalls === 3 ) {
232+ return { done : false , value : laterPromise } ;
233+ }
234+ return { done : true , value : undefined } ;
235+ } ,
236+ return ( ) : IteratorResult < number | Promise < never > > {
237+ returned = true ;
238+ throw new Error ( 'ignored return error' ) ;
239+ } ,
240+ } ;
241+
242+ expectJSON ( await complete ( { listField } ) ) . toDeepEqual ( {
243+ data : { listField : null } ,
244+ errors : [
245+ {
246+ message : 'bad' ,
247+ locations : [ { line : 1 , column : 3 } ] ,
248+ path : [ 'listField' ] ,
249+ } ,
250+ ] ,
251+ } ) ;
252+
253+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
254+
255+ // eslint-disable-next-line no-undef
256+ process . removeListener ( 'unhandledRejection' , unhandledRejectionListener ) ;
257+
258+ expect ( nextCalls ) . to . equal ( 4 ) ;
259+ expect ( returned ) . to . equal ( false ) ;
260+ expect ( unhandledRejection ) . to . equal ( null ) ;
261+ } ) ;
262+
263+ it ( 'handles sync errors with later pending promises without calling `return`' , async ( ) => {
264+ let unhandledRejection : unknown = null ;
265+ const unhandledRejectionListener = ( reason : unknown ) => {
266+ unhandledRejection = reason ;
267+ } ;
268+ // eslint-disable-next-line no-undef
269+ process . on ( 'unhandledRejection' , unhandledRejectionListener ) ;
270+ let returned = false ;
271+ let index = 0 ;
272+ const values = [
273+ delayedReject ( 'first bad' ) ,
274+ null ,
275+ delayedReject ( 'third bad' ) ,
276+ ] ;
277+ const listField : IterableIterator < Promise < string > | null > = {
278+ [ Symbol . iterator ] ( ) : IterableIterator < Promise < string > | null > {
279+ return this ;
280+ } ,
281+ next ( ) : IteratorResult < Promise < string > | null > {
200282 const value = values [ index ++ ] ;
201283 if ( value === undefined ) {
202284 return { done : true , value : undefined } ;
203285 }
204286 return { done : false , value } ;
205287 } ,
206- return ( ) : IteratorResult < number | null > {
288+ return ( ) : IteratorResult < Promise < string > | null > {
207289 returned = true ;
208290 throw new Error ( 'ignored return error' ) ;
209291 } ,
210292 } ;
211293
212- expectJSON ( await complete ( { listField } , '[Int!] ' ) ) . toDeepEqual ( {
213- data : { listField : null } ,
294+ expectJSON ( await complete ( { listField } , '[String!]! ' ) ) . toDeepEqual ( {
295+ data : null ,
214296 errors : [
215297 {
216298 message : 'Cannot return null for non-nullable field Query.listField.' ,
@@ -219,8 +301,15 @@ describe('Execute: Handles abrupt completion in synchronous iterables', () => {
219301 } ,
220302 ] ,
221303 } ) ;
222- expect ( index ) . to . equal ( 2 ) ;
223- expect ( returned ) . to . equal ( true ) ;
304+
305+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
306+
307+ // eslint-disable-next-line no-undef
308+ process . removeListener ( 'unhandledRejection' , unhandledRejectionListener ) ;
309+
310+ expect ( returned ) . to . equal ( false ) ;
311+ expect ( index ) . to . equal ( 4 ) ;
312+ expect ( unhandledRejection ) . to . equal ( null ) ;
224313 } ) ;
225314} ) ;
226315
@@ -235,7 +324,17 @@ describe('Execute: Accepts async iterables as list value', () => {
235324
236325 function completeObjectList (
237326 resolve : GraphQLFieldResolver < { index : number } , unknown > ,
327+ nonNullable = false ,
238328 ) : PromiseOrValue < ExecutionResult > {
329+ const ObjectWrapperType = new GraphQLObjectType ( {
330+ name : 'ObjectWrapper' ,
331+ fields : {
332+ index : {
333+ type : new GraphQLNonNull ( GraphQLString ) ,
334+ resolve,
335+ } ,
336+ } ,
337+ } ) ;
239338 const schema = new GraphQLSchema ( {
240339 query : new GraphQLObjectType ( {
241340 name : 'Query' ,
@@ -247,15 +346,9 @@ describe('Execute: Accepts async iterables as list value', () => {
247346 yield await Promise . resolve ( { index : 2 } ) ;
248347 } ,
249348 type : new GraphQLList (
250- new GraphQLObjectType ( {
251- name : 'ObjectWrapper' ,
252- fields : {
253- index : {
254- type : new GraphQLNonNull ( GraphQLString ) ,
255- resolve,
256- } ,
257- } ,
258- } ) ,
349+ nonNullable
350+ ? new GraphQLNonNull ( ObjectWrapperType )
351+ : ObjectWrapperType ,
259352 ) ,
260353 } ,
261354 } ,
@@ -362,6 +455,41 @@ describe('Execute: Accepts async iterables as list value', () => {
362455 ] ,
363456 } ) ;
364457 } ) ;
458+
459+ it ( 'handles mixture of sync and async errors in AsyncIterables' , async ( ) => {
460+ let unhandledRejection : unknown = null ;
461+ const unhandledRejectionListener = ( reason : unknown ) => {
462+ unhandledRejection = reason ;
463+ } ;
464+ // eslint-disable-next-line no-undef
465+ process . on ( 'unhandledRejection' , unhandledRejectionListener ) ;
466+
467+ expectJSON (
468+ await completeObjectList ( ( { index } ) => {
469+ if ( index === 0 ) {
470+ return delayedReject ( 'bad' ) ;
471+ }
472+ throw new Error ( 'also bad' ) ;
473+ } , true ) ,
474+ ) . toDeepEqual ( {
475+ data : { listField : null } ,
476+ errors : [
477+ {
478+ message : 'also bad' ,
479+ locations : [ { line : 1 , column : 15 } ] ,
480+ path : [ 'listField' , 1 , 'index' ] ,
481+ } ,
482+ ] ,
483+ } ) ;
484+
485+ await new Promise ( ( resolve ) => setTimeout ( resolve , 20 ) ) ;
486+
487+ // eslint-disable-next-line no-undef
488+ process . removeListener ( 'unhandledRejection' , unhandledRejectionListener ) ;
489+
490+ expect ( unhandledRejection ) . to . equal ( null ) ;
491+ } ) ;
492+
365493 it ( 'Handles nulls yielded by async generator' , async ( ) => {
366494 async function * listField ( ) {
367495 yield await Promise . resolve ( 1 ) ;
0 commit comments