UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

592 lines 24.4 kB
"use strict"; var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); exports.isExecutableStep = exports.UnbatchedExecutableStep = exports.ExecutableStep = exports.UnbatchedStep = exports.Step = exports.assertFinalized = exports.$$noExec = void 0; exports.isStep = isStep; exports.assertStep = assertStep; exports.isUnbatchedStep = isUnbatchedStep; exports.isObjectLikeStep = isObjectLikeStep; exports.isListLikeStep = isListLikeStep; exports.isListCapableStep = isListCapableStep; exports.assertListCapableStep = assertListCapableStep; exports.stepHasToSpecifier = stepHasToSpecifier; exports.stepHasToRecord = stepHasToRecord; exports.assertExecutableStep = assertExecutableStep; const tslib_1 = require("tslib"); const chalk_1 = tslib_1.__importDefault(require("chalk")); const tamedevil_1 = tslib_1.__importDefault(require("tamedevil")); const constants_js_1 = require("./constants.js"); const dev_js_1 = require("./dev.js"); const withGlobalLayerPlan_js_1 = require("./engine/lib/withGlobalLayerPlan.js"); const lock_js_1 = require("./engine/lock.js"); const error_js_1 = require("./error.js"); const global_js_1 = require("./global.js"); const inspect_js_1 = require("./inspect.js"); const utils_js_1 = require("./utils.js"); /** * This indicates that a step never executes (e.g. __ItemStep and __ValueStep) * and thus when executed skips direct to reallyCompletedStep. * * @internal */ exports.$$noExec = Symbol("noExec"); const ref_flagError = tamedevil_1.default.ref(error_js_1.flagError, "flagError"); function throwDestroyed() { let message; try { message = `${this} has been destroyed; calling methods on it is no longer possible`; } catch (e) { message = `Step ${this?.id} has been destroyed; calling methods on it is no longer possible`; } throw new Error(message); } function isDeepDepSkippable($dep) { return constants_js_1.$$deepDepSkip in $dep && typeof $dep[constants_js_1.$$deepDepSkip] === "function"; } function reallyAssertFinalized(plan) { if (!plan.isFinalized) { throw new Error(`Step ${plan} is not finalized; did you forget to call \`super.finalize()\` from its \`finalize()\` method?`); } } // Optimise this away in production. exports.assertFinalized = !dev_js_1.isDev ? dev_js_1.noop : reallyAssertFinalized; /** * Executable plans are the plans associated with leaves on the GraphQL tree, * they must be able to execute to return values. */ class Step { static { _a = constants_js_1.$$subroutine, _b = lock_js_1.$$unlock; } constructor() { this[_a] = null; /** * For input values, set `true` if it comes from variables/arguments since * they cannot be modified (even by mutations), set `false` otherwise. * * @internal */ this._isImmutable = false; /** @internal */ this[_b] = undefined; /** * (default = ALL_FLAGS & ~FLAG_NULL) */ this.defaultForbiddenFlags = constants_js_1.DEFAULT_FORBIDDEN_FLAGS; /** @internal */ this._refs = []; /** * If the peerKey of two steps do not match, then they are definitely not * peers. Use this to reduce the load on deduplicate by more quickly * eradicating definitely-not-peers. * * Note: we may well change this to be a function in future, so it's advised * that you don't use this unless you're working inside the graphile/crystal * core codebase. * * @experimental */ this.peerKey = null; this.__trappableFlags = constants_js_1.TRAPPABLE_FLAGS; this.isArgumentsFinalized = false; this.isFinalized = false; this.debug = (0, global_js_1.getDebug)(); const layerPlan = (0, withGlobalLayerPlan_js_1.currentLayerPlan)(); this.layerPlan = layerPlan; this.operationPlan = layerPlan.operationPlan; this._isUnary = true; this._isUnaryLocked = false; // Populated in `OperationPlan` during `finalizeLayerPlans` this._isSelectiveStep = false; this.implicitSideEffectStep = null; this.hasSideEffects ??= false; let hasSideEffects = false; const stepTracker = this.layerPlan.operationPlan.stepTracker; Object.defineProperty(this, "hasSideEffects", { get() { return hasSideEffects; }, set(value) { /** * If steps were created after this step, an this step doesn't depend * on them, then it's no longer safe to change hasSideEffects. */ let nonDependentSteps = null; const maxStepId = stepTracker.stepCount - 1; if (this.id === maxStepId) { // All good - no more steps were created } else { // If the step created them during initialization and is dependent on // them, that's fine too. for (let id = this.id + 1; id <= maxStepId; id++) { const step = stepTracker.getStepById(id); if ((0, utils_js_1.stepADependsOnStepB)(this, step)) continue; if (nonDependentSteps === null) { nonDependentSteps = [step]; } else { nonDependentSteps.push(step); } } } if (nonDependentSteps === null) { hasSideEffects = value; if (value === true) { this.layerPlan.latestSideEffectStep = this; this.operationPlan.resetCache(); } else if (value !== true && hasSideEffects === true) { throw new Error(`Cannot mark ${this} as having no side effects after having set it to have side effects.`); } } else { throw new Error(`Attempted to mark ${this} as having side effects, but other non-dependent steps (${nonDependentSteps .map(String) .join(", ")}) have already been created.`); } }, enumerable: true, configurable: false, }); this.dependencies = []; this.dependencyForbiddenFlags = []; this.dependencyOnReject = []; this.dependencyDataOnly = []; this.dependents = []; this.isOptimized = false; this.allowMultipleOptimizations = false; this._stepOptions = { stream: null, walkIterable: false }; this.store = true; this.polymorphicPaths = (0, withGlobalLayerPlan_js_1.currentPolymorphicPaths)(); // Important: MUST come after `this.layerPlan = ...` this.id = this.layerPlan._addStep(this); } /** * Generally you should only use this once the dependencies of a step are * established, if you use it beforehand and it returns `true` then adding a * non-unary dependency later will result in an error. */ getAndFreezeIsUnary() { this._isUnaryLocked = true; return this._isUnary; } withMyLayerPlan(callback) { return (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(this.layerPlan, this.polymorphicPaths, null, callback); } /** @experimental */ withLayerPlan(callback) { return (0, withGlobalLayerPlan_js_1.withGlobalLayerPlan)(this.layerPlan, this.polymorphicPaths, null, callback); } getStep(id) { return this.layerPlan.getStep(id, this); } getDepOptions(depId) { this._assertAccessAllowed(depId); return this._getDepOptions(depId); } _getDepOptions(depId) { const step = this.dependencies[depId]; const forbiddenFlags = this.dependencyForbiddenFlags[depId]; const onReject = this.dependencyOnReject[depId]; const dataOnly = this.dependencyDataOnly[depId]; const acceptFlags = constants_js_1.ALL_FLAGS & ~forbiddenFlags; return { step, acceptFlags, onReject, dataOnly }; } /** * @internal */ _assertAccessAllowed(depId) { const phase = this.operationPlan.phase; if (phase !== "optimize" && phase !== "plan") return; const step = this.dependencies[depId]; const dataOnly = this.dependencyDataOnly[depId]; if (dataOnly) { throw new Error(`Dependency ${depId} (${step}) of ${this} was declared as "data only", so retrieval is forbidden.`); } } getDep(_depId, _throwOnFlagged = false) { // This gets replaced when `__FlagStep` is loaded. Were we on ESM we could // just put the code here, but since we're not we have to avoid the // circular dependency. throw new Error(`Grafast failed to load correctly`); } maybeGetDep(depId, throwOnFlagged = false) { return depId == null ? null : throwOnFlagged ? this.getDep(depId, true) : this.getDep(depId); } getDepOrConstant(_depId, _fallback) { // This gets replaced when `constant` is loaded. Were we on ESM we could // just put the code here, but since we're not we have to avoid the // circular dependency. throw new Error(`Grafast failed to load correctly`); } /** * Like getDep, except it skips over __ItemStep and similar steps to get to * where the parent really is. * * @experimental */ getDepDeep(depId) { let $dep = this.getDep(depId); // Walk up the tree, looking for the source of this record. We know that // __ItemStep and __ListTransformStep are safe to walk through, but other // classes may not be. while (isDeepDepSkippable($dep)) { $dep = $dep[constants_js_1.$$deepDepSkip](); } return $dep; } /** * Cache a generated step by a given identifier (cacheKey) such that we don't * need to regenerate it on future calls, significantly reducing the load on * deduplication later. * * @experimental */ cacheStep(actionKey, cacheKey, cb) { return this.operationPlan.cacheStep(this, actionKey, cacheKey, cb); } toString() { let meta; try { // If we log out too early, the meta function might fail. meta = this.toStringMeta(); } catch (e) { // Ignore } return chalk_1.default.bold.blue(`${this.constructor.name.replace(/Step$/, "")}${this.layerPlan.id === 0 ? "" : chalk_1.default.grey(`{${this.layerPlan.id}}`)}${this._isUnary ? "➊" : ""}${this._stepOptions.stream != null ? "@s" : ""}${meta != null && meta.length ? chalk_1.default.grey(`<${meta}>`) : ""}[${(0, inspect_js_1.inspect)(this.id, { colors: true })}]`); } /** * This metadata will be merged into toString when referencing this plan. */ toStringMeta() { return null; } planJSONExtra() { return undefined; } /** * **IF IN DOUBT, USE `.addDependency()` INSTEAD! * * This **DANGEROUS** method allows you to create a reference to another * step. A reference is like a dependency except it has no runtime impact - * the data is not passed into execute. In general you should only add * references to steps that you directly or indirectly depend on (e.g. an * ancestor step) so that you may communicate with said step during * `optimize` (for example). Sometimes it's acceptable to reference steps * that you don't transitively depend on; in those cases you're permitted to * pass an `allowIndirectReason` to explain to yourself and others why you * are breaking these rules. * * @experimental */ addRef(rawStep, allowIndirectReason = null) { const step = this.operationPlan.stepTracker.getStepById(rawStep.id); if (dev_js_1.isDev && !allowIndirectReason && !(0, utils_js_1.stepADependsOnStepB)(this, step)) { const allRefs = (step, allDepIds = new Set()) => { allDepIds.add(step.id); for (const dep of step.dependencies) { allRefs(dep, allDepIds); } return allDepIds; }; const refs1 = allRefs(this); const refs2 = allRefs(step); // const commonRefs = refs1.union(refs2); const commonRefs = new Set([...refs1].filter((v) => refs2.has(v))); const printDeps = (step, depth) => { const isCommon = commonRefs.has(step.id); const stepDesc = isCommon ? chalk_1.default.bgWhite.black `<${step}>` : String(step); return `${" ".repeat(depth)}${stepDesc}${!isCommon && step.dependencies.length > 0 ? `:\n${step.dependencies.map((d) => printDeps(d, depth + 1)).join("\n")}` : ""}`; }; throw new Error(`${this} has created a reference to ${step} which is not depended on (directly or indirectly) Self: ${printDeps(this, 1)} Reference: ${printDeps(step, 1)} `); } else if (!(0, utils_js_1.stepAMayDependOnStepB)(this, step)) { throw new Error(`${this}.addRef(${step}) forbidden: invalid plan heirarchy`); } return this._refs.push(step.id) - 1; } /** * Allows you to dereference a reference made via `addRef`. Will resolve to * whatever that step is now (or null if not found). Note that referenced * referenced steps may change to a new step instance due to lifecycle * methods (e.g. deduplicate) or even to an entirely separate class * altogether (e.g. due to optimize). References are not guaranteed to be * honoured. * * @experimental */ getRef(refIdx) { if (!["plan", "validate", "optimize"].includes(this.operationPlan.phase)) { throw new Error(`Cannot call ${this}.getRef() when the operation plan phase is ${this.operationPlan.phase}; getRef may only be called during planning.`); } if (refIdx == null) return null; return (this.operationPlan.stepTracker.getStepById(this._refs[refIdx]) ?? null); } canAddDependency(step) { return (0, utils_js_1.stepAMayDependOnStepB)(this, step); } _addDependency(options) { if (options.step.layerPlan.id > this.layerPlan.id) { throw new Error(`Cannot add dependency ${options.step} to ${this} since the former is in a deeper layerPlan (${options.step.layerPlan} deeper than ${this.layerPlan}; creates a catch-22)`); } return this.operationPlan.stepTracker.addStepDependency(this, options); } addDependency(stepOrOptions) { const options = { dataOnly: false, skipDeduplication: false, ...(stepOrOptions instanceof Step ? { step: stepOrOptions } : stepOrOptions), }; return this._addDependency(options); } addDataDependency(stepOrOptions) { const opts = stepOrOptions instanceof Step ? { step: stepOrOptions } : stepOrOptions; return this._addDependency({ dataOnly: true, skipDeduplication: false, ...opts, }); } // Currently identical to addDependency addStrongDependency(stepOrOptions) { return this._addDependency({ dataOnly: false, skipDeduplication: false, ...(stepOrOptions instanceof Step ? { step: stepOrOptions } : stepOrOptions), }); } /** * Adds "unary" dependencies; in `execute({count, values})` you'll receive a * `values[index]` (where `index` is the return value of this function) with * `isBatch = false` so you can use the `values[index].value` property * directly. */ addUnaryDependency(stepOrOptions) { const options = stepOrOptions instanceof Step ? { step: stepOrOptions } : stepOrOptions; if (options.step.layerPlan.id > this.layerPlan.id) { throw new Error(`Cannot add a unary dependency on ${options.step} to ${this} since the former is in a deeper layerPlan (creates a catch-22)`); } return this.operationPlan.stepTracker.addStepUnaryDependency(this, options); } finalize() { if (typeof this.isSyncAndSafe !== "boolean") { // Take a guess if (!dev_js_1.isDev) { this.isSyncAndSafe = false; } else if (this.execute.constructor.name === "AsyncFunction") { this.isSyncAndSafe = false; } else { console.warn(`${this} uses a regular (non-async) function for 'execute'; if it never returns a promise and the list it returns never includes a promise then setting \`${this}.isSyncAndSafe = true\` will improve performance. If this is not true, set \`${this}.isSyncAndSafe = false\` to dismiss this message.`); this.isSyncAndSafe = false; } } if (this.isSyncAndSafe === true && this.execute.constructor.name === "AsyncFunction") { throw new Error(`${this} claims to be synchronous, however the execute method is asynchronous`); } if (!this.isFinalized) { this.isFinalized = true; } else { throw new Error(`Step ${this} has already been finalized - do not call \`finalize()\` from user code!`); } } /** * This function will be called with 'execution details', an object containing: * * - `count`: the number of entries in the batch that's being executed * - `values`: a tuple representing the runtime values of the steps * dependencies; each value in the tuple is an object, either a batch object * containing a list of size `count` containing the values, or a unary * object containing the single value common to all entries. * - `indexMap`: helper function to map over each index from `0` to `count-1`, * returning the resulting array. * - `indexForEach`: as `indexMap`, but without the array result. * - `meta`: [experimental] * * `execute` must return a list with `count` entries, where each value in the * list relates to the result of executing this plan for the corresponding * entry in each of the entries in the `values` tuple. * * IMPORTANT: it is up to the execute function to cache/memoize results as * appropriate for performance, this can be done via the `meta` object. * * The `meta` object is an empty object stored to `grafastContext.metaByPlan` * that can be used to store anything this plan needs. We recommend that you * add attributes to meta for each purpose (e.g. use `meta.cache` for * memoizing results) so that you can expand your usage of meta in future. */ /* abstract */ execute(details) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions details; throw new Error(`${this} has not implemented an 'execute' method`); } destroy() { // Break ourself enough that if lifecycle methods are attempted an error // will be thrown. This should help weed out bugs where steps are processed // even after they have been removed/deduped. this.addDependency = throwDestroyed; this.deduplicate = throwDestroyed; this.deduplicatedWith = throwDestroyed; this.optimize = throwDestroyed; this.finalize = throwDestroyed; this.execute = throwDestroyed; } } exports.Step = Step; exports.ExecutableStep = Step; function _buildOptimizedExecuteV2Expression(depCount, isSyncAndSafe) { const identifiers = []; for (let i = 0; i < depCount; i++) { identifiers.push(tamedevil_1.default.identifier(`value${i}`)); } const tryOrNot = (inFrag) => { if (isSyncAndSafe) { return inFrag; } else { return (0, tamedevil_1.default) `\ try { ${tamedevil_1.default.indent(inFrag)} } catch (e) { results[i] = ${ref_flagError}(e); }\ `; } }; return (0, tamedevil_1.default) `\ (function execute({ count, values: [${tamedevil_1.default.join(identifiers, ", ")}], extra, }) { const results = []; for (let i = 0; i < count; i++) { ${tryOrNot((0, tamedevil_1.default) `\ results[i] = this.unbatchedExecute(extra, ${tamedevil_1.default.join(identifiers.map((identifier) => (0, tamedevil_1.default) `${identifier}.at(i)`), ", ")});\ `)} } return results; })`; } const MAX_DEPENDENCIES_TO_CACHE = 10; const unsafeCache = []; const safeCache = []; tamedevil_1.default.batch(() => { for (let i = 0; i <= MAX_DEPENDENCIES_TO_CACHE; i++) { const depCount = i; const unsafeExpression = _buildOptimizedExecuteV2Expression(depCount, false); tamedevil_1.default.runInBatch(unsafeExpression, (fn) => { unsafeCache[depCount] = fn; }); const safeExpression = _buildOptimizedExecuteV2Expression(depCount, true); tamedevil_1.default.runInBatch(safeExpression, (fn) => { safeCache[depCount] = fn; }); } }); function buildOptimizedExecute(depCount, isSyncAndSafe, callback) { // Try and satisfy from cache const cache = isSyncAndSafe ? safeCache : unsafeCache; if (depCount <= MAX_DEPENDENCIES_TO_CACHE) { callback(cache[depCount]); return; } // Build it const expression = _buildOptimizedExecuteV2Expression(depCount, isSyncAndSafe); tamedevil_1.default.runInBatch(expression, (fn) => { callback(fn); }); } class UnbatchedStep extends Step { static { this.$$export = { moduleName: "grafast", exportName: "UnbatchedStep", }; } finalize() { if (this.execute === UnbatchedStep.prototype.execute) { // If they've not replaced 'execute', use our optimized form buildOptimizedExecute(this.dependencies.length, this.isSyncAndSafe, (fn) => { this.execute = fn; }); } super.finalize(); } execute({ indexMap, values, extra, }) { const depCount = this.dependencies.length; return indexMap((i) => { try { const tuple = []; for (let j = 0; j < depCount; j++) { tuple[j] = values[j].at(i); } return this.unbatchedExecute(extra, ...tuple); } catch (e) { return (0, error_js_1.flagError)(e); } }); } } exports.UnbatchedStep = UnbatchedStep; exports.UnbatchedExecutableStep = UnbatchedStep; function isStep(step) { return step instanceof Step; } function assertStep(step) { if (!isStep(step)) { throw new Error(`Expected a step, but received something else: ${(0, inspect_js_1.inspect)(step)}`); } } function isUnbatchedStep(step) { return isStep(step) && typeof step.unbatchedExecute === "function"; } function isObjectLikeStep(plan) { return "get" in plan && typeof plan.get === "function"; } function isListLikeStep(plan) { return "at" in plan && typeof plan.at === "function"; } function isListCapableStep(plan) { return "listItem" in plan && typeof plan.listItem === "function"; } function assertListCapableStep(plan, pathDescription) { if (!isListCapableStep(plan)) { throw new Error(`The plan returned from '${pathDescription}' should be a list capable plan, but ${plan} does not implement the 'listItem' method.`); } } function stepHasToSpecifier($step) { return typeof $step.toSpecifier === "function"; } function stepHasToRecord($step) { return typeof $step.toRecord === "function"; } /** @deprecated Use isStep instead */ exports.isExecutableStep = isStep; /** @deprecated Use isStep instead */ function assertExecutableStep(step) { return assertStep(step); } //# sourceMappingURL=step.js.map