Skip to content

Commit fdfa381

Browse files
subscribe: stay synchronous when possible (graphql#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 4532c87 commit fdfa381

File tree

2 files changed

+154
-92
lines changed

2 files changed

+154
-92
lines changed

src/execution/__tests__/subscribe-test.ts

Lines changed: 95 additions & 63 deletions
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,24 +403,22 @@ 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('Deprecated: allows positional arguments to createSourceEventStream', async () => {
@@ -433,27 +455,25 @@ describe('Subscription Initialization Phase', () => {
433455
});
434456

435457
// @ts-expect-error (schema must not be null)
436-
(await expectPromise(createSourceEventStream(null, document))).toRejectWith(
458+
expect(() => createSourceEventStream(null, document)).to.throw(
437459
'Expected null to be a GraphQL schema.',
438460
);
439461

440-
(
441-
await expectPromise(
442-
createSourceEventStream(
443-
// @ts-expect-error
444-
undefined,
445-
document,
446-
),
447-
)
448-
).toRejectWith('Expected undefined to be a GraphQL schema.');
462+
expect(() =>
463+
createSourceEventStream(
464+
// @ts-expect-error
465+
undefined,
466+
document,
467+
),
468+
).to.throw('Expected undefined to be a GraphQL schema.');
449469

450470
// @ts-expect-error (document must not be null)
451-
(await expectPromise(createSourceEventStream(schema, null))).toRejectWith(
471+
expect(() => createSourceEventStream(schema, null)).to.throw(
452472
'Must provide document.',
453473
);
454474

455475
// @ts-expect-error
456-
(await expectPromise(createSourceEventStream(schema))).toRejectWith(
476+
expect(() => createSourceEventStream(schema)).to.throw(
457477
'Must provide document.',
458478
);
459479
});
@@ -462,7 +482,7 @@ describe('Subscription Initialization Phase', () => {
462482
const schema = new GraphQLSchema({ query: DummyQueryType });
463483
const document = parse('subscription { unknownField }');
464484

465-
const result = await subscribe({ schema, document });
485+
const result = subscribe({ schema, document });
466486
expectJSON(result).toDeepEqual({
467487
errors: [
468488
{
@@ -486,7 +506,7 @@ describe('Subscription Initialization Phase', () => {
486506
});
487507
const document = parse('subscription { unknownField }');
488508

489-
const result = await subscribe({ schema, document });
509+
const result = subscribe({ schema, document });
490510
expectJSON(result).toDeepEqual({
491511
errors: [
492512
{
@@ -509,11 +529,11 @@ describe('Subscription Initialization Phase', () => {
509529
});
510530

511531
// @ts-expect-error
512-
(await expectPromise(subscribe({ schema, document: {} }))).toReject();
532+
expect(() => subscribe({ schema, document: {} })).to.throw();
513533
});
514534

515535
it('throws an error if subscribe does not return an iterator', async () => {
516-
expectJSON(await subscribeWithBadFn(() => 'test')).toDeepEqual({
536+
const expectedResult = {
517537
errors: [
518538
{
519539
message:
@@ -522,7 +542,15 @@ describe('Subscription Initialization Phase', () => {
522542
path: ['foo'],
523543
},
524544
],
525-
});
545+
};
546+
547+
expectJSON(subscribeWithBadFn(() => 'test')).toDeepEqual(expectedResult);
548+
549+
expectJSON(
550+
await expectPromise(
551+
subscribeWithBadFn(() => Promise.resolve('test')),
552+
).toResolve(),
553+
).toDeepEqual(expectedResult);
526554
});
527555

528556
it('resolves to an error for subscription resolver errors', async () => {
@@ -538,24 +566,28 @@ describe('Subscription Initialization Phase', () => {
538566

539567
expectJSON(
540568
// Returning an error
541-
await subscribeWithBadFn(() => new Error('test error')),
569+
subscribeWithBadFn(() => new Error('test error')),
542570
).toDeepEqual(expectedResult);
543571

544572
expectJSON(
545573
// Throwing an error
546-
await subscribeWithBadFn(() => {
574+
subscribeWithBadFn(() => {
547575
throw new Error('test error');
548576
}),
549577
).toDeepEqual(expectedResult);
550578

551579
expectJSON(
552580
// Resolving to an error
553-
await subscribeWithBadFn(() => Promise.resolve(new Error('test error'))),
581+
await expectPromise(
582+
subscribeWithBadFn(() => Promise.resolve(new Error('test error'))),
583+
).toResolve(),
554584
).toDeepEqual(expectedResult);
555585

556586
expectJSON(
557587
// Rejecting with an error
558-
await subscribeWithBadFn(() => Promise.reject(new Error('test error'))),
588+
await expectPromise(
589+
subscribeWithBadFn(() => Promise.reject(new Error('test error'))),
590+
).toResolve(),
559591
).toDeepEqual(expectedResult);
560592
});
561593

@@ -582,7 +614,7 @@ describe('Subscription Initialization Phase', () => {
582614

583615
// If we receive variables that cannot be coerced correctly, subscribe() will
584616
// resolve to an ExecutionResult that contains an informative error description.
585-
const result = await subscribe({ schema, document, variableValues });
617+
const result = subscribe({ schema, document, variableValues });
586618
expectJSON(result).toDeepEqual({
587619
errors: [
588620
{
@@ -600,10 +632,10 @@ describe('Subscription Publish Phase', () => {
600632
it('produces a payload for multiple subscribe in same subscription', async () => {
601633
const pubsub = new SimplePubSub<Email>();
602634

603-
const subscription = await createSubscription(pubsub);
635+
const subscription = createSubscription(pubsub);
604636
assert(isAsyncIterable(subscription));
605637

606-
const secondSubscription = await createSubscription(pubsub);
638+
const secondSubscription = createSubscription(pubsub);
607639
assert(isAsyncIterable(secondSubscription));
608640

609641
const payload1 = subscription.next();
@@ -642,7 +674,7 @@ describe('Subscription Publish Phase', () => {
642674

643675
it('produces a payload per subscription event', async () => {
644676
const pubsub = new SimplePubSub<Email>();
645-
const subscription = await createSubscription(pubsub);
677+
const subscription = createSubscription(pubsub);
646678
assert(isAsyncIterable(subscription));
647679

648680
// Wait for the next subscription payload.
@@ -731,7 +763,7 @@ describe('Subscription Publish Phase', () => {
731763

732764
it('produces a payload when there are multiple events', async () => {
733765
const pubsub = new SimplePubSub<Email>();
734-
const subscription = await createSubscription(pubsub);
766+
const subscription = createSubscription(pubsub);
735767
assert(isAsyncIterable(subscription));
736768

737769
let payload = subscription.next();
@@ -797,7 +829,7 @@ describe('Subscription Publish Phase', () => {
797829

798830
it('should not trigger when subscription is already done', async () => {
799831
const pubsub = new SimplePubSub<Email>();
800-
const subscription = await createSubscription(pubsub);
832+
const subscription = createSubscription(pubsub);
801833
assert(isAsyncIterable(subscription));
802834

803835
let payload = subscription.next();
@@ -851,7 +883,7 @@ describe('Subscription Publish Phase', () => {
851883

852884
it('should not trigger when subscription is thrown', async () => {
853885
const pubsub = new SimplePubSub<Email>();
854-
const subscription = await createSubscription(pubsub);
886+
const subscription = createSubscription(pubsub);
855887
assert(isAsyncIterable(subscription));
856888

857889
let payload = subscription.next();
@@ -904,7 +936,7 @@ describe('Subscription Publish Phase', () => {
904936

905937
it('event order is correct for multiple publishes', async () => {
906938
const pubsub = new SimplePubSub<Email>();
907-
const subscription = await createSubscription(pubsub);
939+
const subscription = createSubscription(pubsub);
908940
assert(isAsyncIterable(subscription));
909941

910942
let payload = subscription.next();
@@ -995,7 +1027,7 @@ describe('Subscription Publish Phase', () => {
9951027
});
9961028

9971029
const document = parse('subscription { newMessage }');
998-
const subscription = await subscribe({ schema, document });
1030+
const subscription = subscribe({ schema, document });
9991031
assert(isAsyncIterable(subscription));
10001032

10011033
expect(await subscription.next()).to.deep.equal({
@@ -1056,7 +1088,7 @@ describe('Subscription Publish Phase', () => {
10561088
});
10571089

10581090
const document = parse('subscription { newMessage }');
1059-
const subscription = await subscribe({ schema, document });
1091+
const subscription = subscribe({ schema, document });
10601092
assert(isAsyncIterable(subscription));
10611093

10621094
expect(await subscription.next()).to.deep.equal({
@@ -1066,7 +1098,7 @@ describe('Subscription Publish Phase', () => {
10661098
},
10671099
});
10681100

1069-
(await expectPromise(subscription.next())).toRejectWith('test error');
1101+
await expectPromise(subscription.next()).toRejectWith('test error');
10701102

10711103
expect(await subscription.next()).to.deep.equal({
10721104
done: true,

0 commit comments

Comments
 (0)