From 850fa563c535729a4d5935ef2ba4509800993d35 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Tue, 23 Nov 2021 12:26:53 +0100 Subject: [PATCH 1/3] Expose `createSourceEventStream` Add `createSourceEventStream` to the `CompiledQuery`. Signed-off-by: Georg Bremer --- src/execution.ts | 99 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/src/execution.ts b/src/execution.ts index b1fc6e7e..d99c5c21 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -180,6 +180,11 @@ export interface CompiledQuery< ) => Promise< AsyncIterableIterator> | ExecutionResult >; + createSourceEventStream?: ( + root: any, + context: any, + variables?: Maybe, + ) => Promise | ExecutionResult>; stringify: (v: any) => string; } @@ -274,19 +279,26 @@ export function compileQuery< }; if (context.operation.operation === "subscription") { + const createSourceEventStream = compileSourceEventStream( + context, + type, + fieldMap, + ); + const subscribe = compileSubscription( + createSourceEventStream, + compiledQuery.query + ); + compiledQuery.createSourceEventStream = createBoundCreateSourceEventStream( + context, + createSourceEventStream, + getVariables, + context.operation.name?.value + ); compiledQuery.subscribe = createBoundSubscribe( context, - document, - compileSubscriptionOperation( - context, - type, - fieldMap, - compiledQuery.query - ), + subscribe, getVariables, - context.operation.name != null - ? context.operation.name.value - : undefined + context.operation.name?.value ); } @@ -1678,11 +1690,10 @@ export function isAsyncIterable( return typeof Object(val)[Symbol.asyncIterator] === "function"; } -function compileSubscriptionOperation( +function compileSourceEventStream( context: CompilationContext, type: GraphQLObjectType, - fieldMap: FieldsAndNodes, - queryFn: CompiledQuery["query"] + fieldMap: FieldsAndNodes ) { const fieldNodes = Object.values(fieldMap)[0]; const fieldNode = fieldNodes[0]; @@ -1729,7 +1740,7 @@ function compileSubscriptionOperation( } } - async function createSourceEventStream(executionContext: ExecutionContext) { + return async function createSourceEventStream(executionContext: ExecutionContext) { try { const eventStream = await executeSubscription(executionContext); @@ -1751,7 +1762,12 @@ function compileSubscriptionOperation( throw error; } } +} +function compileSubscription( + createSourceEventStream: (executionContext: ExecutionContext) => Promise | { errors: GraphQLError[] }>, + queryFn: CompiledQuery["query"] +) { return async function subscribe(executionContext: ExecutionContext) { const resultOrStream = await createSourceEventStream(executionContext); @@ -1773,9 +1789,62 @@ function compileSubscriptionOperation( }; } +function createBoundCreateSourceEventStream( + compilationContext: CompilationContext, + func: ( + context: ExecutionContext + ) => Promise | ExecutionResult>, + getVariableValues: (inputs: { [key: string]: any }) => CoercedVariableValues, + operationName: string | undefined +): CompiledQuery["createSourceEventStream"] { + const { resolvers, typeResolvers, isTypeOfs, serializers, resolveInfos } = + compilationContext; + const trimmer = createNullTrimmer(compilationContext); + const fnName = operationName || "subscribe"; + + const ret = { + async [fnName]( + rootValue: any, + context: any, + variables: Maybe<{ [key: string]: any }> + ): Promise | ExecutionResult> { + // this can be shared across in a batch request + const parsedVariables = getVariableValues(variables || {}); + + // Return early errors if variable coercing failed. + if (failToParseVariables(parsedVariables)) { + return { errors: parsedVariables.errors }; + } + + const executionContext: ExecutionContext = { + rootValue, + context, + variables: parsedVariables.coerced, + safeMap, + inspect, + GraphQLError: GraphqlJitError, + resolvers, + typeResolvers, + isTypeOfs, + serializers, + resolveInfos, + trimmer, + promiseCounter: 0, + nullErrors: [], + errors: [], + data: {} + }; + + // eslint-disable-next-line no-useless-call + return func.call(null, executionContext); + } + }; + + return ret[fnName]; +} + function createBoundSubscribe( compilationContext: CompilationContext, - document: DocumentNode, func: ( context: ExecutionContext ) => Promise | ExecutionResult>, From b8a1412d9243449bf4938884e06eed9d08d883fb Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Mon, 6 Dec 2021 17:13:40 +0100 Subject: [PATCH 2/3] Add benchmark for createEventSourceStream Add a benchmark to show it's worth exposing the function. Signed-off-by: Georg Bremer --- src/__benchmarks__/benchmarks.ts | 10 +- src/__benchmarks__/createSourceEventStream.ts | 156 ++++++++++++++++++ 2 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 src/__benchmarks__/createSourceEventStream.ts diff --git a/src/__benchmarks__/benchmarks.ts b/src/__benchmarks__/benchmarks.ts index 01615467..6465805c 100644 --- a/src/__benchmarks__/benchmarks.ts +++ b/src/__benchmarks__/benchmarks.ts @@ -1,5 +1,6 @@ import Benchmark from "benchmark"; import { + createSourceEventStream, DocumentNode, execute, getIntrospectionQuery, @@ -7,6 +8,7 @@ import { parse } from "graphql"; import { compileQuery, isCompiledQuery, isPromise } from "../execution"; +import { benchmarkCreateSourceEventStream } from "./createSourceEventStream"; import { query as fewResolversQuery, schema as fewResolversSchema @@ -51,8 +53,8 @@ const benchmarks: { [key: string]: BenchmarkMaterial } = { async function runBenchmarks() { const skipJS = process.argv[2] === "skip-js"; const skipJSON = process.argv[2] === "skip-json"; - const benchs = await Promise.all( - Object.entries(benchmarks).map( + const benchs = await Promise.all([ + ...Object.entries(benchmarks).map( async ([bench, { query, schema, variables }]) => { const compiledQuery = compileQuery(schema, query, undefined, { debug: true @@ -145,7 +147,9 @@ async function runBenchmarks() { }); return suite; } - ) + ), + benchmarkCreateSourceEventStream(), + ] ); const benchsToRun = benchs.filter(isNotNull); diff --git a/src/__benchmarks__/createSourceEventStream.ts b/src/__benchmarks__/createSourceEventStream.ts new file mode 100644 index 00000000..a47e1504 --- /dev/null +++ b/src/__benchmarks__/createSourceEventStream.ts @@ -0,0 +1,156 @@ +import Benchmark from "benchmark"; +import { + createSourceEventStream, + DocumentNode, + execute, + getIntrospectionQuery, + GraphQLBoolean, + GraphQLID, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + parse + +} from "graphql"; +import { compileQuery, isCompiledQuery, isPromise } from "../execution"; +import { +} from "graphql"; + +const schema = function schema() { + const BlogArticle: GraphQLObjectType = new GraphQLObjectType({ + name: "Article", + fields: { + id: { type: new GraphQLNonNull(GraphQLID) }, + isPublished: { type: GraphQLBoolean }, + title: { type: GraphQLString }, + body: { type: GraphQLString }, + keywords: { type: new GraphQLList(GraphQLString) } + } + }); + + const BlogQuery = new GraphQLObjectType({ + name: "Query", + fields: { + article: { + type: BlogArticle, + args: { id: { type: GraphQLID } }, + resolve: (_, { id }) => article(id) + }, + } + }); + const BlogSubscription = new GraphQLObjectType({ + name: "Subscription", + fields: { + news: { + type: BlogArticle, + args: {}, + subscribe: async () => { + const it: AsyncIterable = { + [Symbol.asyncIterator]() { + return { + next: async () => ({done: true, value: undefined}) + } + }, + } + return it; + } + }, + } + }); + + function article(id: number): any { + return { + id, + isPublished: true, + title: "My Article " + id, + body: "This is a post", + hidden: "This data is not exposed in the schema", + keywords: ["foo", "bar", 1, true, null] + }; + } + + return new GraphQLSchema({ + query: BlogQuery, + subscription: BlogSubscription + }); +}() + +const subscription = parse(` +subscription { + news { + ...articleFields, + } +} + +fragment articleFields on Article { + __typename + id, + isPublished, + title, + body, + hidden, + notdefined +} +`); + +const skipJS = false; + +export function benchmarkCreateSourceEventStream() { + const compiledQuery = compileQuery(schema, subscription, undefined, { + debug: true + } as any); + if (!isCompiledQuery(compiledQuery) || !compiledQuery.createSourceEventStream) { + // eslint-disable-next-line no-console + console.error(`failed to compile`); + return null; + } + const suite = new Benchmark.Suite('createSourceEventStream'); + if (!skipJS) { + suite.add("graphql-js", { + minSamples: 150, + defer: true, + fn(deferred: any) { + const stream = createSourceEventStream( + schema, + subscription, + {}, + ); + if (isPromise(stream)) { + return stream.then((res) => + deferred.resolve(res) + ); + } + return deferred.resolve() + } + }); + } + suite + .add("graphql-jit", { + minSamples: 150, + defer: true, + fn(deferred: any) { + const stream = compiledQuery.createSourceEventStream!( + {}, + undefined + ); + if (isPromise(stream)) { + return stream.then((res) => + deferred.resolve(res) + ); + } + return deferred.resolve() + } + }) + // add listeners + .on("cycle", (event: any) => { + // eslint-disable-next-line no-console + console.log(String(event.target)); + }) + .on("start", () => { + // eslint-disable-next-line no-console + console.log("Starting createSourceEventStream"); + }); + return suite; +} From fc538ff4b30dcbd6979085d608eeea858e9cd988 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Tue, 7 Dec 2021 09:08:50 +0100 Subject: [PATCH 3/3] Move compile createSourceEventStream out of compileQuery The use case for createSourceEventStream is running the event stream and resolvers in different processes. That means compiling the resolvers is not necessary and just costs performance. Signed-off-by: Georg Bremer --- src/__benchmarks__/createSourceEventStream.ts | 15 +-- src/execution.ts | 117 +++++++++++++++--- src/index.ts | 4 +- 3 files changed, 112 insertions(+), 24 deletions(-) diff --git a/src/__benchmarks__/createSourceEventStream.ts b/src/__benchmarks__/createSourceEventStream.ts index a47e1504..3dd958ba 100644 --- a/src/__benchmarks__/createSourceEventStream.ts +++ b/src/__benchmarks__/createSourceEventStream.ts @@ -1,9 +1,6 @@ import Benchmark from "benchmark"; import { createSourceEventStream, - DocumentNode, - execute, - getIntrospectionQuery, GraphQLBoolean, GraphQLID, GraphQLList, @@ -14,9 +11,8 @@ import { parse } from "graphql"; -import { compileQuery, isCompiledQuery, isPromise } from "../execution"; -import { -} from "graphql"; +import { isPromise } from "../execution"; +import { compileSourceEventStream } from ".."; const schema = function schema() { const BlogArticle: GraphQLObjectType = new GraphQLObjectType({ @@ -95,13 +91,14 @@ fragment articleFields on Article { } `); +// TODO const skipJS = false; export function benchmarkCreateSourceEventStream() { - const compiledQuery = compileQuery(schema, subscription, undefined, { + const compiledQuery = compileSourceEventStream(schema, subscription, undefined, { debug: true } as any); - if (!isCompiledQuery(compiledQuery) || !compiledQuery.createSourceEventStream) { + if (!compiledQuery) { // eslint-disable-next-line no-console console.error(`failed to compile`); return null; @@ -131,7 +128,7 @@ export function benchmarkCreateSourceEventStream() { minSamples: 150, defer: true, fn(deferred: any) { - const stream = compiledQuery.createSourceEventStream!( + const stream = compiledQuery( {}, undefined ); diff --git a/src/execution.ts b/src/execution.ts index d99c5c21..551d7bd1 100644 --- a/src/execution.ts +++ b/src/execution.ts @@ -180,14 +180,17 @@ export interface CompiledQuery< ) => Promise< AsyncIterableIterator> | ExecutionResult >; - createSourceEventStream?: ( - root: any, - context: any, - variables?: Maybe, - ) => Promise | ExecutionResult>; stringify: (v: any) => string; } +export type CreateSourceEventStream< + TVariables = { [key: string]: any } +> = ( + root: any, + context: any, + variables?: Maybe, +) => Promise | ExecutionResult>; + interface InternalCompiledQuery extends CompiledQuery { __DO_NOT_USE_THIS_OR_YOU_WILL_BE_FIRED_compilation?: string; } @@ -279,7 +282,7 @@ export function compileQuery< }; if (context.operation.operation === "subscription") { - const createSourceEventStream = compileSourceEventStream( + const createSourceEventStream = compileSourceEventStreamOperation( context, type, fieldMap, @@ -288,12 +291,6 @@ export function compileQuery< createSourceEventStream, compiledQuery.query ); - compiledQuery.createSourceEventStream = createBoundCreateSourceEventStream( - context, - createSourceEventStream, - getVariables, - context.operation.name?.value - ); compiledQuery.subscribe = createBoundSubscribe( context, subscribe, @@ -316,6 +313,98 @@ export function compileQuery< } } +/** + * It compiles a GraphQL query to an executable function + * @param {GraphQLSchema} schema GraphQL schema + * @param {DocumentNode} document Query being submitted + * @param {string} operationName name of the operation + * @param partialOptions compilation options to tune the compiler features + * @returns {CompiledQuery} the cacheable result + */ +export function compileSourceEventStream< + TResult = { [key: string]: any }, + TVariables = { [key: string]: any } +>( + schema: GraphQLSchema, + document: TypedDocumentNode, + operationName?: string, + partialOptions?: Partial +): CreateSourceEventStream { + if (!schema) { + throw new Error(`Expected ${schema} to be a GraphQL schema.`); + } + if (!document) { + throw new Error("Must provide document."); + } + + if ( + partialOptions && + partialOptions.resolverInfoEnricher && + typeof partialOptions.resolverInfoEnricher !== "function" + ) { + throw new Error("resolverInfoEnricher must be a function"); + } + const options = { + disablingCapturingStackErrors: false, + customJSONSerializer: false, + disableLeafSerialization: false, + customSerializers: {}, + ...partialOptions + }; + + // If a valid context cannot be created due to incorrect arguments, + // a "Response" with only errors is returned. + const context = buildCompilationContext( + schema, + document, + options, + operationName + ); + + const getVariables = compileVariableParsing( + schema, + context.operation.variableDefinitions || [] + ); + + const type = getOperationRootType(context.schema, context.operation); + const fieldMap = collectFields( + context, + type, + context.operation.selectionSet, + Object.create(null), + Object.create(null) + ); + + context.deferred.forEach((deferredField) => { + compileDeferredField(context, deferredField) + }); + + // TODO refactor executeSubscription instead of just preparing everything for it + const fieldNodes = Object.values(fieldMap)[0]; + const fieldNode = fieldNodes[0]; + const fieldName = fieldNode.name.value; + const field = resolveFieldDef(context, type, fieldNodes); + + const responsePath = addPath(undefined, fieldName); + getExecutionInfo( context, type, field!.type, fieldName, fieldNodes, responsePath) + // end hack + + if (context.operation.operation !== "subscription") { + throw new Error("Operation must be a subscription"); + } + const createSourceEventStream = compileSourceEventStreamOperation( + context, + type, + fieldMap, + ); + return createBoundCreateSourceEventStream( + context, + createSourceEventStream, + getVariables, + context.operation.name?.value + ); +} + export function isCompiledQuery< C extends CompiledQuery, E extends ExecutionResult @@ -1690,7 +1779,7 @@ export function isAsyncIterable( return typeof Object(val)[Symbol.asyncIterator] === "function"; } -function compileSourceEventStream( +function compileSourceEventStreamOperation( context: CompilationContext, type: GraphQLObjectType, fieldMap: FieldsAndNodes @@ -1796,7 +1885,7 @@ function createBoundCreateSourceEventStream( ) => Promise | ExecutionResult>, getVariableValues: (inputs: { [key: string]: any }) => CoercedVariableValues, operationName: string | undefined -): CompiledQuery["createSourceEventStream"] { +): CreateSourceEventStream { const { resolvers, typeResolvers, isTypeOfs, serializers, resolveInfos } = compilationContext; const trimmer = createNullTrimmer(compilationContext); diff --git a/src/index.ts b/src/index.ts index c93f08a1..82265f96 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,9 @@ export { compileQuery, isCompiledQuery, CompilerOptions, - CompiledQuery + CompiledQuery, + CreateSourceEventStream, + compileSourceEventStream } from "./execution"; export {