Skip to content

Commit 6d42ced

Browse files
subscribe: stay synchronous when possible (#3620)
This change aligns the return types of `execute` and `subscribe` (as well as `createSourceEventStream`) with respect to returning values or promises. Just as GraphQL execution for queries and mutations stays synchronous when possible, creation of the source event stream and returning the mapped `AsyncGenerator` will now occur synchronously when possible, i.e. when the `subscribe` method for the given subscription root field directly returns an `AsyncIterable`, rather than a Promise<AsyncIterable>. Specifically, the return types of `subscribe` and `createSourceEventStream` become: `subscribe(...): PromiseOrValue<AsyncGenerator<ExecutionResult> | ExecutionResult>` `createSourceEventStream(...): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult>`. Previously, they were: `subscribe(...): Promise<AsyncGenerator<ExecutionResult> | ExecutionResult>`). `createSourceEventStream(...): Promise<AsyncIterable<unknown> | ExecutionResult>`. Co-authored-by: Ivan Goncharov <[email protected]>
1 parent ea1894a commit 6d42ced

File tree

2 files changed

+161
-76
lines changed

2 files changed

+161
-76
lines changed

src/execution/__tests__/subscribe-test.ts

+85-51
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { expectJSON } from '../../__testUtils__/expectJSON';
55
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
66

77
import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
8+
import { isPromise } from '../../jsutils/isPromise';
9+
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue';
810

911
import { parse } from '../../language/parser';
1012

@@ -123,38 +125,59 @@ function createSubscription(pubsub: SimplePubSub<Email>) {
123125
return subscribe({ schema: emailSchema, document, rootValue: data });
124126
}
125127

