Skip to content

Commit

Permalink
introduce promiseWithResolvers helper
Browse files Browse the repository at this point in the history
  • Loading branch information
robrichard committed Jun 1, 2023
1 parent bd558cb commit 1e074c4
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 27 deletions.
34 changes: 17 additions & 17 deletions src/execution/__tests__/stream-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -129,14 +130,6 @@ async function completeAsync(
return Promise.all(promises);
}

function createResolvablePromise<T>(): [Promise<T>, (value?: T) => void] {
let resolveFn;
const promise = new Promise<T>((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) }');
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1697,7 +1694,7 @@ describe('Execute: stream directive', () => {
});

const result2Promise = iterator.next();
resolveIterableCompletion();
resolveIterableCompletion(null);
const result2 = await result2Promise;
expectJSON(result2).toDeepEqual({
value: {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 },
Expand Down
17 changes: 7 additions & 10 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -2239,11 +2240,9 @@ class DeferredFragmentRecord {
this._exeContext.subsequentPayloads.add(this);
this.isCompleted = false;
this.data = null;
this.promise = new Promise<ObjMap<unknown> | null>((resolve) => {
this._resolve = (promiseOrValue) => {
resolve(promiseOrValue);
};
}).then((data) => {
const { promise, resolve } = promiseWithResolvers<ObjMap<unknown> | null>();
this._resolve = resolve;
this.promise = promise.then((data) => {
this.data = data;
this.isCompleted = true;
});
Expand Down Expand Up @@ -2290,11 +2289,9 @@ class StreamItemsRecord {
this._exeContext.subsequentPayloads.add(this);
this.isCompleted = false;
this.items = null;
this.promise = new Promise<Array<unknown> | null>((resolve) => {
this._resolve = (promiseOrValue) => {
resolve(promiseOrValue);
};
}).then((items) => {
const { promise, resolve } = promiseWithResolvers<Array<unknown> | null>();
this._resolve = resolve;
this.promise = promise.then((items) => {
this.items = items;
this.isCompleted = true;
});
Expand Down
21 changes: 21 additions & 0 deletions src/jsutils/__tests__/promiseWithResolvers-test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
20 changes: 20 additions & 0 deletions src/jsutils/promiseWithResolvers.ts
Original file line number Diff line number Diff line change
@@ -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<T>(): {
promise: Promise<T>;
resolve: (value: T | PromiseOrValue<T>) => void;
reject: (reason?: any) => void;
} {
// these are assigned synchronously within the Promise constructor
let resolve!: (value: T | PromiseOrValue<T>) => void;
let reject!: (reason?: any) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}

0 comments on commit 1e074c4

Please sign in to comment.