grafast
Version:
Cutting edge GraphQL planning and execution engine
204 lines • 9.08 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.establishOperationPlan = establishOperationPlan;
const tslib_1 = require("tslib");
const lru_1 = tslib_1.__importDefault(require("@graphile/lru"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const constants_js_1 = require("./constants.js");
const constraints_js_1 = require("./constraints.js");
const dev_js_1 = require("./dev.js");
const index_js_1 = require("./index.js");
const timeSource_js_1 = require("./timeSource.js");
const debug = (0, debug_1.default)("grafast:establishOperationPlan");
// How long is a timeout valid for? Here I've set it to 60 seconds.
const TIMEOUT_TIMEOUT = (typeof process !== "undefined" &&
typeof process.env.GRAFAST_TIMEOUT_VALIDITY_MS === "string"
? parseInt(process.env.GRAFAST_TIMEOUT_VALIDITY_MS, 10)
: null) || 60_000;
// OPTIMIZE: we should consider increasing the timeout once the process has been
// running a while (since the JIT should have kicked in) - we could even use
// `setTimeout` to trigger it after certain amount of time elapsed.
/**
* This is a development-only validation to check fragments do, in fact, match
* - even if the objects themselves differ.
*/
function reallyAssertFragmentsMatch(oldFragments, fragments) {
if (oldFragments !== fragments) {
debug("fragments aren't `===` for same operation");
// Consistency check - we assume that if the operation is the same then
// the fragments will be, but this may not be the case depending on if
// GraphQL.js caches the operation node.
const oldKeys = Object.keys(oldFragments).sort();
const newKeys = Object.keys(fragments).sort();
const oldKeyStr = oldKeys.join(",");
const newKeyStr = newKeys.join(",");
if (oldKeyStr.length !== newKeyStr.length) {
throw new Error(`Inconsistency error: operation matches, but fragment keys differ: '${oldKeyStr}' != '${newKeyStr}'.`);
}
for (const key of newKeys) {
if (oldFragments[key] !== fragments[key]) {
throw new Error(`Inconsistency error: operation matches, fragment names match, but fragment '${key}' is not '===' to the previous value.`);
}
}
}
}
// Optimise this away in production.
const assertFragmentsMatch = !dev_js_1.isDev ? dev_js_1.noop : reallyAssertFragmentsMatch;
/**
* Implements the `IsOpPlanCompatible` algorithm.
*
* @remarks Due to the optimisation in `establishOperationPlan`, the schema, document
* and operationName checks have already been performed.
*/
function isOperationPlanResultCompatible(operationPlanResult, variableValues, context, rootValue, errorBehavior) {
if (operationPlanResult.errorBehavior !== errorBehavior)
return false;
const { variableValuesConstraints, contextConstraints, rootValueConstraints, } = operationPlanResult;
if (!(0, constraints_js_1.matchesConstraints)(variableValuesConstraints, variableValues)) {
return false;
}
if (!(0, constraints_js_1.matchesConstraints)(contextConstraints, context)) {
return false;
}
if (!(0, constraints_js_1.matchesConstraints)(rootValueConstraints, rootValue)) {
return false;
}
return true;
}
/**
* Implements the `EstablishOpPlan` algorithm.
*
* @remarks Though EstablishOpPlan accepts document and operationName, we
* instead accept operation and fragments since they're easier to get a hold of
* in GraphQL.js.
*/
function establishOperationPlan(schema, operation, fragments, variableValues, context, rootValue, onError, options) {
const planningTimeout = options.timeouts?.planning;
let cacheByOperation = schema.extensions.grafast?.[constants_js_1.$$cacheByOperation];
let cache = cacheByOperation?.get(operation);
// These two variables to make it easy to trim the linked list later.
let count = 0;
let lastButOneItem = null;
if (cache !== undefined) {
// Dev-only validation
assertFragmentsMatch(cache.fragments, fragments);
let previousItem = null;
let linkedItem = cache.possibleOperationPlans;
while (linkedItem) {
const value = linkedItem.value;
if (isOperationPlanResultCompatible(value, variableValues, context, rootValue, onError)) {
const { error, operationPlan } = value;
if (error != null) {
if (error instanceof index_js_1.SafeError) {
if (error.extensions?.[constants_js_1.$$timeout] != null) {
if (error.extensions[constants_js_1.$$ts] < timeSource_js_1.timeSource.now() - TIMEOUT_TIMEOUT) {
// Remove this out of date timeout
linkedItem = linkedItem.next;
if (previousItem !== null) {
previousItem.next = linkedItem;
}
else {
cache.possibleOperationPlans = linkedItem;
}
continue;
}
if (planningTimeout != null &&
error.extensions[constants_js_1.$$timeout] >= planningTimeout) {
// It was a timeout error - do not retry
throw error;
}
else {
// That's Not My Timeout, let's try again.
}
}
else {
// Not a timeout error - this will always fail in the same way?
throw error;
}
}
else {
// Not a timeout error - this will always fail in the same way?
throw error;
}
}
else {
// Hoist to top of linked list
if (previousItem !== null) {
// Remove linkedItem from existing chain
previousItem.next = linkedItem.next;
// Add rest of chain after linkedItem
linkedItem.next = cache.possibleOperationPlans;
// linkedItem is now head of chain
cache.possibleOperationPlans = linkedItem;
}
// We found a suitable OperationPlan - use that!
return operationPlan;
}
}
count++;
lastButOneItem = previousItem;
previousItem = linkedItem;
linkedItem = linkedItem.next;
}
}
// No suitable OperationPlan found, time to make one.
let operationPlan;
let error;
const variableValuesConstraints = [];
const contextConstraints = [];
const rootValueConstraints = [];
try {
operationPlan = new index_js_1.OperationPlan(schema, operation, fragments, variableValuesConstraints, variableValues, contextConstraints, context, rootValueConstraints, rootValue, onError, options);
}
catch (e) {
error = e;
}
// Store it to the cache
if (!cacheByOperation) {
if (!schema.extensions.grafast) {
schema.extensions.grafast = Object.create(null);
}
cacheByOperation = new lru_1.default({
maxLength: schema.extensions.grafast.operationsCacheMaxLength ?? 500,
});
schema.extensions.grafast[constants_js_1.$$cacheByOperation] = cacheByOperation;
}
const establishOperationPlanResult = {
variableValuesConstraints,
contextConstraints,
rootValueConstraints,
errorBehavior: onError,
...(operationPlan ? { operationPlan } : { error: error }),
};
if (!cache) {
cache = {
fragments,
possibleOperationPlans: {
value: establishOperationPlanResult,
next: null,
},
};
cacheByOperation.set(operation, cache);
}
else {
const max = schema.extensions.grafast.operationOperationPlansCacheMaxLength ?? 50;
if (count >= max) {
// Remove the tail to ensure we never grow too big
lastButOneItem.next = null;
count--;
// LOGGING: we should announce this so that people know there's something that needs fixing in their schema (too much eval?)
}
// Add new operationPlan to top of the linked list.
cache.possibleOperationPlans = {
value: establishOperationPlanResult,
next: cache.possibleOperationPlans,
};
}
if (error !== undefined) {
throw error;
}
else {
return operationPlan;
}
}
//# sourceMappingURL=establishOperationPlan.js.map