Skip to content

Instantly share code, notes, and snippets.

@lenolib
Last active August 14, 2022 21:37
Show Gist options
  • Save lenolib/e801737a949f810fdc2f1dc64926ebd8 to your computer and use it in GitHub Desktop.
Save lenolib/e801737a949f810fdc2f1dc64926ebd8 to your computer and use it in GitHub Desktop.
Monkey patches graphql execute in order to not block event loop by processing long arrays in chunks in completeListValue
# Related graphql-js issue: https://github.com/graphql/graphql-js/issues/2262
const rewire = require('rewire');
const rewiredExecuteModule = rewire('graphql/execution/execute');
const completeValue = rewiredExecuteModule.__get__('completeValue');
const handleFieldError = rewiredExecuteModule.__get__('handleFieldError');
const { GraphQLError } = require('graphql');
const { addPath, pathToArray } = require('graphql/jsutils/Path');
const { locatedError } = require('graphql/error/locatedError');
const _ = require('lodash');
const completeValueOrError = (
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item
) => {
try {
let completedItem;
if (item instanceof Promise) {
// eslint-disable-next-line no-loop-func
completedItem = item.then(resolved =>
completeValue(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
resolved
)
);
} else {
completedItem = completeValue(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item
);
}
if (completedItem instanceof Promise) {
// Note: we don't rely on a `catch` method, but we do expect "thenable"
// to take a second callback for the error case.
// eslint-disable-next-line no-loop-func
return completedItem.then(undefined, rawError => {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
return handleFieldError(error, itemType, exeContext);
});
}
return completedItem;
} catch (rawError) {
const error = locatedError(rawError, fieldNodes, pathToArray(itemPath));
return handleFieldError(error, itemType, exeContext);
}
};
const chunkifyPromises = (
alreadyCompletedFirstChunkItems,
allItems,
callback // takes (chunk, chunkIdx)
) => {
const chunkSize = alreadyCompletedFirstChunkItems.length;
const startIdx = chunkSize;
const returnPromise = _.chunk(allItems.slice(startIdx), chunkSize).reduce(
(prevPromise, chunk, chunkIdx) =>
prevPromise.then(
async reductionResults =>
await Promise.all(
await new Promise(resolve =>
setImmediate(() =>
resolve(reductionResults.concat(callback(chunk, chunkIdx)))
)
)
)
),
Promise.all(alreadyCompletedFirstChunkItems)
);
return returnPromise;
};
const CHUNKIFY_THRESHOLD_MILLIS = 50;
// eslint-disable-next-line max-lines-per-function
function completeListValueChunked(
exeContext,
returnType,
fieldNodes,
info,
path,
result
) {
if (!_.isArray(result)) {
throw new GraphQLError(
`Expected Iterable, but did not find one for field "${info.parentType.name}.${info.fieldName}".`
);
}
const itemType = returnType.ofType;
const completedResults = [];
let containsPromise = false;
let itemPath;
const t0 = new Date().getTime();
let breakIdx;
for (const [idx, item] of result.entries()) {
// Check every Nth item (e.g. 50th) if the elapsed time is larger than X ms.
// If so, break and promise-setImmediate-chain chunks
const elapsed = new Date().getTime() - t0;
if (idx % 20 === 0 && idx > 0 && elapsed > CHUNKIFY_THRESHOLD_MILLIS) {
breakIdx = idx; // Used as chunk size
break;
}
itemPath = addPath(path, idx, undefined);
const completedItem = completeValueOrError(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item
);
if (!containsPromise && completedItem instanceof Promise) {
containsPromise = true;
}
completedResults.push(completedItem);
}
if (breakIdx) {
const startIdx = breakIdx;
const chunkSize = breakIdx;
const completeChunkCallback = (chunk, chunkIdx) => {
return [...chunk.entries()].map(([idx, item]) => {
const pathIdx = startIdx + chunkIdx * chunkSize + idx;
itemPath = addPath(path, pathIdx, undefined);
const completedValue = completeValueOrError(
exeContext,
itemType,
fieldNodes,
info,
itemPath,
item
);
return completedValue;
});
};
const returnPromise = chunkifyPromises(
completedResults,
result,
completeChunkCallback
);
return returnPromise;
} else {
return containsPromise ? Promise.all(completedResults) : completedResults;
}
}
// Monkey-patch the completeListValue function inside the execute module using rewire
rewiredExecuteModule.__set__('completeListValue', completeListValueChunked);
// Use the rewired execute method in the actual server
const rewiredExecute = rewiredExecuteModule.execute;
@lenolib
Copy link
Author

lenolib commented Apr 16, 2021

Fixed a critical missing return statement on line 135

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment