Skip to content

Commit

Permalink
feat(utils): new helpers for promises
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Nov 22, 2024
1 parent 4ce2e24 commit 414e404
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/lazy-otters-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/utils': minor
---

new `fakePromise`, `mapMaybePromise` and `fakeRejectPromise` helper functions
8 changes: 3 additions & 5 deletions packages/executor/src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
addPath,
collectFields,
createGraphQLError,
fakePromise,
getArgumentValues,
getDefinedRootType,
GraphQLStreamDirective,
Expand Down Expand Up @@ -1575,16 +1576,13 @@ export function flattenIncrementalResults<TData>(
},
next() {
if (done) {
return Promise.resolve({
value: undefined,
done,
});
return fakePromise({ value: undefined, done });
}
if (initialResultSent) {
return subsequentIterator.next();
}
initialResultSent = true;
return Promise.resolve({
return fakePromise({
value: incrementalResults.initialResult,
done,
});
Expand Down
22 changes: 9 additions & 13 deletions packages/executors/envelop/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExecutionArgs, Plugin } from '@envelop/core';
import { ExecutionArgs, mapMaybePromise, Plugin } from '@envelop/core';
import { Executor, isPromise, MaybePromise } from '@graphql-tools/utils';
import { schemaFromExecutor } from '@graphql-tools/wrap';

Expand Down Expand Up @@ -97,25 +97,21 @@ export function useExecutor<TPluginContext extends Record<string, any>>(
pluginCtx.schema$ = pluginCtx.schema;
}
ensureSchema(args.contextValue);
if (isPromise(pluginCtx.schemaSetPromise$)) {
return pluginCtx.schemaSetPromise$.then(() => {
setExecuteFn(executorToExecuteFn);
}) as Promise<void>;
}
setExecuteFn(executorToExecuteFn);
// @ts-expect-error - Typings are wrong
return mapMaybePromise(pluginCtx.schemaSetPromise$, () => {
setExecuteFn(executorToExecuteFn);
});
},
onSubscribe({ args, setSubscribeFn }) {
if (args.schema) {
pluginCtx.schema = args.schema;
pluginCtx.schema$ = pluginCtx.schema;
}
ensureSchema(args.contextValue);
if (isPromise(pluginCtx.schemaSetPromise$)) {
return pluginCtx.schemaSetPromise$.then(() => {
setSubscribeFn(executorToExecuteFn);
}) as Promise<void>;
}
setSubscribeFn(executorToExecuteFn);
// @ts-expect-error - Typings are wrong
return mapMaybePromise(pluginCtx.schemaSetPromise$, () => {
setSubscribeFn(executorToExecuteFn);
});
},
onValidate({ params, context, setResult }) {
if (params.schema) {
Expand Down
3 changes: 2 additions & 1 deletion packages/links/src/AwaitVariablesLink.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as apolloImport from '@apollo/client';
import { mapMaybePromise } from '@graphql-tools/utils';

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

function getFinalPromise(object: any): Promise<any> {
return Promise.resolve(object).then(resolvedObject => {
return mapMaybePromise(object, resolvedObject => {
if (resolvedObject == null) {
return resolvedObject;
}
Expand Down
18 changes: 18 additions & 0 deletions packages/utils/src/createDeferred.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface PromiseWithResolvers<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason: any) => void;
}

export function createDeferred<T>(): PromiseWithResolvers<T> {
if (Promise.withResolvers) {
return Promise.withResolvers<T>();
}
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason: any) => void;
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return { promise, resolve, reject };
}
61 changes: 61 additions & 0 deletions packages/utils/src/fakePromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
function isPromise<T>(val: T | Promise<T>): val is Promise<T> {
return (val as any)?.then != null;
}

export function fakeRejectPromise(error: unknown): Promise<never> {
if (isPromise(error)) {
return error as Promise<never>;
}
return {
then() {
return this;
},
catch(reject: (error: unknown) => any) {
if (reject) {
return fakePromise(reject(error));
}
return this;
},
finally(cb) {
if (cb) {
cb();
}
return this;
},
[Symbol.toStringTag]: 'Promise',
};
}

export function fakePromise<T>(value: T): Promise<T> {
if (isPromise(value)) {
return value;
}
// Write a fake promise to avoid the promise constructor
// being called with `new Promise` in the browser.
return {
then(resolve: (value: T) => any) {
if (resolve) {
const callbackResult = resolve(value);
if (isPromise(callbackResult)) {
return callbackResult;
}
return fakePromise(callbackResult);
}
return this;
},
catch() {
return this;
},
finally(cb) {
if (cb) {
const callbackResult = cb();
if (isPromise(callbackResult)) {
return callbackResult.then(() => value);
}
return fakePromise(value);
}
return this;
},
[Symbol.toStringTag]: 'Promise',
};
}
3 changes: 3 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ export * from './directives.js';
export * from './mergeIncrementalResult.js';
export * from './debugTimer.js';
export * from './getDirectiveExtensions.js';
export * from './map-maybe-promise.js';
export * from './fakePromise.js';
export * from './createDeferred.js';
5 changes: 2 additions & 3 deletions packages/utils/src/jsutils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MaybePromise } from './executor.js';
import { mapMaybePromise } from './map-maybe-promise.js';

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

for (const value of values) {
accumulator = isPromise(accumulator)
? accumulator.then(resolved => callbackFn(resolved, value))
: callbackFn(accumulator, value);
accumulator = mapMaybePromise(accumulator, resolved => callbackFn(resolved, value));
}

return accumulator;
Expand Down
27 changes: 27 additions & 0 deletions packages/utils/src/map-maybe-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { isPromise } from '../src/jsutils.js';
import { MaybePromise } from './executor.js';

export function mapMaybePromise<T, R>(
value: MaybePromise<T>,
mapper: (v: T) => MaybePromise<R>,
errorMapper?: (e: any) => MaybePromise<R>,
): MaybePromise<R> {
if (isPromise(value)) {
if (errorMapper) {
try {
return value.then(mapper, errorMapper);
} catch (e) {
return errorMapper(e);
}
}
return value.then(mapper);
}
if (errorMapper) {
try {
return mapper(value);
} catch (e) {
return errorMapper(e);
}
}
return mapper(value);
}
37 changes: 19 additions & 18 deletions packages/utils/src/mapAsyncIterator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MaybePromise } from './executor.js';
import { isPromise } from './jsutils.js';
import { fakePromise, fakeRejectPromise } from './fakePromise.js';
import { mapMaybePromise } from './map-maybe-promise.js';

/**
* Given an AsyncIterable and a callback function, return an AsyncIterator
Expand All @@ -21,18 +22,17 @@ export function mapAsyncIterator<T, U>(
if (onEnd) {
let onEndWithValueResult: any /** R in onEndWithValue */;
onEndWithValue = value => {
if (onEndWithValueResult) {
return onEndWithValueResult;
}
const onEnd$ = onEnd();
return (onEndWithValueResult = isPromise(onEnd$) ? onEnd$.then(() => value) : value);
onEndWithValueResult ||= mapMaybePromise(onEnd(), () => value);
return onEndWithValueResult;
};
}

if (typeof iterator.return === 'function') {
$return = iterator.return;
abruptClose = (error: any) => {
const rethrow = () => Promise.reject(error);
const rethrow = () => {
throw error;
};
return $return.call(iterator).then(rethrow, rethrow);
};
}
Expand All @@ -41,7 +41,9 @@ export function mapAsyncIterator<T, U>(
if (result.done) {
return onEndWithValue ? onEndWithValue(result) : result;
}
return asyncMapValue(result.value, onNext).then(iteratorResult, abruptClose);
return mapMaybePromise(result.value, value =>
mapMaybePromise(onNext(value), iteratorResult, abruptClose),
);
}

let mapReject: any;
Expand All @@ -50,10 +52,10 @@ export function mapAsyncIterator<T, U>(
// Capture rejectCallback to ensure it cannot be null.
const reject = onError;
mapReject = (error: any) => {
if (onErrorResult) {
return onErrorResult;
}
return (onErrorResult = asyncMapValue(error, reject).then(iteratorResult, abruptClose));
onErrorResult ||= mapMaybePromise(error, error =>
mapMaybePromise(reject(error), iteratorResult, abruptClose),
);
return onErrorResult;
};
}

Expand All @@ -64,25 +66,24 @@ export function mapAsyncIterator<T, U>(
return() {
const res$ = $return
? $return.call(iterator).then(mapResult, mapReject)
: Promise.resolve({ value: undefined, done: true });
: fakePromise({ value: undefined, done: true });
return onEndWithValue ? res$.then(onEndWithValue) : res$;
},
throw(error: any) {
if (typeof iterator.throw === 'function') {
return iterator.throw(error).then(mapResult, mapReject);
}
return Promise.reject(error).catch(abruptClose);
if (abruptClose) {
return abruptClose(error);
}
return fakeRejectPromise(error);
},
[Symbol.asyncIterator]() {
return this;
},
};
}

function asyncMapValue<T, U>(value: T, callback: (value: T) => PromiseLike<U> | U): Promise<U> {
return new Promise(resolve => resolve(callback(value)));
}

function iteratorResult<T>(value: T): IteratorResult<T> {
return { value, done: false };
}
10 changes: 6 additions & 4 deletions packages/utils/src/observableToAsyncIterable.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { fakePromise } from './fakePromise.js';

export interface Observer<T> {
next: (value: T) => void;
error: (error: Error) => void;
Expand Down Expand Up @@ -58,13 +60,13 @@ export function observableToAsyncIterable<T>(observable: Observable<T>): AsyncIt

const subscription = observable.subscribe({
next(value: any) {
pushValue(value);
return pushValue(value);
},
error(err: Error) {
pushError(err);
return pushError(err);
},
complete() {
pushDone();
return pushDone();
},
});

Expand All @@ -87,7 +89,7 @@ export function observableToAsyncIterable<T>(observable: Observable<T>): AsyncIt
},
return() {
emptyQueue();
return Promise.resolve({ value: undefined, done: true });
return fakePromise({ value: undefined, done: true });
},
throw(error) {
emptyQueue();
Expand Down

0 comments on commit 414e404

Please sign in to comment.