Skip to content

Commit 414e404

Browse files
committed
feat(utils): new helpers for promises
1 parent 4ce2e24 commit 414e404

File tree

11 files changed

+155
-44
lines changed

11 files changed

+155
-44
lines changed

.changeset/lazy-otters-buy.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-tools/utils': minor
3+
---
4+
5+
new `fakePromise`, `mapMaybePromise` and `fakeRejectPromise` helper functions

packages/executor/src/execution/execute.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
addPath,
3636
collectFields,
3737
createGraphQLError,
38+
fakePromise,
3839
getArgumentValues,
3940
getDefinedRootType,
4041
GraphQLStreamDirective,
@@ -1575,16 +1576,13 @@ export function flattenIncrementalResults<TData>(
15751576
},
15761577
next() {
15771578
if (done) {
1578-
return Promise.resolve({
1579-
value: undefined,
1580-
done,
1581-
});
1579+
return fakePromise({ value: undefined, done });
15821580
}
15831581
if (initialResultSent) {
15841582
return subsequentIterator.next();
15851583
}
15861584
initialResultSent = true;
1587-
return Promise.resolve({
1585+
return fakePromise({
15881586
value: incrementalResults.initialResult,
15891587
done,
15901588
});

packages/executors/envelop/src/index.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExecutionArgs, Plugin } from '@envelop/core';
1+
import { ExecutionArgs, mapMaybePromise, Plugin } from '@envelop/core';
22
import { Executor, isPromise, MaybePromise } from '@graphql-tools/utils';
33
import { schemaFromExecutor } from '@graphql-tools/wrap';
44

@@ -97,25 +97,21 @@ export function useExecutor<TPluginContext extends Record<string, any>>(
9797
pluginCtx.schema$ = pluginCtx.schema;
9898
}
9999
ensureSchema(args.contextValue);
100-
if (isPromise(pluginCtx.schemaSetPromise$)) {
101-
return pluginCtx.schemaSetPromise$.then(() => {
102-
setExecuteFn(executorToExecuteFn);
103-
}) as Promise<void>;
104-
}
105-
setExecuteFn(executorToExecuteFn);
100+
// @ts-expect-error - Typings are wrong
101+
return mapMaybePromise(pluginCtx.schemaSetPromise$, () => {
102+
setExecuteFn(executorToExecuteFn);
103+
});
106104
},
107105
onSubscribe({ args, setSubscribeFn }) {
108106
if (args.schema) {
109107
pluginCtx.schema = args.schema;
110108
pluginCtx.schema$ = pluginCtx.schema;
111109
}
112110
ensureSchema(args.contextValue);
113-
if (isPromise(pluginCtx.schemaSetPromise$)) {
114-
return pluginCtx.schemaSetPromise$.then(() => {
115-
setSubscribeFn(executorToExecuteFn);
116-
}) as Promise<void>;
117-
}
118-
setSubscribeFn(executorToExecuteFn);
111+
// @ts-expect-error - Typings are wrong
112+
return mapMaybePromise(pluginCtx.schemaSetPromise$, () => {
113+
setSubscribeFn(executorToExecuteFn);
114+
});
119115
},
120116
onValidate({ params, context, setResult }) {
121117
if (params.schema) {

packages/links/src/AwaitVariablesLink.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as apolloImport from '@apollo/client';
2+
import { mapMaybePromise } from '@graphql-tools/utils';
23

34
const apollo: typeof apolloImport = (apolloImport as any)?.default ?? apolloImport;
45

56
function getFinalPromise(object: any): Promise<any> {
6-
return Promise.resolve(object).then(resolvedObject => {
7+
return mapMaybePromise(object, resolvedObject => {
78
if (resolvedObject == null) {
89
return resolvedObject;
910
}

packages/utils/src/createDeferred.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface PromiseWithResolvers<T> {
2+
promise: Promise<T>;
3+
resolve: (value: T | PromiseLike<T>) => void;
4+
reject: (reason: any) => void;
5+
}
6+
7+
export function createDeferred<T>(): PromiseWithResolvers<T> {
8+
if (Promise.withResolvers) {
9+
return Promise.withResolvers<T>();
10+
}
11+
let resolve!: (value: T | PromiseLike<T>) => void;
12+
let reject!: (reason: any) => void;
13+
const promise = new Promise<T>((_resolve, _reject) => {
14+
resolve = _resolve;
15+
reject = _reject;
16+
});
17+
return { promise, resolve, reject };
18+
}

packages/utils/src/fakePromise.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
function isPromise<T>(val: T | Promise<T>): val is Promise<T> {
2+
return (val as any)?.then != null;
3+
}
4+
5+
export function fakeRejectPromise(error: unknown): Promise<never> {
6+
if (isPromise(error)) {
7+
return error as Promise<never>;
8+
}
9+
return {
10+
then() {
11+
return this;
12+
},
13+
catch(reject: (error: unknown) => any) {
14+
if (reject) {
15+
return fakePromise(reject(error));
16+
}
17+
return this;
18+
},
19+
finally(cb) {
20+
if (cb) {
21+
cb();
22+
}
23+
return this;
24+
},
25+
[Symbol.toStringTag]: 'Promise',
26+
};
27+
}
28+
29+
export function fakePromise<T>(value: T): Promise<T> {
30+
if (isPromise(value)) {
31+
return value;
32+
}
33+
// Write a fake promise to avoid the promise constructor
34+
// being called with `new Promise` in the browser.
35+
return {
36+
then(resolve: (value: T) => any) {
37+
if (resolve) {
38+
const callbackResult = resolve(value);
39+
if (isPromise(callbackResult)) {
40+
return callbackResult;
41+
}
42+
return fakePromise(callbackResult);
43+
}
44+
return this;
45+
},
46+
catch() {
47+
return this;
48+
},
49+
finally(cb) {
50+
if (cb) {
51+
const callbackResult = cb();
52+
if (isPromise(callbackResult)) {
53+
return callbackResult.then(() => value);
54+
}
55+
return fakePromise(value);
56+
}
57+
return this;
58+
},
59+
[Symbol.toStringTag]: 'Promise',
60+
};
61+
}

packages/utils/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,6 @@ export * from './directives.js';
5555
export * from './mergeIncrementalResult.js';
5656
export * from './debugTimer.js';
5757
export * from './getDirectiveExtensions.js';
58+
export * from './map-maybe-promise.js';
59+
export * from './fakePromise.js';
60+
export * from './createDeferred.js';

packages/utils/src/jsutils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { MaybePromise } from './executor.js';
2+
import { mapMaybePromise } from './map-maybe-promise.js';
23

34
export function isIterableObject(value: unknown): value is Iterable<unknown> {
45
return value != null && typeof value === 'object' && Symbol.iterator in value;
@@ -20,9 +21,7 @@ export function promiseReduce<T, U>(
2021
let accumulator = initialValue;
2122

2223
for (const value of values) {
23-
accumulator = isPromise(accumulator)
24-
? accumulator.then(resolved => callbackFn(resolved, value))
25-
: callbackFn(accumulator, value);
24+
accumulator = mapMaybePromise(accumulator, resolved => callbackFn(resolved, value));
2625
}
2726

2827
return accumulator;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { isPromise } from '../src/jsutils.js';
2+
import { MaybePromise } from './executor.js';
3+
4+
export function mapMaybePromise<T, R>(
5+
value: MaybePromise<T>,
6+
mapper: (v: T) => MaybePromise<R>,
7+
errorMapper?: (e: any) => MaybePromise<R>,
8+
): MaybePromise<R> {
9+
if (isPromise(value)) {
10+
if (errorMapper) {
11+
try {
12+
return value.then(mapper, errorMapper);
13+
} catch (e) {
14+
return errorMapper(e);
15+
}
16+
}
17+
return value.then(mapper);
18+
}
19+
if (errorMapper) {
20+
try {
21+
return mapper(value);
22+
} catch (e) {
23+
return errorMapper(e);
24+
}
25+
}
26+
return mapper(value);
27+
}

packages/utils/src/mapAsyncIterator.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { MaybePromise } from './executor.js';
2-
import { isPromise } from './jsutils.js';
2+
import { fakePromise, fakeRejectPromise } from './fakePromise.js';
3+
import { mapMaybePromise } from './map-maybe-promise.js';
34

45
/**
56
* Given an AsyncIterable and a callback function, return an AsyncIterator
@@ -21,18 +22,17 @@ export function mapAsyncIterator<T, U>(
2122
if (onEnd) {
2223
let onEndWithValueResult: any /** R in onEndWithValue */;
2324
onEndWithValue = value => {
24-
if (onEndWithValueResult) {
25-
return onEndWithValueResult;
26-
}
27-
const onEnd$ = onEnd();
28-
return (onEndWithValueResult = isPromise(onEnd$) ? onEnd$.then(() => value) : value);
25+
onEndWithValueResult ||= mapMaybePromise(onEnd(), () => value);
26+
return onEndWithValueResult;
2927
};
3028
}
3129

3230
if (typeof iterator.return === 'function') {
3331
$return = iterator.return;
3432
abruptClose = (error: any) => {
35-
const rethrow = () => Promise.reject(error);
33+
const rethrow = () => {
34+
throw error;
35+
};
3636
return $return.call(iterator).then(rethrow, rethrow);
3737
};
3838
}
@@ -41,7 +41,9 @@ export function mapAsyncIterator<T, U>(
4141
if (result.done) {
4242
return onEndWithValue ? onEndWithValue(result) : result;
4343
}
44-
return asyncMapValue(result.value, onNext).then(iteratorResult, abruptClose);
44+
return mapMaybePromise(result.value, value =>
45+
mapMaybePromise(onNext(value), iteratorResult, abruptClose),
46+
);
4547
}
4648

4749
let mapReject: any;
@@ -50,10 +52,10 @@ export function mapAsyncIterator<T, U>(
5052
// Capture rejectCallback to ensure it cannot be null.
5153
const reject = onError;
5254
mapReject = (error: any) => {
53-
if (onErrorResult) {
54-
return onErrorResult;
55-
}
56-
return (onErrorResult = asyncMapValue(error, reject).then(iteratorResult, abruptClose));
55+
onErrorResult ||= mapMaybePromise(error, error =>
56+
mapMaybePromise(reject(error), iteratorResult, abruptClose),
57+
);
58+
return onErrorResult;
5759
};
5860
}
5961

@@ -64,25 +66,24 @@ export function mapAsyncIterator<T, U>(
6466
return() {
6567
const res$ = $return
6668
? $return.call(iterator).then(mapResult, mapReject)
67-
: Promise.resolve({ value: undefined, done: true });
69+
: fakePromise({ value: undefined, done: true });
6870
return onEndWithValue ? res$.then(onEndWithValue) : res$;
6971
},
7072
throw(error: any) {
7173
if (typeof iterator.throw === 'function') {
7274
return iterator.throw(error).then(mapResult, mapReject);
7375
}
74-
return Promise.reject(error).catch(abruptClose);
76+
if (abruptClose) {
77+
return abruptClose(error);
78+
}
79+
return fakeRejectPromise(error);
7580
},
7681
[Symbol.asyncIterator]() {
7782
return this;
7883
},
7984
};
8085
}
8186

82-
function asyncMapValue<T, U>(value: T, callback: (value: T) => PromiseLike<U> | U): Promise<U> {
83-
return new Promise(resolve => resolve(callback(value)));
84-
}
85-
8687
function iteratorResult<T>(value: T): IteratorResult<T> {
8788
return { value, done: false };
8889
}

packages/utils/src/observableToAsyncIterable.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { fakePromise } from './fakePromise.js';
2+
13
export interface Observer<T> {
24
next: (value: T) => void;
35
error: (error: Error) => void;
@@ -58,13 +60,13 @@ export function observableToAsyncIterable<T>(observable: Observable<T>): AsyncIt
5860

5961
const subscription = observable.subscribe({
6062
next(value: any) {
61-
pushValue(value);
63+
return pushValue(value);
6264
},
6365
error(err: Error) {
64-
pushError(err);
66+
return pushError(err);
6567
},
6668
complete() {
67-
pushDone();
69+
return pushDone();
6870
},
6971
});
7072

@@ -87,7 +89,7 @@ export function observableToAsyncIterable<T>(observable: Observable<T>): AsyncIt
8789
},
8890
return() {
8991
emptyQueue();
90-
return Promise.resolve({ value: undefined, done: true });
92+
return fakePromise({ value: undefined, done: true });
9193
},
9294
throw(error) {
9395
emptyQueue();

0 commit comments

Comments
 (0)