Skip to content

Commit

Permalink
enable batch execution (#1965)
Browse files Browse the repository at this point in the history
WIP:

When `batch` is set to true for a given subschemaConfig, batches all delegated root fields into a combined request passed to the executor. Moreover, batches all requests to a given subschema into the minimum number of requests, collecting queries and mutations separately, preserving operation order. Distributes properly pathed errors to the originating requests.

Adapted from Gatsby query batcher by @vladar.

Caveats:
* Uses a Dataloader under the hood, which is created anew upon each request -- relies on a unique context argument per request to make this happen!
* Does not pass `info` argument to the batched executor call, disabling any executors that used extensions annotated onto info.

TODO:
- Add testing!
- Extensions support should be added by a custom option?

Related:

gatsbyjs/gatsby#22347 (comment)
#1710 (comment)
#1959 (comment)
#1954
  • Loading branch information
yaacovCR committed Aug 31, 2020
1 parent 7fe5997 commit 53d9833
Show file tree
Hide file tree
Showing 9 changed files with 530 additions and 2 deletions.
3 changes: 2 additions & 1 deletion packages/delegate/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
"@graphql-tools/schema": "6.1.0",
"@graphql-tools/utils": "6.1.0",
"@ardatan/aggregate-error": "0.0.1",
"dataloader": "2.0.0",
"is-promise": "4.0.0",
"tslib": "~2.0.1"
},
"publishConfig": {
"access": "public",
"directory": "dist"
}
}
}
7 changes: 6 additions & 1 deletion packages/delegate/src/delegateToSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { createRequestFromInfo, getDelegatingOperation } from './createRequest';
import { Transformer } from './Transformer';

import AggregateError from '@ardatan/aggregate-error';
import { getBatchingExecutor } from './getBatchingExecutor';

export function delegateToSchema(options: IDelegateToSchemaOptions | GraphQLSchema): any {
if (isSchema(options)) {
Expand Down Expand Up @@ -165,9 +166,13 @@ export function delegateRequest({
}

if (targetOperation === 'query' || targetOperation === 'mutation') {
const executor =
let executor =
subschemaConfig?.executor || createDefaultExecutor(targetSchema, subschemaConfig?.rootValue || targetRootValue);

if (subschemaConfig?.batch) {
executor = getBatchingExecutor(context, subschemaConfig, executor);
}

const executionResult = executor({
document: processedRequest.document,
variables: processedRequest.variables,
Expand Down
71 changes: 71 additions & 0 deletions packages/delegate/src/getBatchingExecutor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { getOperationAST } from 'graphql';

import isPromise from 'is-promise';

import DataLoader from 'dataloader';

import { ExecutionResult, Request } from '@graphql-tools/utils';

import { SubschemaConfig, ExecutionParams } from './types';
import { memoize2of3 } from './memoize';
import { mergeRequests } from './mergeRequests';
import { splitResult } from './splitResult';

export const getBatchingExecutor = memoize2of3(function (
_context: Record<string, any>,
_subschemaConfig: SubschemaConfig,
executor: ({ document, context, variables, info }: ExecutionParams) => ExecutionResult | Promise<ExecutionResult>
) {
const loader = new DataLoader(createLoadFn(executor));
return (request: Request) => loader.load(request);
});

function createLoadFn(
executor: ({ document, context, variables, info }: ExecutionParams) => ExecutionResult | Promise<ExecutionResult>
) {
return async (requests: Array<Request>): Promise<Array<ExecutionResult>> => {
const requestBatches: Array<Array<Request>> = [];
let index = 0;
const request = requests[index];
let currentBatch: Array<Request> = [request];
requestBatches.push(currentBatch);
const operationType = getOperationAST(request.document).operation;
while (++index < requests.length) {
const currentOperationType = getOperationAST(requests[index].document).operation;
if (operationType === currentOperationType) {
currentBatch.push(requests[index]);
} else {
currentBatch = [requests[index]];
requestBatches.push(currentBatch);
}
}

let containsPromises = false;
const executionResults: Array<ExecutionResult | Promise<ExecutionResult>> = [];
requestBatches.forEach(requestBatch => {
const mergedRequest = mergeRequests(requestBatch);
const executionResult = executor(mergedRequest);

if (isPromise(executionResult)) {
containsPromises = true;
}
executionResults.push(executionResult);
});

if (containsPromises) {
return Promise.all(executionResults).then(resultBatches => {
let results: Array<ExecutionResult> = [];
resultBatches.forEach((resultBatch, index) => {
results = results.concat(splitResult(resultBatch, requestBatches[index].length));
});
return results;
});
}

let results: Array<ExecutionResult> = [];
(executionResults as Array<ExecutionResult>).forEach((resultBatch, index) => {
results = results.concat(splitResult(resultBatch, requestBatches[index].length));
});
return results;
};
}
40 changes: 40 additions & 0 deletions packages/delegate/src/memoize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,43 @@ export function memoize2<T1 extends Record<string, any>, T2 extends Record<strin

return memoized;
}

export function memoize2of3<
T1 extends Record<string, any>,
T2 extends Record<string, any>,
T3 extends any,
R extends any
>(fn: (A1: T1, A2: T2, A3: T3) => R): (A1: T1, A2: T2, A3: T3) => R {
let cache1: WeakMap<T1, WeakMap<T2, R>>;

function memoized(a1: T1, a2: T2, a3: T3) {
if (!cache1) {
cache1 = new WeakMap();
const cache2: WeakMap<T2, R> = new WeakMap();
cache1.set(a1, cache2);
const newValue = fn(a1, a2, a3);
cache2.set(a2, newValue);
return newValue;
}

let cache2 = cache1.get(a1);
if (!cache2) {
cache2 = new WeakMap();
cache1.set(a1, cache2);
const newValue = fn(a1, a2, a3);
cache2.set(a2, newValue);
return newValue;
}

const cachedValue = cache2.get(a2);
if (cachedValue === undefined) {
const newValue = fn(a1, a2, a3);
cache2.set(a2, newValue);
return newValue;
}

return cachedValue;
}

return memoized;
}
Loading

0 comments on commit 53d9833

Please sign in to comment.