Skip to content

Commit d22d32d

Browse files
authored
introduce promiseWithResolvers helper (#3902)
Promise.withResolvers is currently a [stage 2 proposal for JS](https://github.com/tc39/proposal-promise-with-resolvers). This adds an equivalent jsutil to standardize usage.
1 parent bd558cb commit d22d32d

File tree

4 files changed

+65
-27
lines changed

4 files changed

+65
-27
lines changed

src/execution/__tests__/stream-test.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, it } from 'mocha';
44
import { expectJSON } from '../../__testUtils__/expectJSON.js';
55

66
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js';
7+
import { promiseWithResolvers } from '../../jsutils/promiseWithResolvers.js';
78

89
import type { DocumentNode } from '../../language/ast.js';
910
import { parse } from '../../language/parser.js';
@@ -129,14 +130,6 @@ async function completeAsync(
129130
return Promise.all(promises);
130131
}
131132

132-
function createResolvablePromise<T>(): [Promise<T>, (value?: T) => void] {
133-
let resolveFn;
134-
const promise = new Promise<T>((resolve) => {
135-
resolveFn = resolve;
136-
});
137-
return [promise, resolveFn as unknown as (value?: T) => void];
138-
}
139-
140133
describe('Execute: stream directive', () => {
141134
it('Can stream a list field', async () => {
142135
const document = parse('{ scalarList @stream(initialCount: 1) }');
@@ -1564,7 +1557,8 @@ describe('Execute: stream directive', () => {
15641557
]);
15651558
});
15661559
it('Returns payloads in correct order when parent deferred fragment resolves slower than stream', async () => {
1567-
const [slowFieldPromise, resolveSlowField] = createResolvablePromise();
1560+
const { promise: slowFieldPromise, resolve: resolveSlowField } =
1561+
promiseWithResolvers();
15681562
const document = parse(`
15691563
query {
15701564
nestedObject {
@@ -1655,9 +1649,12 @@ describe('Execute: stream directive', () => {
16551649
});
16561650
});
16571651
it('Can @defer fields that are resolved after async iterable is complete', async () => {
1658-
const [slowFieldPromise, resolveSlowField] = createResolvablePromise();
1659-
const [iterableCompletionPromise, resolveIterableCompletion] =
1660-
createResolvablePromise();
1652+
const { promise: slowFieldPromise, resolve: resolveSlowField } =
1653+
promiseWithResolvers();
1654+
const {
1655+
promise: iterableCompletionPromise,
1656+
resolve: resolveIterableCompletion,
1657+
} = promiseWithResolvers();
16611658

16621659
const document = parse(`
16631660
query {
@@ -1697,7 +1694,7 @@ describe('Execute: stream directive', () => {
16971694
});
16981695

16991696
const result2Promise = iterator.next();
1700-
resolveIterableCompletion();
1697+
resolveIterableCompletion(null);
17011698
const result2 = await result2Promise;
17021699
expectJSON(result2).toDeepEqual({
17031700
value: {
@@ -1741,9 +1738,12 @@ describe('Execute: stream directive', () => {
17411738
});
17421739
});
17431740
it('Can @defer fields that are resolved before async iterable is complete', async () => {
1744-
const [slowFieldPromise, resolveSlowField] = createResolvablePromise();
1745-
const [iterableCompletionPromise, resolveIterableCompletion] =
1746-
createResolvablePromise();
1741+
const { promise: slowFieldPromise, resolve: resolveSlowField } =
1742+
promiseWithResolvers();
1743+
const {
1744+
promise: iterableCompletionPromise,
1745+
resolve: resolveIterableCompletion,
1746+
} = promiseWithResolvers();
17471747

17481748
const document = parse(`
17491749
query {
@@ -1819,7 +1819,7 @@ describe('Execute: stream directive', () => {
18191819
done: false,
18201820
});
18211821
const result4Promise = iterator.next();
1822-
resolveIterableCompletion();
1822+
resolveIterableCompletion(null);
18231823
const result4 = await result4Promise;
18241824
expectJSON(result4).toDeepEqual({
18251825
value: { hasNext: false },

src/execution/execute.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { addPath, pathToArray } from '../jsutils/Path.js';
1212
import { promiseForObject } from '../jsutils/promiseForObject.js';
1313
import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js';
1414
import { promiseReduce } from '../jsutils/promiseReduce.js';
15+
import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js';
1516

1617
import type { GraphQLFormattedError } from '../error/GraphQLError.js';
1718
import { GraphQLError } from '../error/GraphQLError.js';
@@ -2239,11 +2240,9 @@ class DeferredFragmentRecord {
22392240
this._exeContext.subsequentPayloads.add(this);
22402241
this.isCompleted = false;
22412242
this.data = null;
2242-
this.promise = new Promise<ObjMap<unknown> | null>((resolve) => {
2243-
this._resolve = (promiseOrValue) => {
2244-
resolve(promiseOrValue);
2245-
};
2246-
}).then((data) => {
2243+
const { promise, resolve } = promiseWithResolvers<ObjMap<unknown> | null>();
2244+
this._resolve = resolve;
2245+
this.promise = promise.then((data) => {
22472246
this.data = data;
22482247
this.isCompleted = true;
22492248
});
@@ -2290,11 +2289,9 @@ class StreamItemsRecord {
22902289
this._exeContext.subsequentPayloads.add(this);
22912290
this.isCompleted = false;
22922291
this.items = null;
2293-
this.promise = new Promise<Array<unknown> | null>((resolve) => {
2294-
this._resolve = (promiseOrValue) => {
2295-
resolve(promiseOrValue);
2296-
};
2297-
}).then((items) => {
2292+
const { promise, resolve } = promiseWithResolvers<Array<unknown> | null>();
2293+
this._resolve = resolve;
2294+
this.promise = promise.then((items) => {
22982295
this.items = items;
22992296
this.isCompleted = true;
23002297
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect } from 'chai';
2+
import { describe, it } from 'mocha';
3+
4+
import { expectPromise } from '../../__testUtils__/expectPromise.js';
5+
6+
import { promiseWithResolvers } from '../promiseWithResolvers.js';
7+
8+
describe('promiseWithResolvers', () => {
9+
it('resolves values', async () => {
10+
const { promise, resolve } = promiseWithResolvers();
11+
resolve('foo');
12+
expect(await expectPromise(promise).toResolve()).to.equal('foo');
13+
});
14+
15+
it('rejects values', async () => {
16+
const { promise, reject } = promiseWithResolvers();
17+
const error = new Error('rejected');
18+
reject(error);
19+
await expectPromise(promise).toRejectWith('rejected');
20+
});
21+
});

src/jsutils/promiseWithResolvers.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { PromiseOrValue } from './PromiseOrValue.js';
2+
3+
/**
4+
* Based on Promise.withResolvers proposal
5+
* https://github.com/tc39/proposal-promise-with-resolvers
6+
*/
7+
export function promiseWithResolvers<T>(): {
8+
promise: Promise<T>;
9+
resolve: (value: T | PromiseOrValue<T>) => void;
10+
reject: (reason?: any) => void;
11+
} {
12+
// these are assigned synchronously within the Promise constructor
13+
let resolve!: (value: T | PromiseOrValue<T>) => void;
14+
let reject!: (reason?: any) => void;
15+
const promise = new Promise<T>((res, rej) => {
16+
resolve = res;
17+
reject = rej;
18+
});
19+
return { promise, resolve, reject };
20+
}

0 commit comments

Comments
 (0)