diff --git a/src/execution/__tests__/stream-test.ts b/src/execution/__tests__/stream-test.ts index 2a14b6cf90..d75f280277 100644 --- a/src/execution/__tests__/stream-test.ts +++ b/src/execution/__tests__/stream-test.ts @@ -4,6 +4,7 @@ import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON.js'; import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js'; +import { promiseWithResolvers } from '../../jsutils/promiseWithResolvers.js'; import type { DocumentNode } from '../../language/ast.js'; import { parse } from '../../language/parser.js'; @@ -129,14 +130,6 @@ async function completeAsync( return Promise.all(promises); } -function createResolvablePromise(): [Promise, (value?: T) => void] { - let resolveFn; - const promise = new Promise((resolve) => { - resolveFn = resolve; - }); - return [promise, resolveFn as unknown as (value?: T) => void]; -} - describe('Execute: stream directive', () => { it('Can stream a list field', async () => { const document = parse('{ scalarList @stream(initialCount: 1) }'); @@ -1564,7 +1557,8 @@ describe('Execute: stream directive', () => { ]); }); it('Returns payloads in correct order when parent deferred fragment resolves slower than stream', async () => { - const [slowFieldPromise, resolveSlowField] = createResolvablePromise(); + const { promise: slowFieldPromise, resolve: resolveSlowField } = + promiseWithResolvers(); const document = parse(` query { nestedObject { @@ -1655,9 +1649,12 @@ describe('Execute: stream directive', () => { }); }); it('Can @defer fields that are resolved after async iterable is complete', async () => { - const [slowFieldPromise, resolveSlowField] = createResolvablePromise(); - const [iterableCompletionPromise, resolveIterableCompletion] = - createResolvablePromise(); + const { promise: slowFieldPromise, resolve: resolveSlowField } = + promiseWithResolvers(); + const { + promise: iterableCompletionPromise, + resolve: resolveIterableCompletion, + } = promiseWithResolvers(); const document = parse(` query { @@ -1697,7 +1694,7 @@ describe('Execute: stream directive', () => { }); const result2Promise = iterator.next(); - resolveIterableCompletion(); + resolveIterableCompletion(null); const result2 = await result2Promise; expectJSON(result2).toDeepEqual({ value: { @@ -1741,9 +1738,12 @@ describe('Execute: stream directive', () => { }); }); it('Can @defer fields that are resolved before async iterable is complete', async () => { - const [slowFieldPromise, resolveSlowField] = createResolvablePromise(); - const [iterableCompletionPromise, resolveIterableCompletion] = - createResolvablePromise(); + const { promise: slowFieldPromise, resolve: resolveSlowField } = + promiseWithResolvers(); + const { + promise: iterableCompletionPromise, + resolve: resolveIterableCompletion, + } = promiseWithResolvers(); const document = parse(` query { @@ -1819,7 +1819,7 @@ describe('Execute: stream directive', () => { done: false, }); const result4Promise = iterator.next(); - resolveIterableCompletion(); + resolveIterableCompletion(null); const result4 = await result4Promise; expectJSON(result4).toDeepEqual({ value: { hasNext: false }, diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 926d517820..d5aca82871 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -12,6 +12,7 @@ import { addPath, pathToArray } from '../jsutils/Path.js'; import { promiseForObject } from '../jsutils/promiseForObject.js'; import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js'; import { promiseReduce } from '../jsutils/promiseReduce.js'; +import { promiseWithResolvers } from '../jsutils/promiseWithResolvers.js'; import type { GraphQLFormattedError } from '../error/GraphQLError.js'; import { GraphQLError } from '../error/GraphQLError.js'; @@ -2239,11 +2240,9 @@ class DeferredFragmentRecord { this._exeContext.subsequentPayloads.add(this); this.isCompleted = false; this.data = null; - this.promise = new Promise | null>((resolve) => { - this._resolve = (promiseOrValue) => { - resolve(promiseOrValue); - }; - }).then((data) => { + const { promise, resolve } = promiseWithResolvers | null>(); + this._resolve = resolve; + this.promise = promise.then((data) => { this.data = data; this.isCompleted = true; }); @@ -2290,11 +2289,9 @@ class StreamItemsRecord { this._exeContext.subsequentPayloads.add(this); this.isCompleted = false; this.items = null; - this.promise = new Promise | null>((resolve) => { - this._resolve = (promiseOrValue) => { - resolve(promiseOrValue); - }; - }).then((items) => { + const { promise, resolve } = promiseWithResolvers | null>(); + this._resolve = resolve; + this.promise = promise.then((items) => { this.items = items; this.isCompleted = true; }); diff --git a/src/jsutils/__tests__/promiseWithResolvers-test.ts b/src/jsutils/__tests__/promiseWithResolvers-test.ts new file mode 100644 index 0000000000..453a6fb68d --- /dev/null +++ b/src/jsutils/__tests__/promiseWithResolvers-test.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { expectPromise } from '../../__testUtils__/expectPromise.js'; + +import { promiseWithResolvers } from '../promiseWithResolvers.js'; + +describe('promiseWithResolvers', () => { + it('resolves values', async () => { + const { promise, resolve } = promiseWithResolvers(); + resolve('foo'); + expect(await expectPromise(promise).toResolve()).to.equal('foo'); + }); + + it('rejects values', async () => { + const { promise, reject } = promiseWithResolvers(); + const error = new Error('rejected'); + reject(error); + await expectPromise(promise).toRejectWith('rejected'); + }); +}); diff --git a/src/jsutils/promiseWithResolvers.ts b/src/jsutils/promiseWithResolvers.ts new file mode 100644 index 0000000000..d5a5eeaccd --- /dev/null +++ b/src/jsutils/promiseWithResolvers.ts @@ -0,0 +1,20 @@ +import type { PromiseOrValue } from './PromiseOrValue.js'; + +/** + * Based on Promise.withResolvers proposal + * https://github.com/tc39/proposal-promise-with-resolvers + */ +export function promiseWithResolvers(): { + promise: Promise; + resolve: (value: T | PromiseOrValue) => void; + reject: (reason?: any) => void; +} { + // these are assigned synchronously within the Promise constructor + let resolve!: (value: T | PromiseOrValue) => void; + let reject!: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +}