diff --git a/CHANGELOG.md b/CHANGELOG.md index 0992a8e31..72ae37fa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - update `TypeResolver` interface to match with `GraphQLTypeResolver` from `graphql-js` - add basic support for directives with `@Directive()` decorator (#369) - add possibility to tune up the performance and disable auth & middlewares stack for simple field resolvers (#479) +- optimize resolvers execution paths to speed up a lot basic scenarios (#488) ### Fixes - refactor union types function syntax handling to prevent possible errors with circular refs - fix transforming and validating nested inputs and arrays (#462) diff --git a/benchmarks/array/graphql-js/async.ts b/benchmarks/array/graphql-js/async.ts new file mode 100644 index 000000000..1898f6584 --- /dev/null +++ b/benchmarks/array/graphql-js/async.ts @@ -0,0 +1,65 @@ +import { + GraphQLSchema, + GraphQLObjectType, + GraphQLString, + GraphQLNonNull, + GraphQLBoolean, + GraphQLInt, + GraphQLList, +} from "graphql"; + +import { runBenchmark, ARRAY_ITEMS } from "../run"; + +const SampleObjectType: GraphQLObjectType = new GraphQLObjectType({ + name: "SampleObject", + fields: () => ({ + stringField: { + type: new GraphQLNonNull(GraphQLString), + resolve: async source => { + return source.stringField; + }, + }, + numberField: { + type: new GraphQLNonNull(GraphQLInt), + resolve: async source => { + return source.numberField; + }, + }, + booleanField: { + type: new GraphQLNonNull(GraphQLBoolean), + resolve: async source => { + return source.booleanField; + }, + }, + nestedField: { + type: SampleObjectType, + resolve: async source => { + return source.nestedField; + }, + }, + }), +}); + +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: "Query", + fields: { + multipleNestedObjects: { + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SampleObjectType))), + resolve: () => + Array.from({ length: ARRAY_ITEMS }, (_, index) => ({ + stringField: "stringField", + booleanField: true, + numberField: index, + nestedField: { + stringField: "stringField", + booleanField: true, + numberField: index, + }, + })), + }, + }, + }), +}); + +runBenchmark(schema).catch(console.error); diff --git a/benchmarks/array/graphql-js.ts b/benchmarks/array/graphql-js/standard.ts similarity index 94% rename from benchmarks/array/graphql-js.ts rename to benchmarks/array/graphql-js/standard.ts index cbb121e63..c6e2b1259 100644 --- a/benchmarks/array/graphql-js.ts +++ b/benchmarks/array/graphql-js/standard.ts @@ -8,12 +8,12 @@ import { GraphQLList, } from "graphql"; -import { runBenchmark, ARRAY_ITEMS } from "./run"; +import { runBenchmark, ARRAY_ITEMS } from "../run"; const SampleObjectType: GraphQLObjectType = new GraphQLObjectType({ name: "SampleObject", fields: () => ({ - sampleField: { + stringField: { type: new GraphQLNonNull(GraphQLString), }, numberField: { diff --git a/benchmarks/array/results.txt b/benchmarks/array/results.txt index 8c1a3f8ac..6453b1a22 100644 --- a/benchmarks/array/results.txt +++ b/benchmarks/array/results.txt @@ -1,15 +1,34 @@ Core i7 2700K @ 3.5GHz Windows 10 x64 -10 000 array items | 100 iterations +25 000 array items | 50 iterations +Node.js v13.5 ----- -Node.js v13.2 +TypeGraphQL standard -- 42.551s +- 15.518s + +using sync field resolvers +- 18.180s + +using async field resolvers +- 39.934s + +using getters +- 31.207s + +standard with global middleware +- 62.664s with `simpleResolvers: true` -- 11.841s +- 14.980s + +----- +`graphql-js` + +standard +- 13.276s -graphql-js -- 9.963s +async field resolvers +- 25.630s diff --git a/benchmarks/array/run.ts b/benchmarks/array/run.ts index 608631c39..d9e70a115 100644 --- a/benchmarks/array/run.ts +++ b/benchmarks/array/run.ts @@ -1,8 +1,8 @@ import { GraphQLSchema, execute } from "graphql"; import { gql } from "apollo-server"; -const BENCHMARK_ITERATIONS = 100; -export const ARRAY_ITEMS = 10000; +const BENCHMARK_ITERATIONS = 50; +export const ARRAY_ITEMS = 25000; export async function runBenchmark(schema: GraphQLSchema) { const multipleNestedObjectsQuery = gql` diff --git a/benchmarks/array/type-graphql/async-field-resolvers.ts b/benchmarks/array/type-graphql/async-field-resolvers.ts new file mode 100644 index 000000000..e69011b7a --- /dev/null +++ b/benchmarks/array/type-graphql/async-field-resolvers.ts @@ -0,0 +1,78 @@ +import "reflect-metadata"; +import { + buildSchema, + Field, + ObjectType, + Resolver, + Query, + Int, + FieldResolver, + Root, +} from "../../../build/package/dist"; + +import { runBenchmark, ARRAY_ITEMS } from "../run"; + +@ObjectType() +class SampleObject { + @Field() + stringField!: string; + + @Field(type => Int) + numberField!: number; + + @Field() + booleanField!: boolean; + + @Field({ nullable: true }) + nestedField?: SampleObject; +} + +@Resolver(SampleObject) +class SampleResolver { + @Query(returns => [SampleObject]) + multipleNestedObjects(): SampleObject[] { + return Array.from( + { length: ARRAY_ITEMS }, + (_, index): SampleObject => ({ + stringField: "stringField", + booleanField: true, + numberField: index, + nestedField: { + stringField: "stringField", + booleanField: true, + numberField: index, + }, + }), + ); + } + + @FieldResolver() + async stringField(@Root() source: SampleObject) { + return source.stringField; + } + + @FieldResolver() + async numberField(@Root() source: SampleObject) { + return source.numberField; + } + + @FieldResolver() + async booleanField(@Root() source: SampleObject) { + return source.booleanField; + } + + @FieldResolver() + async nestedField(@Root() source: SampleObject) { + return source.nestedField; + } +} + +async function main() { + const schema = await buildSchema({ + resolvers: [SampleResolver], + }); + + await runBenchmark(schema); +} + +main().catch(console.error); diff --git a/benchmarks/array/type-graphql/simple-resolvers.ts b/benchmarks/array/type-graphql/simple-resolvers.ts new file mode 100644 index 000000000..f2a97750d --- /dev/null +++ b/benchmarks/array/type-graphql/simple-resolvers.ts @@ -0,0 +1,65 @@ +import "reflect-metadata"; +import { + buildSchema, + Field, + ObjectType, + Resolver, + Query, + Int, + MiddlewareFn, +} from "../../../build/package/dist"; + +import { runBenchmark, ARRAY_ITEMS } from "../run"; + +@ObjectType({ simpleResolvers: true }) +class SampleObject { + @Field() + stringField!: string; + + @Field(type => Int) + numberField!: number; + + @Field() + booleanField!: boolean; + + @Field({ nullable: true }) + nestedField?: SampleObject; +} + +@Resolver() +class SampleResolver { + @Query(returns => [SampleObject]) + multipleNestedObjects(): SampleObject[] { + return Array.from( + { length: ARRAY_ITEMS }, + (_, index): SampleObject => ({ + stringField: "stringField", + booleanField: true, + numberField: index, + nestedField: { + stringField: "stringField", + booleanField: true, + numberField: index, + }, + }), + ); + } +} + +const log = (...args: any[]) => void 0; // noop + +const loggingMiddleware: MiddlewareFn = ({ info }, next) => { + log(`${info.parentType.name}.${info.fieldName} accessed`); + return next(); +}; + +async function main() { + const schema = await buildSchema({ + resolvers: [SampleResolver], + globalMiddlewares: [loggingMiddleware], + }); + + await runBenchmark(schema); +} + +main().catch(console.error); diff --git a/benchmarks/array/type-graphql.ts b/benchmarks/array/type-graphql/standard.ts similarity index 88% rename from benchmarks/array/type-graphql.ts rename to benchmarks/array/type-graphql/standard.ts index ea5ab82fa..36d5fbb9c 100644 --- a/benchmarks/array/type-graphql.ts +++ b/benchmarks/array/type-graphql/standard.ts @@ -1,9 +1,9 @@ import "reflect-metadata"; -import { buildSchema, Field, ObjectType, Resolver, Query, Int } from "../../build/package"; +import { buildSchema, Field, ObjectType, Resolver, Query, Int } from "../../../build/package/dist"; -import { runBenchmark, ARRAY_ITEMS } from "./run"; +import { runBenchmark, ARRAY_ITEMS } from "../run"; -@ObjectType({ simpleResolvers: true }) +@ObjectType() class SampleObject { @Field() stringField!: string; diff --git a/benchmarks/array/type-graphql/sync-field-resolvers.ts b/benchmarks/array/type-graphql/sync-field-resolvers.ts new file mode 100644 index 000000000..1c9cc6e16 --- /dev/null +++ b/benchmarks/array/type-graphql/sync-field-resolvers.ts @@ -0,0 +1,78 @@ +import "reflect-metadata"; +import { + buildSchema, + Field, + ObjectType, + Resolver, + Query, + Int, + FieldResolver, + Root, +} from "../../../build/package/dist"; + +import { runBenchmark, ARRAY_ITEMS } from "../run"; + +@ObjectType() +class SampleObject { + @Field() + stringField!: string; + + @Field(type => Int) + numberField!: number; + + @Field() + booleanField!: boolean; + + @Field({ nullable: true }) + nestedField?: SampleObject; +} + +@Resolver(SampleObject) +class SampleResolver { + @Query(returns => [SampleObject]) + multipleNestedObjects(): SampleObject[] { + return Array.from( + { length: ARRAY_ITEMS }, + (_, index): SampleObject => ({ + stringField: "stringField", + booleanField: true, + numberField: index, + nestedField: { + stringField: "stringField", + booleanField: true, + numberField: index, + }, + }), + ); + } + + @FieldResolver() + stringField(@Root() source: SampleObject) { + return source.stringField; + } + + @FieldResolver() + numberField(@Root() source: SampleObject) { + return source.numberField; + } + + @FieldResolver() + booleanField(@Root() source: SampleObject) { + return source.booleanField; + } + + @FieldResolver() + nestedField(@Root() source: SampleObject) { + return source.nestedField; + } +} + +async function main() { + const schema = await buildSchema({ + resolvers: [SampleResolver], + }); + + await runBenchmark(schema); +} + +main().catch(console.error); diff --git a/benchmarks/array/type-graphql/sync-getters.ts b/benchmarks/array/type-graphql/sync-getters.ts new file mode 100644 index 000000000..3f9d6e19f --- /dev/null +++ b/benchmarks/array/type-graphql/sync-getters.ts @@ -0,0 +1,62 @@ +import "reflect-metadata"; +import { buildSchema, Field, ObjectType, Resolver, Query, Int } from "../../../build/package/dist"; + +import { runBenchmark, ARRAY_ITEMS } from "../run"; + +@ObjectType() +class SampleObject { + stringField!: string; + @Field({ name: "stringField" }) + get getStringField(): string { + return this.stringField; + } + + numberField!: number; + @Field(type => Int, { name: "numberField" }) + get getNumberField(): number { + return this.numberField; + } + + booleanField!: boolean; + @Field({ name: "booleanField" }) + get getBooleanField(): boolean { + return this.booleanField; + } + + nestedField?: SampleObject; + @Field(type => SampleObject, { name: "nestedField", nullable: true }) + get getNestedField(): SampleObject | undefined { + return this.nestedField; + } +} + +@Resolver(SampleObject) +class SampleResolver { + @Query(returns => [SampleObject]) + multipleNestedObjects(): SampleObject[] { + return Array.from( + { length: ARRAY_ITEMS }, + (_, index): SampleObject => + ({ + stringField: "stringField", + booleanField: true, + numberField: index, + nestedField: { + stringField: "stringField", + booleanField: true, + numberField: index, + } as SampleObject, + } as SampleObject), + ); + } +} + +async function main() { + const schema = await buildSchema({ + resolvers: [SampleResolver], + }); + + await runBenchmark(schema); +} + +main().catch(console.error); diff --git a/benchmarks/array/type-graphql/with-global-middleware.ts b/benchmarks/array/type-graphql/with-global-middleware.ts new file mode 100644 index 000000000..2b63a22dc --- /dev/null +++ b/benchmarks/array/type-graphql/with-global-middleware.ts @@ -0,0 +1,65 @@ +import "reflect-metadata"; +import { + buildSchema, + Field, + ObjectType, + Resolver, + Query, + Int, + MiddlewareFn, +} from "../../../build/package/dist"; + +import { runBenchmark, ARRAY_ITEMS } from "../run"; + +@ObjectType() +class SampleObject { + @Field() + stringField!: string; + + @Field(type => Int) + numberField!: number; + + @Field() + booleanField!: boolean; + + @Field({ nullable: true }) + nestedField?: SampleObject; +} + +@Resolver() +class SampleResolver { + @Query(returns => [SampleObject]) + multipleNestedObjects(): SampleObject[] { + return Array.from( + { length: ARRAY_ITEMS }, + (_, index): SampleObject => ({ + stringField: "stringField", + booleanField: true, + numberField: index, + nestedField: { + stringField: "stringField", + booleanField: true, + numberField: index, + }, + }), + ); + } +} + +const log = (...args: any[]) => void 0; // noop + +const loggingMiddleware: MiddlewareFn = ({ info }, next) => { + log(`${info.parentType.name}.${info.fieldName} accessed`); + return next(); +}; + +async function main() { + const schema = await buildSchema({ + resolvers: [SampleResolver], + globalMiddlewares: [loggingMiddleware], + }); + + await runBenchmark(schema); +} + +main().catch(console.error); diff --git a/docs/performance.md b/docs/performance.md index 9cba9a1fe..ece807b53 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -10,11 +10,33 @@ While this enable easy and convenient development, it's sometimes a tradeoff in To measure the overhead of the abstraction, a few demo examples were made to compare the usage of TypeGraphQL against the implementations using "bare metal" - raw `graphql-js` library. The benchmarks are located in a [folder on the GitHub repo](https://github.com/MichalLytek/type-graphql/tree/master/benchmarks). -The most demanding cases like returning an array of 10 000 nested objects shows that it's about 4 times slower. In real apps (e.g. with complex database queries) it's usually a much lower factor but still not negligible. That's why TypeGraphQL has some built-in performance optimization options. +The most demanding cases like returning an array of 25 000 nested objects showed that in some cases it might be about 5 times slower. + +| | 25 000 array items | Deeply nested object | +| -------------------- | :----------------: | :------------------: | +| Standard TypeGraphQL | 1253.28 ms | 45.57 μs | +| `graphql-js` | 265.52 ms | 24.22 μs | + +In real apps (e.g. with complex database queries) it's usually a much lower factor but still not negligible. That's why TypeGraphQL has some built-in performance optimization options. ## Optimizations -When we have a query that returns a huge amount of JSON-like data and we don't need any field-level access control, we can turn off the whole authorization and middlewares stack for selected field resolvers using a `{ simple: true }` decorator option, e.g.: +Promises in JS have a quite big performance overhead. In the same example of returning an array with 25 000 items, if we change the Object Type field resolvers to an asynchronous one that return a promise, the execution slows down by a half even in "raw" `graphql-js`. + +| `graphql-js` | 25 000 array items | +| --------------- | :----------------: | +| sync resolvers | 265.52 ms | +| async resolvers | 512.61 ms | + +TypeGraphQL tries to avoid the async execution path when it's possible, e.g. if the query/mutation/field resolver doesn't use the auth feature, doesn't use args (or has args validation disabled) and if doesn't return a promise. So if you find a bottleneck in your app, try to investigate your resolvers, disable not used features and maybe remove some unnecessary async/await usage. + +Also, using middlewares implicitly turns on the async execution path (for global middlewares the middlewares stack is created even for every implicit field resolver!), so be careful when using this feature if you care about the performance very much (and maybe then use the "simple resolvers" tweak described below). + +The whole middleware stack will be soon redesigned with a performance in mind and with a new API that will also allow fine-grained scoping of global middlewares. Stay tuned! + +## Further performance tweaks + +When we have a query that returns a huge amount of JSON-like data and we don't need any field-level access control or other custom middlewares, we can turn off the whole authorization and middlewares stack for selected field resolver using a `{ simple: true }` decorator option, e.g.: ```typescript @ObjectType() @@ -22,20 +44,12 @@ class SampleObject { @Field() sampleField: string; - @Field(type => [SuperComplexObject], { simple: true }) - superComplexData: SuperComplexObject[]; + @Field({ simple: true }) + publicFrequentlyQueriedField: SomeType; } ``` -This simple trick can speed up the execution up to 72%! The benchmarks show that using simple resolvers allows for as fast execution as with bare `graphql-js` - the measured overhead is only about ~20%, which is a much more reasonable value than 400%. Below you can see [the benchmarks results](https://github.com/MichalLytek/type-graphql/tree/master/benchmarks): - -| | 10 000 array items | Deeply nested object | -| ---------------------------------- | :----------------: | :------------------: | -| Standard TypeGraphQL | 42.551s | 4.557s | -| `graphql-js` | 9.963s | 2.422s | -| TypeGraphQL with "simpleResolvers" | 11.841s | 3.086s | - -Moreover, you can also apply this behavior for all the fields of the object type by using a `{ simpleResolvers: true }` decorator option, e.g.: +Moreover, we can also apply this behavior for all the fields of the object type by using a `{ simpleResolvers: true }` decorator option, e.g.: ```typescript @ObjectType({ simpleResolvers: true }) @@ -51,8 +65,17 @@ class Post { } ``` -### Caution +This simple trick can speed up the execution up to 76%! The benchmarks show that using simple resolvers allows for as fast execution as with bare `graphql-js` - the measured overhead is only about ~13%, which is a much more reasonable value than 500%. Below you can see [the benchmarks results](https://github.com/MichalLytek/type-graphql/tree/master/benchmarks): + +| | 25 000 array items | +| ----------------------------------------------------------------------------- | :----------------: | +| `graphql-js` | 265.52 ms | +| Standard TypeGraphQL | 310.36 ms | +| TypeGraphQL with a global middleware | 1253.28 ms | +| **TypeGraphQL with "simpleResolvers" applied
(and a global middleware)** | **299.61 ms** | + +> This optimization **is not turned on by default** mostly because of the global middlewares and authorization feature. -This optimization **is not turned on by default** mostly because of the global middlewares and authorization feature. By using "simple resolvers" we are turning them off, so we have to be aware of the consequences - `@Authorized` guard on fields won't work for that fields so they will be publicly available, as well as global middlewares won't be executed for that fields, so we might lost, for example, performance metrics or access logs. +By using "simple resolvers" we are turning them off, so we have to be aware of the consequences - `@Authorized` guard on fields won't work for that fields so they will be publicly available, as well as global middlewares won't be executed for that fields, so we might lost, for example, performance metrics or access logs. -That's why we should **be really careful with using this feature**. The rule of thumb is to use "simple resolvers" only when it's really needed, like returning huge array of nested objects. +That's why we should **be really careful with using this tweak**. The rule of thumb is to use "simple resolvers" only when it's really needed, like returning huge array of nested objects. diff --git a/src/resolvers/create.ts b/src/resolvers/create.ts index e161d103b..c74871891 100644 --- a/src/resolvers/create.ts +++ b/src/resolvers/create.ts @@ -9,6 +9,7 @@ import { getParams, applyMiddlewares, applyAuthChecker } from "./helpers"; import { convertToType } from "../helpers/types"; import { BuildContext } from "../schema/build-context"; import { ResolverData } from "../interfaces"; +import isPromiseLike from "../utils/isPromiseLike"; export function createHandlerResolver( resolverMetadata: BaseResolverMetadata, @@ -24,17 +25,23 @@ export function createHandlerResolver( const middlewares = globalMiddlewares.concat(resolverMetadata.middlewares!); applyAuthChecker(middlewares, authMode, authChecker, resolverMetadata.roles); - return async (root, args, context, info) => { + return (root, args, context, info) => { const resolverData: ResolverData = { root, args, context, info }; const targetInstance = container.getInstance(resolverMetadata.target, resolverData); - return applyMiddlewares(container, resolverData, middlewares, async () => { - const params: any[] = await getParams( + return applyMiddlewares(container, resolverData, middlewares, () => { + const params: Promise | any[] = getParams( resolverMetadata.params!, resolverData, globalValidate, pubSub, ); - return targetInstance[resolverMetadata.methodName].apply(targetInstance, params); + if (isPromiseLike(params)) { + return params.then(resolvedParams => + targetInstance[resolverMetadata.methodName].apply(targetInstance, resolvedParams), + ); + } else { + return targetInstance[resolverMetadata.methodName].apply(targetInstance, params); + } }); }; } @@ -86,13 +93,8 @@ export function createBasicFieldResolver( const middlewares = globalMiddlewares.concat(fieldMetadata.middlewares!); applyAuthChecker(middlewares, authMode, authChecker, fieldMetadata.roles); - return async (root, args, context, info) => { + return (root, args, context, info) => { const resolverData: ResolverData = { root, args, context, info }; - return await applyMiddlewares( - container, - resolverData, - middlewares, - () => root[fieldMetadata.name], - ); + return applyMiddlewares(container, resolverData, middlewares, () => root[fieldMetadata.name]); }; } diff --git a/src/resolvers/helpers.ts b/src/resolvers/helpers.ts index cf0f32b52..b3893e0ba 100644 --- a/src/resolvers/helpers.ts +++ b/src/resolvers/helpers.ts @@ -9,55 +9,59 @@ import { Middleware, MiddlewareFn, MiddlewareClass } from "../interfaces/Middlew import { IOCContainer } from "../utils/container"; import { AuthMiddleware } from "../helpers/auth-middleware"; import { convertArgsToInstance, convertArgToInstance } from "./convert-args"; +import isPromiseLike from "../utils/isPromiseLike"; -export async function getParams( +export function getParams( params: ParamMetadata[], resolverData: ResolverData, globalValidate: boolean | ValidatorOptions, pubSub: PubSubEngine, -): Promise { - return Promise.all( - params - .sort((a, b) => a.index - b.index) - .map(async paramInfo => { - switch (paramInfo.kind) { - case "args": - return await validateArg( - convertArgsToInstance(paramInfo, resolverData.args), - globalValidate, - paramInfo.validate, - ); - case "arg": - return await validateArg( - convertArgToInstance(paramInfo, resolverData.args), - globalValidate, - paramInfo.validate, - ); - case "context": - if (paramInfo.propertyName) { - return resolverData.context[paramInfo.propertyName]; - } - return resolverData.context; - case "root": - const rootValue = paramInfo.propertyName - ? resolverData.root[paramInfo.propertyName] - : resolverData.root; - if (!paramInfo.getType) { - return rootValue; - } - return convertToType(paramInfo.getType(), rootValue); - case "info": - return resolverData.info; - case "pubSub": - if (paramInfo.triggerKey) { - return (payload: any) => pubSub.publish(paramInfo.triggerKey!, payload); - } - return pubSub; - case "custom": - return await paramInfo.resolver(resolverData); - } - }), - ); +): Promise | any[] { + const paramValues = params + .sort((a, b) => a.index - b.index) + .map(paramInfo => { + switch (paramInfo.kind) { + case "args": + return validateArg( + convertArgsToInstance(paramInfo, resolverData.args), + globalValidate, + paramInfo.validate, + ); + case "arg": + return validateArg( + convertArgToInstance(paramInfo, resolverData.args), + globalValidate, + paramInfo.validate, + ); + case "context": + if (paramInfo.propertyName) { + return resolverData.context[paramInfo.propertyName]; + } + return resolverData.context; + case "root": + const rootValue = paramInfo.propertyName + ? resolverData.root[paramInfo.propertyName] + : resolverData.root; + if (!paramInfo.getType) { + return rootValue; + } + return convertToType(paramInfo.getType(), rootValue); + case "info": + return resolverData.info; + case "pubSub": + if (paramInfo.triggerKey) { + return (payload: any) => pubSub.publish(paramInfo.triggerKey!, payload); + } + return pubSub; + case "custom": + return paramInfo.resolver(resolverData); + } + }); + if (paramValues.some(isPromiseLike)) { + return Promise.all(paramValues); + } else { + return paramValues; + } } export function applyAuthChecker( @@ -71,12 +75,15 @@ export function applyAuthChecker( } } -export async function applyMiddlewares( +export function applyMiddlewares( container: IOCContainer, resolverData: ResolverData, middlewares: Array>, resolverHandlerFunction: () => any, ): Promise { + if (middlewares.length === 0) { + return resolverHandlerFunction(); + } let middlewaresIndex = -1; async function dispatchHandler(currentIndex: number): Promise { if (currentIndex <= middlewaresIndex) { diff --git a/src/utils/isPromiseLike.ts b/src/utils/isPromiseLike.ts new file mode 100644 index 000000000..1a9743f3b --- /dev/null +++ b/src/utils/isPromiseLike.ts @@ -0,0 +1,5 @@ +export default function isPromiseLike( + value: PromiseLike | TValue, +): value is PromiseLike { + return value != null && typeof (value as PromiseLike).then === "function"; +}