126-
async function expectPromise(promise: Promise<unknown>) {
127-
let caughtError: Error;
128-
129-
try {
130-
/* c8 ignore next 2 */
131-
await promise;
132-
expect.fail('promise should have thrown but did not');
133-
} catch (error) {
134-
caughtError = error;
135-
}
128+
// TODO: consider adding this method to testUtils (with tests)
129+
function expectPromise(maybePromise: unknown) {
130+
assert(isPromise(maybePromise));
136131

137132
return {
138-
toReject() {
139-
expect(caughtError).to.be.an.instanceOf(Error);
133+
toResolve() {
134+
return maybePromise;
140135
},
141-
toRejectWith(message: string) {
136+
async toRejectWith(message: string) {
137+
let caughtError: Error;
138+
139+
try {
140+
/* c8 ignore next 2 */
141+
await maybePromise;
142+
expect.fail('promise should have thrown but did not');
143+
} catch (error) {
144+
caughtError = error;
145+
}
146+
142147
expect(caughtError).to.be.an.instanceOf(Error);
143148
expect(caughtError).to.have.property('message', message);
144149
},
145150
};
146151
}
147152

153+
// TODO: consider adding this method to testUtils (with tests)
154+
function expectEqualPromisesOrValues<T>(
155+
value1: PromiseOrValue<T>,
156+
value2: PromiseOrValue<T>,
157+
): PromiseOrValue<T> {
158+
if (isPromise(value1)) {
159+
assert(isPromise(value2));
160+
return Promise.all([value1, value2]).then((resolved) => {
161+
expectJSON(resolved[1]).toDeepEqual(resolved[0]);
162+
return resolved[0];
163+
});
164+
}
165+
166+
assert(!isPromise(value2));
167+
expectJSON(value2).toDeepEqual(value1);
168+
return value1;
169+
}
170+
148171
const DummyQueryType = new GraphQLObjectType({
149172
name: 'Query',
150173
fields: {
151174
dummy: { type: GraphQLString },
152175
},
153176
});
154177

155-
async function subscribeWithBadFn(
178+
function subscribeWithBadFn(
156179
subscribeFn: () => unknown,
157-
): Promise<ExecutionResult> {
180+
): PromiseOrValue<ExecutionResult | AsyncIterable<unknown>> {
158181
const schema = new GraphQLSchema({
159182
query: DummyQueryType,
160183
subscription: new GraphQLObjectType({
@@ -165,13 +188,11 @@ async function subscribeWithBadFn(
165188
}),
166189
});
167190
const document = parse('subscription { foo }');
168-
const result = await subscribe({ schema, document });
169191

170-
assert(!isAsyncIterable(result));
171-
expectJSON(await createSourceEventStream(schema, document)).toDeepEqual(
172-
result,
192+
return expectEqualPromisesOrValues(
193+
subscribe({ schema, document }),
194+
createSourceEventStream(schema, document),
173195
);
174-
return result;
175196
}
176197

177198
/* eslint-disable @typescript-eslint/require-await */
@@ -193,7 +214,7 @@ describe('Subscription Initialization Phase', () => {
193214
yield { foo: 'FooValue' };
194215
}
195216

196-
const subscription = await subscribe({
217+
const subscription = subscribe({
197218
schema,
198219
document: parse('subscription { foo }'),
199220
rootValue: { foo: fooGenerator },
@@ -229,7 +250,7 @@ describe('Subscription Initialization Phase', () => {
229250
}),
230251
});
231252

232-
const subscription = await subscribe({
253+
const subscription = subscribe({
233254
schema,
234255
document: parse('subscription { foo }'),
235256
});
@@ -267,10 +288,13 @@ describe('Subscription Initialization Phase', () => {
267288
}),
268289
});
269290

270-
const subscription = await subscribe({
291+
const promise = subscribe({
271292
schema,
272293
document: parse('subscription { foo }'),
273294
});
295+
assert(isPromise(promise));
296+
297+
const subscription = await promise;
274298
assert(isAsyncIterable(subscription));
275299

276300
expect(await subscription.next()).to.deep.equal({
@@ -299,7 +323,7 @@ describe('Subscription Initialization Phase', () => {
299323
yield { foo: 'FooValue' };
300324
}
301325

302-
const subscription = await subscribe({
326+
const subscription = subscribe({
303327
schema,
304328
document: parse('subscription { foo }'),
305329
rootValue: { customFoo: fooGenerator },
@@ -349,7 +373,7 @@ describe('Subscription Initialization Phase', () => {
349373
}),
350374
});
351375

352-
const subscription = await subscribe({
376+
const subscription = subscribe({
353377
schema,
354378
document: parse('subscription { foo bar }'),
355379
});
@@ -379,31 +403,29 @@ describe('Subscription Initialization Phase', () => {
379403
});
380404

381405
// @ts-expect-error (schema must not be null)
382-
(await expectPromise(subscribe({ schema: null, document }))).toRejectWith(
406+
expect(() => subscribe({ schema: null, document })).to.throw(
383407
'Expected null to be a GraphQL schema.',
384408
);
385409

386410
// @ts-expect-error
387-
(await expectPromise(subscribe({ document }))).toRejectWith(
411+
expect(() => subscribe({ document })).to.throw(
388412
'Expected undefined to be a GraphQL schema.',
389413
);
390414

391415
// @ts-expect-error (document must not be null)
392-
(await expectPromise(subscribe({ schema, document: null }))).toRejectWith(
416+
expect(() => subscribe({ schema, document: null })).to.throw(
393417
'Must provide document.',
394418
);
395419

396420
// @ts-expect-error
397-
(await expectPromise(subscribe({ schema }))).toRejectWith(
398-
'Must provide document.',
399-
);
421+
expect(() => subscribe({ schema })).to.throw('Must provide document.');
400422
});
401423

402424
it('resolves to an error if schema does not support subscriptions', async () => {
403425
const schema = new GraphQLSchema({ query: DummyQueryType });
404426
const document = parse('subscription { unknownField }');
405427

406-
const result = await subscribe({ schema, document });
428+
const result = subscribe({ schema, document });
407429
expectJSON(result).toDeepEqual({
408430
errors: [
409431
{
@@ -427,7 +449,7 @@ describe('Subscription Initialization Phase', () => {
427449
});
428450
const document = parse('subscription { unknownField }');
429451

430-
const result = await subscribe({ schema, document });
452+
const result = subscribe({ schema, document });
431453
expectJSON(result).toDeepEqual({
432454
errors: [
433455
{
@@ -450,11 +472,11 @@ describe('Subscription Initialization Phase', () => {
450472
});
451473

452474
// @ts-expect-error
453-
(await expectPromise(subscribe({ schema, document: {} }))).toReject();
475+
expect(() => subscribe({ schema, document: {} })).to.throw();
454476
});
455477

456478
it('throws an error if subscribe does not return an iterator', async () => {
457-
expectJSON(await subscribeWithBadFn(() => 'test')).toDeepEqual({
479+
const expectedResult = {
458480
errors: [
459481
{
460482
message:
@@ -463,7 +485,15 @@ describe('Subscription Initialization Phase', () => {
463485
path: ['foo'],
464486
},
465487
],
466-
});
488+
};
489+
490+
expectJSON(subscribeWithBadFn(() => 'test')).toDeepEqual(expectedResult);
491+
492+
expectJSON(
493+
await expectPromise(
494+
subscribeWithBadFn(() => Promise.resolve('test')),
495+
).toResolve(),
496+
).toDeepEqual(expectedResult);
467497
});
468498

469499
it('resolves to an error for subscription resolver errors', async () => {
@@ -479,24 +509,28 @@ describe('Subscription Initialization Phase', () => {
479509

480510
expectJSON(
481511
// Returning an error
482-
await subscribeWithBadFn(() => new Error('test error')),
512+
subscribeWithBadFn(() => new Error('test error')),
483513
).toDeepEqual(expectedResult);
484514

485515
expectJSON(
486516
// Throwing an error
487-
await subscribeWithBadFn(() => {
517+
subscribeWithBadFn(() => {
488518
throw new Error('test error');
489519
}),
490520
).toDeepEqual(expectedResult);
491521

492522
expectJSON(
493523
// Resolving to an error
494-
await subscribeWithBadFn(() => Promise.resolve(new Error('test error'))),
524+
await expectPromise(
525+
subscribeWithBadFn(() => Promise.resolve(new Error('test error'))),
526+
).toResolve(),
495527
).toDeepEqual(expectedResult);
496528

497529
expectJSON(
498530
// Rejecting with an error
499-
await subscribeWithBadFn(() => Promise.reject(new Error('test error'))),
531+
await expectPromise(
532+
subscribeWithBadFn(() => Promise.reject(new Error('test error'))),
533+
).toResolve(),
500534
).toDeepEqual(expectedResult);
501535
});
502536

@@ -523,7 +557,7 @@ describe('Subscription Initialization Phase', () => {
523557

524558
// If we receive variables that cannot be coerced correctly, subscribe() will
525559
// resolve to an ExecutionResult that contains an informative error description.
526-
const result = await subscribe({ schema, document, variableValues });
560+
const result = subscribe({ schema, document, variableValues });
527561
expectJSON(result).toDeepEqual({
528562
errors: [
529563
{
@@ -541,10 +575,10 @@ describe('Subscription Publish Phase', () => {
541575
it('produces a payload for multiple subscribe in same subscription', async () => {
542576
const pubsub = new SimplePubSub<Email>();
543577

544-
const subscription = await createSubscription(pubsub);
578+
const subscription = createSubscription(pubsub);
545579
assert(isAsyncIterable(subscription));
546580

547-
const secondSubscription = await createSubscription(pubsub);
581+
const secondSubscription = createSubscription(pubsub);
548582
assert(isAsyncIterable(secondSubscription));
549583

550584
const payload1 = subscription.next();
@@ -583,7 +617,7 @@ describe('Subscription Publish Phase', () => {
583617

584618
it('produces a payload per subscription event', async () => {
585619
const pubsub = new SimplePubSub<Email>();
586-
const subscription = await createSubscription(pubsub);
620+
const subscription = createSubscription(pubsub);
587621
assert(isAsyncIterable(subscription));
588622

589623
// Wait for the next subscription payload.
@@ -672,7 +706,7 @@ describe('Subscription Publish Phase', () => {
672706

673707
it('produces a payload when there are multiple events', async () => {
674708
const pubsub = new SimplePubSub<Email>();
675-
const subscription = await createSubscription(pubsub);
709+
const subscription = createSubscription(pubsub);
676710
assert(isAsyncIterable(subscription));
677711

678712
let payload = subscription.next();
@@ -738,7 +772,7 @@ describe('Subscription Publish Phase', () => {
738772

739773
it('should not trigger when subscription is already done', async () => {
740774
const pubsub = new SimplePubSub<Email>();
741-
const subscription = await createSubscription(pubsub);
775+
const subscription = createSubscription(pubsub);
742776
assert(isAsyncIterable(subscription));
743777

744778
let payload = subscription.next();
@@ -792,7 +826,7 @@ describe('Subscription Publish Phase', () => {
792826

793827
it('should not trigger when subscription is thrown', async () => {
794828
const pubsub = new SimplePubSub<Email>();
795-
const subscription = await createSubscription(pubsub);
829+
const subscription = createSubscription(pubsub);
796830
assert(isAsyncIterable(subscription));
797831

798832
let payload = subscription.next();
@@ -845,7 +879,7 @@ describe('Subscription Publish Phase', () => {
845879

846880
it('event order is correct for multiple publishes', async () => {
847881
const pubsub = new SimplePubSub<Email>();
848-
const subscription = await createSubscription(pubsub);
882+
const subscription = createSubscription(pubsub);
849883
assert(isAsyncIterable(subscription));
850884

851885
let payload = subscription.next();
@@ -936,7 +970,7 @@ describe('Subscription Publish Phase', () => {
936970
});
937971

938972
const document = parse('subscription { newMessage }');
939-
const subscription = await subscribe({ schema, document });
973+
const subscription = subscribe({ schema, document });
940974
assert(isAsyncIterable(subscription));
941975

942976
expect(await subscription.next()).to.deep.equal({
@@ -997,7 +1031,7 @@ describe('Subscription Publish Phase', () => {
9971031
});
9981032

9991033
const document = parse('subscription { newMessage }');
1000-
const subscription = await subscribe({ schema, document });
1034+
const subscription = subscribe({ schema, document });
10011035
assert(isAsyncIterable(subscription));
10021036

10031037
expect(await subscription.next()).to.deep.equal({
@@ -1007,7 +1041,7 @@ describe('Subscription Publish Phase', () => {
10071041
},
10081042
});
10091043

1010-
(await expectPromise(subscription.next())).toRejectWith('test error');
1044+
await expectPromise(subscription.next()).toRejectWith('test error');
10111045

10121046
expect(await subscription.next()).to.deep.equal({
10131047
done: true,

0 commit comments

Comments
 (0)